// Copyright 2017-2026 @pezkuwi/app-referenda authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { ApiPromise } from '@pezkuwi/api'; import type { Option } from '@pezkuwi/types'; import type { PezpalletConvictionVotingVoteCasting, PezpalletConvictionVotingVoteVoting, PezpalletReferendaReferendumInfoConvictionVotingTally } from '@pezkuwi/types/lookup'; import type { BN } from '@pezkuwi/util'; import type { Lock, PalletReferenda, PalletVote } from './types.js'; import { useMemo } from 'react'; import { CONVICTIONS } from '@pezkuwi/react-components/ConvictionDropdown'; import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks'; import { BN_MAX_INTEGER } from '@pezkuwi/util'; const OPT_CLASS = { transform: (locks: [BN, BN][]): BN[] => locks.map(([classId]) => classId) }; const OPT_VOTES = { transform: ([[params], votes]: [[[string, BN][]], PezpalletConvictionVotingVoteVoting[]]): [classId: BN, refIds: BN[], casting: PezpalletConvictionVotingVoteCasting][] => votes .map((v, index): null | [BN, BN[], PezpalletConvictionVotingVoteCasting] => { if (!v.isCasting) { return null; } const casting = v.asCasting; return [ params[index][1], casting.votes.map(([refId]) => refId), casting ]; }) .filter((v): v is [BN, BN[], PezpalletConvictionVotingVoteCasting] => !!v), withParamsTransform: true }; const OPT_REFS = { transform: ([[params], optTally]: [[BN[]], Option[]]): [BN, PezpalletReferendaReferendumInfoConvictionVotingTally][] => optTally .map((v, index): null | [BN, PezpalletReferendaReferendumInfoConvictionVotingTally] => v.isSome ? [params[index], v.unwrap()] : null ) .filter((v): v is [BN, PezpalletReferendaReferendumInfoConvictionVotingTally] => !!v), withParamsTransform: true }; function getVoteParams (accountId: string, lockClasses?: BN[]): [[accountId: string, classId: BN][]] | undefined { if (lockClasses) { return [lockClasses.map((classId) => [accountId, classId])]; } return undefined; } function getRefParams (votes?: [classId: BN, refIds: BN[], casting: PezpalletConvictionVotingVoteCasting][]): [BN[]] | undefined { if (votes?.length) { const refIds = votes.reduce((all, [, refIds]) => all.concat(refIds), []); if (refIds.length) { return [refIds]; } } return undefined; } function getLocks (api: ApiPromise, palletVote: PalletVote, votes: [classId: BN, refIds: BN[], casting: PezpalletConvictionVotingVoteCasting][], referenda: [BN, PezpalletReferendaReferendumInfoConvictionVotingTally][]): Lock[] { const lockPeriod = api.consts[palletVote].voteLockingPeriod as BN; const locks: Lock[] = []; for (let i = 0, voteCount = votes.length; i < voteCount; i++) { const [classId,, casting] = votes[i]; for (let i = 0, castCount = casting.votes.length; i < castCount; i++) { const [refId, accountVote] = casting.votes[i]; const refInfo = referenda.find(([id]) => id.eq(refId)); if (refInfo) { const [, tally] = refInfo; let total: BN | undefined; let endBlock: BN | undefined; let convictionIndex = 0; let locked = 'None'; const durationIndex = 1; if (accountVote.isStandard) { const { balance, vote } = accountVote.asStandard; total = balance; if ((tally.isApproved && vote.isAye) || (tally.isRejected && vote.isNay)) { convictionIndex = vote.conviction.index; locked = vote.conviction.type; } } else if (accountVote.isSplit) { const { aye, nay } = accountVote.asSplit; total = aye.add(nay); } else if (accountVote.isSplitAbstain) { const { abstain, aye, nay } = accountVote.asSplitAbstain; total = aye.add(nay).add(abstain); } else { console.error(`Unable to handle ${accountVote.type}`); } if (tally.isOngoing) { endBlock = BN_MAX_INTEGER; } else if (tally.isKilled) { endBlock = tally.asKilled; } else if (tally.isCancelled || tally.isTimedOut) { endBlock = tally.isCancelled ? tally.asCancelled[0] : tally.asTimedOut[0]; } else if (tally.isApproved || tally.isRejected) { endBlock = lockPeriod .muln(convictionIndex ? CONVICTIONS[convictionIndex - 1][durationIndex] : 0) .add( tally.isApproved ? tally.asApproved[0] : tally.asRejected[0] ); } else { console.error(`Unable to handle ${tally.type}`); } if (total && endBlock) { locks.push({ classId, endBlock, locked, refId, total }); } } } } return locks; } function useAccountLocksImpl (palletReferenda: PalletReferenda, palletVote: PalletVote, accountId: string): Lock[] | undefined { const { api } = useApi(); // retrieve the locks for the account (all classes) via the accountId const lockParams = useMemo( () => [accountId], [accountId] ); const lockClasses = useCall(lockParams && api.query[palletVote]?.classLocksFor, lockParams, OPT_CLASS); // retrieve the specific votes casted over the classes & accountId const voteParams = useMemo( () => getVoteParams(accountId, lockClasses), [accountId, lockClasses] ); const votes = useCall<[BN, BN[], PezpalletConvictionVotingVoteCasting][] | undefined>(voteParams && api.query[palletVote]?.votingFor.multi, voteParams, OPT_VOTES); // retrieve the referendums that were voted on const refParams = useMemo( () => getRefParams(votes), [votes] ); const referenda = useCall(refParams && api.query[palletReferenda]?.referendumInfoFor.multi, refParams, OPT_REFS); // combine the referenda outcomes and the votes into locks return useMemo( () => votes && referenda && getLocks(api, palletVote, votes, referenda), [api, palletVote, referenda, votes] ); } export default createNamedHook('useAccountLocks', useAccountLocksImpl);