diff --git a/apps/multisig/package.json b/apps/multisig/package.json index 1d4791cf..1228d6a7 100644 --- a/apps/multisig/package.json +++ b/apps/multisig/package.json @@ -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", @@ -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", diff --git a/apps/multisig/src/App.tsx b/apps/multisig/src/App.tsx index 229aae71..ee0999b1 100644 --- a/apps/multisig/src/App.tsx +++ b/apps/multisig/src/App.tsx @@ -32,43 +32,52 @@ import { SkeletonLayout } from './layouts/SkeletonLayout' import { Helmet } from 'react-helmet' import { CONFIG } from '@lib/config' -const App: React.FC = () => ( - - - chain.genesisHash)} - coingeckoApiUrl="https://coingecko.talismn.workers.dev" - > - - - }> - - - {CONFIG.IS_POLKADOT_MULTISIG ? 'Polkadot Multisig by Signet' : 'Signet'} - - - {/* */} - - - - - - - - - - - {t => } - - - - - - - - - -) +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 ( + + + chain.genesisHash)} + coingeckoApiUrl="https://coingecko.talismn.workers.dev" + > + + + }> + + + {CONFIG.IS_POLKADOT_MULTISIG ? 'Polkadot Multisig by Signet' : 'Signet'} + + + {/* */} + + + + + + + + + + + + + + {t => } + + + + + + + + + + ) +} export default App diff --git a/apps/multisig/src/components/AmountFlexibleInput/index.tsx b/apps/multisig/src/components/AmountFlexibleInput/index.tsx index 88a35db9..32484779 100644 --- a/apps/multisig/src/components/AmountFlexibleInput/index.tsx +++ b/apps/multisig/src/components/AmountFlexibleInput/index.tsx @@ -15,6 +15,7 @@ type Props = { setAmount: (a: string) => void setSelectedToken?: (t: BaseToken) => void amountPerBlockBn?: BN + placeholder?: string } export const AmountFlexibleInput: React.FC = ({ @@ -22,6 +23,7 @@ export const AmountFlexibleInput: React.FC = ({ tokens, selectedToken, leadingLabel, + placeholder, setAmount, setSelectedToken, }) => { @@ -79,7 +81,7 @@ export const AmountFlexibleInput: React.FC = ({
t.type === 'substrate-native') if (!token) throw Error(`Chain does not have a native token!`) diff --git a/apps/multisig/src/domains/referenda/index.ts b/apps/multisig/src/domains/referenda/index.ts index e4d8aa85..876915ed 100644 --- a/apps/multisig/src/domains/referenda/index.ts +++ b/apps/multisig/src/domains/referenda/index.ts @@ -22,8 +22,13 @@ 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 @@ -31,6 +36,14 @@ export type VoteDetails = { } } +export type VoteDetailsForm = Omit & { + details: { + Standard: StandardVoteParams + Split: SplitVoteParams + SplitAbstain: SplitAbstainVoteParams + } +} + type ReferendumBasicInfo = { index: number isOngoing: boolean @@ -56,11 +69,18 @@ export const defaultVoteDetails: Required = { }, } +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() + const [isLoading, setIsLoading] = useState(true) const apiLoadable = useRecoilValueLoadable(pjsApiSelector(chain.genesisHash)) const isPalletSupported = useMemo(() => { @@ -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) @@ -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]) @@ -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) */ diff --git a/apps/multisig/src/hooks/queries/useGetReferendums.ts b/apps/multisig/src/hooks/queries/useGetReferendums.ts new file mode 100644 index 00000000..1413b3c2 --- /dev/null +++ b/apps/multisig/src/hooks/queries/useGetReferendums.ts @@ -0,0 +1,51 @@ +import { useQueries } from '@tanstack/react-query' +import { SupportedChainIds } from '@domains/chains/generated-chains' + +const supportedChains: Partial> = { + '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 => { + 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), + } + }, + }) +} diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/ConvictionsDropdown.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/ConvictionsDropdown.tsx index 9218394c..56173291 100644 --- a/apps/multisig/src/layouts/NewTransaction/Vote/ConvictionsDropdown.tsx +++ b/apps/multisig/src/layouts/NewTransaction/Vote/ConvictionsDropdown.tsx @@ -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> } const CONVICTIONS = [1, 2, 4, 8, 16, 32].map((lock, index): [value: number, duration: number] => [index + 1, lock]) @@ -20,8 +21,17 @@ 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 = ({ conviction, onChange }) => { +const ConvictionsDropdown: React.FC = ({ conviction, setVoteDetails }) => { const options = createConvictionsOpts() + + const handleChange = (value: number) => { + setVoteDetails(prev => { + const updatedVal = { ...prev } + updatedVal.details.Standard.vote.conviction = value + return updatedVal + }) + } + return ( - {ongoingReferendums?.map(referendum => ( - - ))} + {ongoingReferendums?.map(referendum => { + const { title } = referendumsData?.find(ref => ref?.referendumIndex === referendum.index) || {} + const headlineText = title ? `Proposal #${referendum.index} - ${title}` : `Proposal #${referendum.index}` + return + })} ) } diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/VoteOptions.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/VoteOptions.tsx index 30f80a1f..e0efd3a8 100644 --- a/apps/multisig/src/layouts/NewTransaction/Vote/VoteOptions.tsx +++ b/apps/multisig/src/layouts/NewTransaction/Vote/VoteOptions.tsx @@ -1,13 +1,13 @@ import { css } from '@emotion/css' import { Button } from '@talismn/ui' -import { VoteDetails, defaultVoteDetails } from '@domains/referenda' +import { VoteDetailsForm } from '@domains/referenda' type Props = { - value: VoteDetails['details'] - onChange: (v: VoteDetails['details']) => void + voteDetails: VoteDetailsForm + setVoteDetails: React.Dispatch> } -const VoteOptions: React.FC = ({ value, onChange }) => { +const VoteOptions: React.FC = ({ voteDetails, setVoteDetails }) => { return (
= ({ value, onChange }) => { > +
) } diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/VotingForm.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/VotingForm.tsx index 267d843e..97b88d9e 100644 --- a/apps/multisig/src/layouts/NewTransaction/Vote/VotingForm.tsx +++ b/apps/multisig/src/layouts/NewTransaction/Vote/VotingForm.tsx @@ -1,8 +1,10 @@ +import React from 'react' import { Button } from '@components/ui/button' import { BaseToken } from '@domains/chains' import { useSelectedMultisig } from '@domains/multisig' -import { VoteDetails, isVoteDetailsComplete } from '@domains/referenda' +import { VoteDetailsForm, isVoteDetailsComplete } from '@domains/referenda' import VoteOptions from './VoteOptions' +import VoteSplitAbstain from './mode/VoteSplitAbstain' import VoteStandard from './mode/VoteStandard' import { ProposalsDropdown } from './ProposalsDropdown' import { hasPermission } from '@domains/proxy/util' @@ -12,20 +14,16 @@ import { Vote } from '@talismn/icons' type Props = { token?: BaseToken - voteDetails: VoteDetails - onChange: (v: VoteDetails) => void + voteDetails: VoteDetailsForm + setVoteDetails: React.Dispatch> onNext: () => void } -const VotingForm: React.FC = ({ onChange, onNext, token, voteDetails }) => { +const VotingForm: React.FC = ({ setVoteDetails, onNext, token, voteDetails }) => { const [multisig] = useSelectedMultisig() const { hasDelayedPermission, hasNonDelayedPermission } = hasPermission(multisig, 'governance') - const handleDetailsChange = (details: VoteDetails['details']) => { - onChange({ referendumId: voteDetails.referendumId, details }) - } - return ( <> } title="Vote" /> @@ -33,13 +31,15 @@ const VotingForm: React.FC = ({ onChange, onNext, token, voteDetails }) = onChange({ ...voteDetails, referendumId })} + onChange={referendumId => setVoteDetails(prev => ({ ...prev, referendumId }))} /> - - {voteDetails.details.Standard ? ( - - ) : // TODO: add UI for Abstain and Split votes - null} + + {voteDetails.convictionVote === 'Standard' ? ( + + ) : ( + // TODO: add UI for Split votes + + )} {hasNonDelayedPermission === false ? ( hasDelayedPermission ? ( diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/index.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/index.tsx index 6bd511d6..c36d0bf9 100644 --- a/apps/multisig/src/layouts/NewTransaction/Vote/index.tsx +++ b/apps/multisig/src/layouts/NewTransaction/Vote/index.tsx @@ -2,8 +2,8 @@ import { useMemo, useState } from 'react' import { SplitAbstainVoteParams, StandardVoteParams, - VoteDetails, - defaultVoteDetails, + VoteDetailsForm, + defaultVote, isVoteDetailsComplete, isVoteFeatureSupported, } from '@domains/referenda' @@ -14,15 +14,15 @@ import { SplitVoteParams } from '@domains/referenda' import { pjsApiSelector } from '@domains/chains/pjs-api' import { TransactionSidesheet } from '@components/TransactionSidesheet' import { useToast } from '@components/ui/use-toast' +import PendingVotes from './PendingVotes' const VoteAction: React.FC = () => { const multisig = useRecoilValue(selectedMultisigState) const apiLoadable = useRecoilValueLoadable(pjsApiSelector(multisig.chain.genesisHash)) const tokens = useRecoilValueLoadable(selectedMultisigChainTokensState) const [reviewing, setReviewing] = useState(false) - const [voteDetails, setVoteDetails] = useState({ - details: { Standard: defaultVoteDetails.Standard }, - }) + const [removeVoteId, setRemoveVoteId] = useState(null) + const [voteDetails, setVoteDetails] = useState(defaultVote) const { toast } = useToast() // instead of allowing the user to select any token later on, we just use the first native token of the chain @@ -30,43 +30,60 @@ const VoteAction: React.FC = () => { tokens.state === 'hasValue' ? tokens.contents.find(({ type }) => type === 'substrate-native') : undefined const isPalletSupported = apiLoadable.state === 'hasValue' ? isVoteFeatureSupported(apiLoadable.contents) : undefined + const isApiReady = + apiLoadable.state === 'hasValue' && + apiLoadable.contents.tx && + !!apiLoadable.contents.tx?.convictionVoting && + !!isPalletSupported + const extrinsic = useMemo(() => { - if ( - apiLoadable.state !== 'hasValue' || - !isPalletSupported || - voteDetails.referendumId === undefined || - !apiLoadable.contents.tx || - !apiLoadable.contents.tx?.convictionVoting || - !nativeToken || - !isVoteDetailsComplete(voteDetails) - ) - return + if (!isApiReady) return try { - const voteExtrinsic = apiLoadable.contents.tx?.convictionVoting.vote( - voteDetails.referendumId, - voteDetails.details as - | { Standard: StandardVoteParams } - | { Split: SplitVoteParams } - | { SplitAbstain: SplitAbstainVoteParams } - ) - return voteExtrinsic + let extrinsicAction + + if (removeVoteId) { + extrinsicAction = apiLoadable.contents.tx?.convictionVoting.removeVote(null, removeVoteId) + } else if (isVoteDetailsComplete(voteDetails)) { + const selectedConviction = { [voteDetails.convictionVote!]: voteDetails.details[voteDetails.convictionVote!] } + + extrinsicAction = apiLoadable.contents.tx?.convictionVoting.vote( + voteDetails.referendumId!, + selectedConviction as + | { Standard: StandardVoteParams } + | { Split: SplitVoteParams } + | { SplitAbstain: SplitAbstainVoteParams } + ) + } + + return extrinsicAction } catch (e) { console.error(e) } - }, [apiLoadable.contents.tx, apiLoadable.state, isPalletSupported, nativeToken, voteDetails]) + }, [apiLoadable.contents.tx?.convictionVoting, isApiReady, removeVoteId, voteDetails]) + + const handleOnRemoveVote = (referendumId: string) => { + setRemoveVoteId(referendumId) + setReviewing(true) + } const transactionName = useMemo(() => { - const vote = voteDetails.details.Standard?.vote.aye ? 'Aye' : 'Nay' + if (removeVoteId) { + return `Remove vote on Proposal #${removeVoteId}` + } + let vote = voteDetails.details.Standard.vote.aye ? 'Aye' : 'Nay' + if (voteDetails.convictionVote === 'SplitAbstain') { + vote = 'Abstain' + } return `Vote ${vote} on Proposal #${voteDetails.referendumId}` - }, [voteDetails]) + }, [removeVoteId, voteDetails.convictionVote, voteDetails.details.Standard.vote.aye, voteDetails.referendumId]) return ( -
+
setReviewing(true)} /> {extrinsic && ( @@ -80,11 +97,15 @@ const VoteAction: React.FC = () => { description: e.message, }) }} - onClose={() => setReviewing(false)} + onClose={() => { + setReviewing(false) + setRemoveVoteId(null) + }} open={reviewing} /> )}
+
) } diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteSplitAbstain.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteSplitAbstain.tsx new file mode 100644 index 00000000..a2b58d36 --- /dev/null +++ b/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteSplitAbstain.tsx @@ -0,0 +1,54 @@ +import { BaseToken } from '@domains/chains' +import { VoteDetailsForm } from '@domains/referenda' +import { AmountFlexibleInput } from '@components/AmountFlexibleInput' +import { parseUnits } from '@util/numbers' +import BN from 'bn.js' + +interface VoteSplitAbstainProps { + token?: BaseToken + setVoteDetails: React.Dispatch> +} + +enum VoteDirection { + Abstain = 'abstain', + Aye = 'aye', + Nay = 'nay', +} + +export default function VoteSplitAbstain({ token, setVoteDetails }: VoteSplitAbstainProps) { + const handleAmountChange = (amount: string, field: VoteDirection) => { + if (!token) return + + let balance = new BN(0) + try { + balance = parseUnits(amount, token.decimals) + } catch (e) { + // if failed to parse, input is likely '' or invalid number, hence we default to BN(0) + balance = new BN(0) + } + + setVoteDetails(prev => { + const prevBal = prev.details.SplitAbstain[field] + if (balance.eq(prevBal)) return prev + + const updatedVal = { ...prev } + updatedVal.details.SplitAbstain[field] = balance + return updatedVal + }) + } + + return ( + <> + {Object.values(VoteDirection).map(direction => ( + handleAmountChange(amount, direction)} + /> + ))} + + ) +} diff --git a/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteStandard.tsx b/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteStandard.tsx index a1922bea..4fa3fa8c 100644 --- a/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteStandard.tsx +++ b/apps/multisig/src/layouts/NewTransaction/Vote/mode/VoteStandard.tsx @@ -1,17 +1,18 @@ +import React from 'react' import { AmountFlexibleInput } from '@components/AmountFlexibleInput' import { BaseToken } from '@domains/chains' -import { StandardVoteParams, VoteDetails } from '@domains/referenda' +import { StandardVoteParams, VoteDetailsForm } from '@domains/referenda' import ConvictionsDropdown from '../ConvictionsDropdown' import { parseUnits } from '@util/numbers' import BN from 'bn.js' type Props = { token?: BaseToken - onChange: (v: VoteDetails['details']) => void + setVoteDetails: React.Dispatch> params: StandardVoteParams } -const VoteStandard = ({ params, onChange, token }: Props) => { +const VoteStandard = ({ params, setVoteDetails, token }: Props) => { const handleAmountChange = (amount: string) => { if (!token) return @@ -23,23 +24,11 @@ const VoteStandard = ({ params, onChange, token }: Props) => { balance = new BN(0) } if (balance.eq(params.balance)) return - onChange({ - Standard: { - balance, - vote: params.vote, - }, - }) - } - const handleConvictionChange = (conviction: number) => { - onChange({ - Standard: { - balance: params.balance, - vote: { - conviction, - aye: params.vote.aye, - }, - }, + setVoteDetails(prev => { + const updatedVal = { ...prev } + updatedVal.details.Standard.balance = balance + return updatedVal }) } @@ -52,7 +41,7 @@ const VoteStandard = ({ params, onChange, token }: Props) => { leadingLabel="Amount to vote" setAmount={handleAmountChange} /> - + ) } diff --git a/apps/multisig/src/layouts/Overview/Transactions/VoteTransactionDetails.tsx b/apps/multisig/src/layouts/Overview/Transactions/VoteTransactionDetails.tsx index 2deb9815..be82bb13 100644 --- a/apps/multisig/src/layouts/Overview/Transactions/VoteTransactionDetails.tsx +++ b/apps/multisig/src/layouts/Overview/Transactions/VoteTransactionDetails.tsx @@ -1,54 +1,71 @@ +import { useMemo } from 'react' import { Transaction, TransactionType } from '@domains/multisig' import { css } from '@emotion/css' import { ExternalLink } from '@talismn/icons' import AmountRow from '@components/AmountRow' import { createConvictionsOpts } from '../../NewTransaction/Vote/ConvictionsDropdown' import { VoteDetails } from '../../../domains/referenda' +import clsx from 'clsx' +import BN from 'bn.js' type Props = { t: Transaction } -// TODO: make this component support UI for Abstain and Split vote types -const VotePill: React.FC<{ details: VoteDetails['details'] }> = ({ details }) => ( -
+// TODO: make this component support UI for Split vote types +export const VotePill: React.FC<{ voteDetails: VoteDetails }> = ({ voteDetails }) => { + const { method, convictionVote } = voteDetails + + const getLabelAndColor = (): Record => { + if (method === 'removeVote') { + return { label: 'Remove', color: 'bg-white' } + } else if (convictionVote === 'SplitAbstain') { + return { label: 'Abstain', color: 'bg-[#B9D9FF]' } + } else if (voteDetails.details.Standard?.vote.aye) { + return { label: 'Aye', color: 'bg-[#21C91D]' } + } + return { label: 'Nay', color: 'bg-[#F34A4A]' } + } + + const { label, color } = getLabelAndColor() + + return (
-

{details.Standard?.vote.aye ? 'Aye' : 'Nay'}

-
-) + > +
+

{label}

+
+ ) +} export const VoteTransactionHeaderContent: React.FC = ({ t }) => { if (t.decoded?.type !== TransactionType.Vote || !t.decoded.voteDetails) return null - const { details, token } = t.decoded.voteDetails + const { convictionVote, details, token } = t.decoded.voteDetails + const { Standard, SplitAbstain } = details - if (!details.Standard) return null + const amount = + convictionVote === 'SplitAbstain' + ? Object.values(SplitAbstain!).reduce((acc, balance) => acc.add(balance), new BN(0)) + : new BN(0) return (
- +
@@ -57,13 +74,58 @@ export const VoteTransactionHeaderContent: React.FC = ({ t }) => { } export const VoteExpandedDetails: React.FC = ({ t }) => { - if (t.decoded?.type !== TransactionType.Vote || !t.decoded.voteDetails) return null + const renderExpandedDetails = useMemo(() => { + if (t.decoded?.type !== TransactionType.Vote || !t.decoded.voteDetails) return null + + const convictionsOptions = createConvictionsOpts() + const { + details: { Standard, SplitAbstain }, + token, + } = t?.decoded?.voteDetails - const { details, token, referendumId } = t.decoded.voteDetails - const convictionsOptions = createConvictionsOpts() + if (Standard) { + return ( + <> +
+

Vote value

+ +
+
+

Conviction

+

{convictionsOptions[Standard.vote.conviction]?.headlineText ?? 'Unknown'}

+
+ + ) + } + if (SplitAbstain) { + return ( + <> + {Object.entries(SplitAbstain) + .map(([key, balance]) => ( +
+

{key} vote value

+ +
+ )) + .reverse()} + + ) + } + }, [t.decoded?.type, t?.decoded?.voteDetails]) - if (!details.Standard) return null + if (t.decoded?.type !== TransactionType.Vote || !t.decoded.voteDetails) return null + const { referendumId } = t?.decoded?.voteDetails const name = `Referendum #${referendumId}` return ( @@ -85,21 +147,9 @@ export const VoteExpandedDetails: React.FC = ({ t }) => { ) : (

{name}

)} - -
-
-

Vote value

- -
-
-

Conviction

-

{convictionsOptions[details.Standard.vote.conviction]?.headlineText ?? 'Unknown'}

+
+ {renderExpandedDetails}
) diff --git a/yarn.lock b/yarn.lock index e366c2b7..433fb67e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2928,7 +2928,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" dependencies: @@ -10487,6 +10487,9 @@ __metadata: "@talismn/siws": 0.0.8 "@talismn/ui": "workspace:^" "@talismn/util": ^0.2.0 + "@tanstack/eslint-plugin-query": ^5.35.6 + "@tanstack/react-query": ^5.37.1 + "@tanstack/react-query-devtools": ^5.37.1 "@tanstack/react-table": ^8.16.0 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^13.4.0 @@ -10759,6 +10762,54 @@ __metadata: languageName: unknown linkType: soft +"@tanstack/eslint-plugin-query@npm:^5.35.6": + version: 5.35.6 + resolution: "@tanstack/eslint-plugin-query@npm:5.35.6" + dependencies: + "@typescript-eslint/utils": ^6.20.0 + peerDependencies: + eslint: ^8.0.0 + checksum: b95ed8541be636e5af175669a658da6cbb6eb849ef9f980152e7e4f684906ca266076ea18036d6dabaccd468b5a8219eb25752512d77e82d99077908772eaac8 + languageName: node + linkType: hard + +"@tanstack/query-core@npm:5.36.1": + version: 5.36.1 + resolution: "@tanstack/query-core@npm:5.36.1" + checksum: 9ac291eff02bcfd20c8460ce04683b2551d6b51635120eb9efb1be37c6839e8428ddf85ef94bf1b71ed162b5e5cd5dfef64e14f089058d725f7cee4d8ee07d0a + languageName: node + linkType: hard + +"@tanstack/query-devtools@npm:5.37.1": + version: 5.37.1 + resolution: "@tanstack/query-devtools@npm:5.37.1" + checksum: 73ef22b9a8a747fe8187c79e6673ff55f7feba85d57f95fd5bdb1c93f57ba57a19cc4f3f81cfe81adb18b73753e4e9046d3684fbc5bdd757d869ac274cf6f950 + languageName: node + linkType: hard + +"@tanstack/react-query-devtools@npm:^5.37.1": + version: 5.37.1 + resolution: "@tanstack/react-query-devtools@npm:5.37.1" + dependencies: + "@tanstack/query-devtools": 5.37.1 + peerDependencies: + "@tanstack/react-query": ^5.37.1 + react: ^18.0.0 + checksum: b73191d6e582f219522a7b9bd855ef21d9af37bab5ece7592fc98f0768f65fcf0e4116ce18f8baab2fd7c779ddf9277d09d221c1fa5d9aebad11e5300aa917c0 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.37.1": + version: 5.37.1 + resolution: "@tanstack/react-query@npm:5.37.1" + dependencies: + "@tanstack/query-core": 5.36.1 + peerDependencies: + react: ^18.0.0 + checksum: 4bac8aeed5ab6718ba3eb527542784c9475e3eda5f2602eea1479f672b5dfcfb48289b4a26ccf5ef3cba793e4b5cc00070b33e85417dda6ecbe11a09d85ad1c5 + languageName: node + linkType: hard + "@tanstack/react-table@npm:^8.16.0": version: 8.16.0 resolution: "@tanstack/react-table@npm:8.16.0" @@ -11283,7 +11334,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.3, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.3, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 @@ -11623,6 +11674,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.5.0": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 + languageName: node + linkType: hard + "@types/send@npm:*": version: 0.17.4 resolution: "@types/send@npm:0.17.4" @@ -11900,6 +11958,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/scope-manager@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + checksum: 71028b757da9694528c4c3294a96cc80bc7d396e383a405eab3bc224cda7341b88e0fc292120b35d3f31f47beac69f7083196c70616434072fbcd3d3e62d3376 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/type-utils@npm:5.62.0" @@ -11924,6 +11992,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/types@npm:6.21.0" + checksum: 9501b47d7403417af95fc1fb72b2038c5ac46feac0e1598a46bcb43e56a606c387e9dcd8a2a0abe174c91b509f2d2a8078b093786219eb9a01ab2fbf9ee7b684 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:1.13.0": version: 1.13.0 resolution: "@typescript-eslint/typescript-estree@npm:1.13.0" @@ -11952,6 +12027,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: 9.0.3 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependenciesMeta: + typescript: + optional: true + checksum: dec02dc107c4a541e14fb0c96148f3764b92117c3b635db3a577b5a56fc48df7a556fa853fb82b07c0663b4bf2c484c9f245c28ba3e17e5cb0918ea4cab2ea21 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.45.0, @typescript-eslint/utils@npm:^5.58.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -11970,6 +12064,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.20.0": + version: 6.21.0 + resolution: "@typescript-eslint/utils@npm:6.21.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@types/json-schema": ^7.0.12 + "@types/semver": ^7.5.0 + "@typescript-eslint/scope-manager": 6.21.0 + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/typescript-estree": 6.21.0 + semver: ^7.5.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: b129b3a4aebec8468259f4589985cb59ea808afbfdb9c54f02fad11e17d185e2bf72bb332f7c36ec3c09b31f18fc41368678b076323e6e019d06f74ee93f7bf2 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -11980,6 +12091,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + eslint-visitor-keys: ^3.4.1 + checksum: 67c7e6003d5af042d8703d11538fca9d76899f0119130b373402819ae43f0bc90d18656aa7add25a24427ccf1a0efd0804157ba83b0d4e145f06107d7d1b7433 + languageName: node + linkType: hard + "@uiw/codemirror-extensions-basic-setup@npm:4.21.21": version: 4.21.21 resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.21" @@ -24692,6 +24813,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3, minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -24710,15 +24840,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -32152,6 +32273,15 @@ storybook-preset-craco@artisanofcode/storybook-preset-craco: languageName: node linkType: hard +"ts-api-utils@npm:^1.0.1": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: c746ddabfdffbf16cb0b0db32bb287236a19e583057f8649ee7c49995bb776e1d3ef384685181c11a1a480369e022ca97512cb08c517b2d2bd82c83754c97012 + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0"