mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-13 04:41:02 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposalExternal } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressMini, Button, Columar, ExpandButton, LinkExternal, Table } from '@pezkuwi/react-components';
|
||||
import { useCollectiveMembers, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Fasttrack from './Fasttrack.js';
|
||||
import PreImageButton from './PreImageButton.js';
|
||||
import ProposalCell from './ProposalCell.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: DeriveProposalExternal;
|
||||
}
|
||||
|
||||
function External ({ className = '', value: { image, imageHash, threshold } }: Props): React.ReactElement<Props> | null {
|
||||
const { isMember, members } = useCollectiveMembers('technicalCommittee');
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={`${className} isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}>
|
||||
<ProposalCell
|
||||
imageHash={imageHash}
|
||||
proposal={image?.proposal}
|
||||
/>
|
||||
<td className='address'>
|
||||
{image && (
|
||||
<AddressMini value={image.proposer} />
|
||||
)}
|
||||
</td>
|
||||
<Table.Column.Balance value={image?.balance} />
|
||||
<td className='actions'>
|
||||
<Button.Group>
|
||||
{!image?.proposal && (
|
||||
<PreImageButton imageHash={imageHash} />
|
||||
)}
|
||||
{threshold && isMember && (
|
||||
<Fasttrack
|
||||
imageHash={imageHash}
|
||||
members={members}
|
||||
threshold={threshold}
|
||||
/>
|
||||
)}
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
</Button.Group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={`${className} ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}>
|
||||
<td
|
||||
className='columar'
|
||||
colSpan={100}
|
||||
>
|
||||
<Columar is100>
|
||||
<Columar.Column>
|
||||
<LinkExternal
|
||||
data={imageHash}
|
||||
type='democracyExternal'
|
||||
withTitle
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(External);
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposalImage } from '@pezkuwi/api-derive/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { CallExpander, Holder } from '@pezkuwi/react-params';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: Hash;
|
||||
}
|
||||
|
||||
function ExternalCell ({ className = '', value }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const preimage = useCall<DeriveProposalImage>(api.derive.democracy.preimage, [value]);
|
||||
|
||||
if (!preimage?.proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Holder
|
||||
className={className}
|
||||
withBorder
|
||||
withPadding
|
||||
>
|
||||
<CallExpander
|
||||
labelHash={t('proposal hash')}
|
||||
value={preimage.proposal}
|
||||
withHash
|
||||
/>
|
||||
</Holder>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ExternalCell);
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposalExternal } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import External from './External.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Externals ({ className }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const external = useCall<DeriveProposalExternal | null>(api.derive.democracy.nextExternal);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('external'), 'start'],
|
||||
[t('proposer'), 'address'],
|
||||
[t('locked')],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={external === null && t('No external proposal')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{external && <External value={external} />}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Externals);
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { Hash, VoteThreshold } from '@pezkuwi/types/interfaces';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { getFastTrackThreshold } from '@pezkuwi/apps-config';
|
||||
import { Button, Input, InputAddress, InputNumber, Modal, Toggle, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useCollectiveInstance, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { BN, isString } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
imageHash: Hash | HexString;
|
||||
members: string[];
|
||||
threshold: VoteThreshold;
|
||||
}
|
||||
|
||||
interface ProposalState {
|
||||
proposal?: SubmittableExtrinsic<'promise'> | null;
|
||||
proposalLength: number;
|
||||
}
|
||||
|
||||
// default, assuming 6s blocks
|
||||
const ONE_HOUR = (60 * 60) / 6;
|
||||
const DEF_DELAY = new BN(ONE_HOUR);
|
||||
const DEF_VOTING = new BN(3 * ONE_HOUR);
|
||||
|
||||
function Fasttrack ({ imageHash, members, threshold }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isFasttrackOpen, toggleFasttrack] = useToggle();
|
||||
const [accountId, setAcountId] = useState<string | null>(null);
|
||||
const [delayBlocks, setDelayBlocks] = useState<BN | undefined>(DEF_DELAY);
|
||||
const [votingBlocks, setVotingBlocks] = useState<BN | undefined>(api.consts.democracy.fastTrackVotingPeriod || DEF_VOTING);
|
||||
const [{ proposal, proposalLength }, setProposal] = useState<ProposalState>(() => ({ proposalLength: 0 }));
|
||||
const [withVote, toggleVote] = useToggle(true);
|
||||
const modLocation = useCollectiveInstance('technicalCommittee');
|
||||
const proposalCount = useCall<BN>(modLocation && api.query[modLocation].proposalCount);
|
||||
|
||||
const memberThreshold = useMemo(
|
||||
() => new BN(
|
||||
Math.ceil(
|
||||
members.length * getFastTrackThreshold(api, !votingBlocks || api.consts.democracy.fastTrackVotingPeriod.lte(votingBlocks))
|
||||
)
|
||||
),
|
||||
[api, members, votingBlocks]
|
||||
);
|
||||
|
||||
const extrinsic = useMemo(
|
||||
(): SubmittableExtrinsic<'promise'> | null => {
|
||||
if (!modLocation || !proposal || !proposalCount || !api.tx.utility) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const proposeTx = api.tx[modLocation].propose.meta.args.length === 3
|
||||
? api.tx[modLocation].propose(memberThreshold, proposal, proposalLength)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Old-type
|
||||
: api.tx[modLocation].propose(memberThreshold, proposal);
|
||||
|
||||
return withVote && (members.length > 1)
|
||||
? api.tx.utility.batch([
|
||||
proposeTx,
|
||||
api.tx[modLocation].vote(proposal.method.hash, proposalCount, true)
|
||||
])
|
||||
: proposeTx;
|
||||
}, [api, members, memberThreshold, modLocation, proposal, proposalCount, proposalLength, withVote]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
const proposal = delayBlocks && !delayBlocks.isZero() && votingBlocks && !votingBlocks.isZero()
|
||||
? api.tx.democracy.fastTrack(imageHash, votingBlocks, delayBlocks)
|
||||
: null;
|
||||
|
||||
setProposal({
|
||||
proposal,
|
||||
proposalLength: proposal?.length || 0
|
||||
});
|
||||
}, [api, delayBlocks, imageHash, members, votingBlocks]);
|
||||
|
||||
if (!modLocation || !api.tx.utility) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFasttrackOpen && (
|
||||
<Modal
|
||||
header={t('Fast track proposal')}
|
||||
onClose={toggleFasttrack}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('Select the committee account you wish to make the proposal with.')}>
|
||||
<InputAddress
|
||||
filter={members}
|
||||
label={t('propose from account')}
|
||||
onChange={setAcountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The external proposal to send to the technical committee')}>
|
||||
<Input
|
||||
isDisabled
|
||||
label={t('preimage hash')}
|
||||
value={isString(imageHash) ? imageHash : imageHash.toHex()}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The voting period and delay to apply to this proposal. The threshold is calculated from these values.')}>
|
||||
<InputNumber
|
||||
autoFocus
|
||||
isZeroable={false}
|
||||
label={t('voting period')}
|
||||
onChange={setVotingBlocks}
|
||||
value={votingBlocks}
|
||||
/>
|
||||
<InputNumber
|
||||
isZeroable={false}
|
||||
label={t('delay')}
|
||||
onChange={setDelayBlocks}
|
||||
value={delayBlocks}
|
||||
/>
|
||||
<InputNumber
|
||||
defaultValue={memberThreshold}
|
||||
isDisabled
|
||||
label={t('threshold')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
{(members.length > 1) && (
|
||||
<Modal.Columns hint={t('Submit an Aye vote alongside the proposal as part of a batch')}>
|
||||
<Toggle
|
||||
label={t('Submit Aye vote with proposal')}
|
||||
onChange={toggleVote}
|
||||
value={withVote}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='forward'
|
||||
isDisabled={!accountId}
|
||||
label={t('Fast track')}
|
||||
onStart={toggleFasttrack}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='forward'
|
||||
isDisabled={threshold.isSuperMajorityApprove}
|
||||
label={t('Fast track')}
|
||||
onClick={toggleFasttrack}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Fasttrack);
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/promise/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, Modal, Static, styled, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { Extrinsic } from '@pezkuwi/react-params';
|
||||
import { Available } from '@pezkuwi/react-query';
|
||||
import { BN, BN_ZERO, isString } from '@pezkuwi/util';
|
||||
import { blake2AsHex } from '@pezkuwi/util-crypto';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isImminent?: boolean;
|
||||
imageHash?: Hash | HexString;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface HashState {
|
||||
encodedHash: string;
|
||||
encodedProposal: string;
|
||||
storageFee: BN | null;
|
||||
}
|
||||
|
||||
const ZERO_HASH = blake2AsHex('');
|
||||
|
||||
function PreImage ({ className = '', imageHash, isImminent = false, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiDefaultTxSudo } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [{ encodedHash, encodedProposal, storageFee }, setHash] = useState<HashState>({ encodedHash: ZERO_HASH, encodedProposal: '', storageFee: null });
|
||||
const [proposal, setProposal] = useState<SubmittableExtrinsic>();
|
||||
|
||||
useEffect((): void => {
|
||||
const encodedProposal = proposal?.method.toHex() || '';
|
||||
const storageFee = api.consts.democracy.preimageByteDeposit
|
||||
? (api.consts.democracy.preimageByteDeposit as unknown as BN).mul(
|
||||
encodedProposal
|
||||
? new BN((encodedProposal.length - 2) / 2)
|
||||
: BN_ZERO
|
||||
)
|
||||
: null;
|
||||
|
||||
setHash({ encodedHash: blake2AsHex(encodedProposal), encodedProposal, storageFee });
|
||||
}, [api, proposal]);
|
||||
|
||||
const isMatched = useMemo(
|
||||
() => imageHash
|
||||
? isString(imageHash)
|
||||
? imageHash === encodedHash
|
||||
: imageHash.eq(encodedHash)
|
||||
: true,
|
||||
[encodedHash, imageHash]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
className={className}
|
||||
header={t('Submit preimage')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will pay the fees for the preimage, based on the size thereof.')}>
|
||||
<InputAddress
|
||||
label={t('send from account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={<span className='label'>{t('transferable')}</span>}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={
|
||||
<>
|
||||
<p>{t('The image (proposal) will be stored on-chain against the hash of the contents.')}</p>
|
||||
<p>{t('When submitting a proposal the hash needs to be known. Proposals can be submitted with hash-only, but upon dispatch the preimage needs to be available.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Extrinsic
|
||||
defaultValue={apiDefaultTxSudo}
|
||||
label={t('propose')}
|
||||
onChange={setProposal}
|
||||
/>
|
||||
<Static
|
||||
label={t('preimage hash')}
|
||||
value={encodedHash}
|
||||
withCopy
|
||||
/>
|
||||
</Modal.Columns>
|
||||
{!isImminent && storageFee && (
|
||||
<Modal.Columns hint={t('The calculated storage costs based on the size and the per-bytes fee.')}>
|
||||
<InputBalance
|
||||
defaultValue={storageFee}
|
||||
isDisabled
|
||||
label={t('calculated storage fee')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!proposal || !accountId || !isMatched || !encodedProposal}
|
||||
label={t('Submit preimage')}
|
||||
onStart={onClose}
|
||||
params={[encodedProposal]}
|
||||
tx={
|
||||
isImminent
|
||||
? api.tx.democracy.noteImminentPreimage
|
||||
: api.tx.democracy.notePreimage
|
||||
}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
.toggleImminent {
|
||||
margin: 0.5rem 0;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(PreImage);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import PreImage from './PreImage.js';
|
||||
|
||||
interface Props {
|
||||
imageHash: Hash | HexString;
|
||||
isImminent?: boolean;
|
||||
}
|
||||
|
||||
function PreImageButton ({ imageHash, isImminent }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isPreimageOpen, togglePreimage] = useToggle();
|
||||
|
||||
return (
|
||||
api.tx.democracy.notePreimage
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Image')}
|
||||
onClick={togglePreimage}
|
||||
/>
|
||||
{isPreimageOpen && (
|
||||
<PreImage
|
||||
imageHash={imageHash}
|
||||
isImminent={isImminent}
|
||||
onClose={togglePreimage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(PreImageButton);
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposal } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddressMini, Button, Columar, ExpandButton, ExpanderScroll, LinkExternal, Table } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import PreImageButton from './PreImageButton.js';
|
||||
import ProposalCell from './ProposalCell.js';
|
||||
import Seconding from './Seconding.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: DeriveProposal;
|
||||
}
|
||||
|
||||
function Proposal ({ className = '', value: { balance, image, imageHash, index, proposer, seconds } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
|
||||
const seconding = useMemo(
|
||||
() => seconds.filter((_address, index) => index !== 0),
|
||||
[seconds]
|
||||
);
|
||||
|
||||
const renderSeconds = useCallback(
|
||||
() => seconding.map((address, count): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={`${count}:${address.toHex()}`}
|
||||
value={address}
|
||||
withBalance={false}
|
||||
withShrink
|
||||
/>
|
||||
)),
|
||||
[seconding]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={`${className} isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}>
|
||||
<Table.Column.Id value={index} />
|
||||
<ProposalCell
|
||||
imageHash={imageHash}
|
||||
proposal={image?.proposal}
|
||||
/>
|
||||
<td className='address'>
|
||||
<AddressMini value={proposer} />
|
||||
</td>
|
||||
<td className='number together media--1200'>
|
||||
<FormatBalance value={balance} />
|
||||
</td>
|
||||
<td className='expand'>
|
||||
{seconding.length !== 0 && (
|
||||
<ExpanderScroll
|
||||
empty={seconding && t('No endorsements')}
|
||||
renderChildren={renderSeconds}
|
||||
summary={t('Endorsed ({{count}})', { replace: { count: seconding.length } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='actions'>
|
||||
<Button.Group>
|
||||
{!image?.proposal && (
|
||||
<PreImageButton imageHash={imageHash} />
|
||||
)}
|
||||
<Seconding
|
||||
deposit={balance}
|
||||
depositors={seconds || []}
|
||||
image={image}
|
||||
proposalId={index}
|
||||
/>
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
</Button.Group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={`${className} ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}>
|
||||
<td />
|
||||
<td
|
||||
className='columar'
|
||||
colSpan={100}
|
||||
>
|
||||
<Columar is100>
|
||||
<Columar.Column>
|
||||
<LinkExternal
|
||||
data={index}
|
||||
type='democracyProposal'
|
||||
withTitle
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposal);
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Compact } from '@pezkuwi/types';
|
||||
import type { Hash, Proposal, ProposalIndex } from '@pezkuwi/types/interfaces';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { useApi, usePreimage } from '@pezkuwi/react-hooks';
|
||||
import { CallExpander } from '@pezkuwi/react-params';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import ExternalCell from './ExternalCell.js';
|
||||
import TreasuryCell from './TreasuryCell.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
imageHash: Hash | HexString;
|
||||
isCollective?: boolean;
|
||||
proposal?: Proposal | null;
|
||||
}
|
||||
|
||||
const METHOD_EXTE = ['externalPropose', 'externalProposeDefault', 'externalProposeMajority', 'fastTrack'];
|
||||
const METHOD_TREA = ['approveProposal', 'rejectProposal'];
|
||||
|
||||
function ProposalCell ({ className = '', imageHash, isCollective, proposal }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const preimage = usePreimage(imageHash);
|
||||
|
||||
// while we still have this endpoint, democracy will use it
|
||||
const displayProposal = isCollective
|
||||
? proposal
|
||||
: api.query.democracy?.preimages
|
||||
? proposal
|
||||
: preimage?.proposal;
|
||||
|
||||
if (!displayProposal) {
|
||||
const textHash = imageHash.toString();
|
||||
|
||||
return (
|
||||
<td className={`${className} all hash`}>
|
||||
<div className='shortHash'>{textHash}</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
const { method, section } = displayProposal.registry.findMetaCall(displayProposal.callIndex);
|
||||
const isTreasury = section === 'treasury' && METHOD_TREA.includes(method);
|
||||
const isExternal = section === 'democracy' && METHOD_EXTE.includes(method);
|
||||
|
||||
return (
|
||||
<StyledTd className={`${className} all`}>
|
||||
<CallExpander
|
||||
labelHash={t('proposal hash')}
|
||||
value={displayProposal}
|
||||
withHash={!isTreasury && !isExternal}
|
||||
>
|
||||
{isExternal && (
|
||||
<ExternalCell value={displayProposal.args[0] as Hash} />
|
||||
)}
|
||||
{isTreasury && (
|
||||
<TreasuryCell value={displayProposal.args[0] as Compact<ProposalIndex>} />
|
||||
)}
|
||||
</CallExpander>
|
||||
</StyledTd>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTd = styled.td`
|
||||
.shortHash {
|
||||
+ div {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(ProposalCell);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposal } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import ProposalDisplay from './Proposal.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Proposals ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const proposals = useCall<DeriveProposal[]>(api.derive.democracy.proposals);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('proposals'), 'start', 2],
|
||||
[t('proposer'), 'address'],
|
||||
[t('locked'), 'media--1200'],
|
||||
[undefined, undefined, 2]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={proposals && t('No active proposals')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{proposals?.map((proposal): React.ReactNode => (
|
||||
<ProposalDisplay
|
||||
key={proposal.index.toString()}
|
||||
value={proposal}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposals);
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Input, InputAddress, InputBalance, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, usePreimage } from '@pezkuwi/react-hooks';
|
||||
import { Available } from '@pezkuwi/react-query';
|
||||
import { BN_ZERO, isFunction, isHex } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface HashState {
|
||||
imageHash?: HexString | null;
|
||||
isImageHashValid: boolean;
|
||||
}
|
||||
|
||||
interface ImageState {
|
||||
imageLen: BN;
|
||||
imageLenDefault?: BN;
|
||||
isImageLenValid: boolean;
|
||||
}
|
||||
|
||||
function Propose ({ className = '', onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [balance, setBalance] = useState<BN | undefined>();
|
||||
const [{ imageHash, isImageHashValid }, setImageHash] = useState<HashState>({ imageHash: null, isImageHashValid: false });
|
||||
const [{ imageLen, imageLenDefault, isImageLenValid }, setImageLen] = useState<ImageState>({ imageLen: BN_ZERO, isImageLenValid: false });
|
||||
const publicProps = useCall<unknown[]>(api.query.democracy.publicProps);
|
||||
const preimage = usePreimage(imageHash);
|
||||
|
||||
useEffect((): void => {
|
||||
preimage?.proposalLength && setImageLen((prev) => ({
|
||||
imageLen: prev.imageLen,
|
||||
imageLenDefault: preimage.proposalLength,
|
||||
isImageLenValid: prev.isImageLenValid
|
||||
}));
|
||||
}, [preimage]);
|
||||
|
||||
const _onChangeImageHash = useCallback(
|
||||
(h?: string) =>
|
||||
setImageHash({
|
||||
imageHash: h as HexString,
|
||||
isImageHashValid: isHex(h, 256)
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const _onChangeImageLen = useCallback(
|
||||
(value?: BN): void => {
|
||||
value && setImageLen((prev) => ({
|
||||
imageLen: value,
|
||||
imageLenDefault: prev.imageLenDefault,
|
||||
isImageLenValid: !value.isZero()
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const hasMinLocked = balance?.gte(api.consts.democracy.minimumDeposit);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Submit proposal')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The proposal will be registered from this account and the balance lock will be applied here.')}>
|
||||
<InputAddress
|
||||
label={t('send from account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={<span className='label'>{t('transferable')}</span>}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
hint={
|
||||
<>
|
||||
<p>{t('The hash of the preimage for the proposal as previously submitted or intended.')}</p>
|
||||
<p>{t('The length value will be auto-populated from the on-chain value if it is found.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
isError={!isImageHashValid}
|
||||
label={t('preimage hash')}
|
||||
onChange={_onChangeImageHash}
|
||||
value={imageHash || ''}
|
||||
/>
|
||||
<InputNumber
|
||||
defaultValue={imageLenDefault}
|
||||
isDisabled={!!preimage?.proposalLength && !preimage?.proposalLength.isZero() && isImageHashValid && isImageLenValid}
|
||||
isError={!isImageLenValid}
|
||||
key='inputLength'
|
||||
label={t('preimage length')}
|
||||
onChange={_onChangeImageLen}
|
||||
value={imageLen}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The associated deposit for this proposal should be more then the minimum on-chain deposit required. It will be locked until the proposal passes.')}>
|
||||
<InputBalance
|
||||
defaultValue={api.consts.democracy.minimumDeposit}
|
||||
isError={!hasMinLocked}
|
||||
label={t('locked balance')}
|
||||
onChange={setBalance}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={api.consts.democracy.minimumDeposit}
|
||||
isDisabled
|
||||
label={t('minimum deposit')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!balance || !hasMinLocked || !isImageHashValid || !accountId || !publicProps || (isFunction(api.tx.preimage?.notePreimage) && !isFunction(api.tx.democracy?.notePreimage) && !preimage)}
|
||||
label={t('Submit proposal')}
|
||||
onStart={onClose}
|
||||
params={
|
||||
api.tx.democracy.propose.meta.args.length === 3
|
||||
? [imageHash, balance, publicProps?.length]
|
||||
: isFunction(api.tx.preimage?.notePreimage) && !isFunction(api.tx.democracy?.notePreimage)
|
||||
? [preimage && { Lookup: { hash: preimage.proposalHash, len: imageLen } }, balance]
|
||||
: [imageHash, balance]
|
||||
}
|
||||
tx={api.tx.democracy.propose}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Propose);
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveReferendumExt } from '@pezkuwi/api-derive/types';
|
||||
import type { Balance } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Badge, Button, Columar, ExpandButton, Icon, LinkExternal, Progress, Table } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useBestNumber, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { BlockToTime } from '@pezkuwi/react-query';
|
||||
import { BN, BN_ONE, formatNumber, isBoolean } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useChangeCalc from '../useChangeCalc.js';
|
||||
import PreImageButton from './PreImageButton.js';
|
||||
import ProposalCell from './ProposalCell.js';
|
||||
import ReferendumVotes from './ReferendumVotes.js';
|
||||
import Voting from './Voting.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: DeriveReferendumExt;
|
||||
}
|
||||
|
||||
interface Percentages {
|
||||
aye: string;
|
||||
nay: string;
|
||||
turnout: string;
|
||||
}
|
||||
|
||||
interface VoteType {
|
||||
hasVoted: boolean;
|
||||
hasVotedAye: boolean;
|
||||
}
|
||||
|
||||
function percentage (val: BN, div: BN): string {
|
||||
return Math.min(100, val.muln(10000).div(div).toNumber() / 100).toFixed(2);
|
||||
}
|
||||
|
||||
function Referendum ({ className = '', value: { allAye, allNay, image, imageHash, index, isPassing, status, voteCountAye, voteCountNay, votedAye, votedNay, votedTotal } }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const bestNumber = useBestNumber();
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
const totalIssuance = useCall<Balance>(api.query.balances?.totalIssuance);
|
||||
const { changeAye, changeNay } = useChangeCalc(status.threshold, votedAye, votedNay, votedTotal);
|
||||
const threshold = useMemo(
|
||||
() => status.threshold.type.toString().replace('majority', ' majority '),
|
||||
[status]
|
||||
);
|
||||
const totalCalculated = votedAye.add(votedNay);
|
||||
|
||||
const [percentages, { hasVoted, hasVotedAye }] = useMemo(
|
||||
(): [Percentages | null, VoteType] => {
|
||||
if (totalIssuance) {
|
||||
const aye = allAye.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const nay = allNay.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const hasVotedAye = allAye.some(({ accountId }) => allAccounts.includes(accountId.toString()));
|
||||
|
||||
return [
|
||||
{
|
||||
aye: votedTotal.isZero()
|
||||
? ''
|
||||
: `${percentage(aye, votedTotal)}%`,
|
||||
nay: votedTotal.isZero()
|
||||
? ''
|
||||
: `${percentage(nay, votedTotal)}%`,
|
||||
turnout: `${percentage(votedTotal, totalIssuance)}%`
|
||||
},
|
||||
{
|
||||
hasVoted: hasVotedAye || allNay.some(({ accountId }) => allAccounts.includes(accountId.toString())),
|
||||
hasVotedAye
|
||||
}
|
||||
];
|
||||
} else {
|
||||
return [null, { hasVoted: false, hasVotedAye: false }];
|
||||
}
|
||||
},
|
||||
[allAccounts, allAye, allNay, totalIssuance, votedTotal]
|
||||
);
|
||||
|
||||
if (!bestNumber || status.end.sub(bestNumber).lten(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enactBlock = status.end.add(status.delay);
|
||||
const remainBlock = status.end.sub(bestNumber).isub(BN_ONE);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={`${className} isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}>
|
||||
<Table.Column.Id value={index} />
|
||||
<ProposalCell
|
||||
imageHash={imageHash}
|
||||
proposal={image?.proposal}
|
||||
/>
|
||||
<td className='number together media--1200'>
|
||||
<BlockToTime value={remainBlock} />
|
||||
{t('{{blocks}} blocks', { replace: { blocks: formatNumber(remainBlock) } })}
|
||||
</td>
|
||||
<td className='number together media--1400'>
|
||||
<BlockToTime value={enactBlock.sub(bestNumber)} />
|
||||
#{formatNumber(enactBlock)}
|
||||
</td>
|
||||
<td className='number together media--1400'>
|
||||
{percentages && (
|
||||
<>
|
||||
<div>{percentages.turnout}</div>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className='badge'>
|
||||
{isBoolean(isPassing) && (
|
||||
<Badge
|
||||
color={isPassing ? 'green' : 'red'}
|
||||
hover={
|
||||
isPassing
|
||||
? t('{{threshold}}, passing', { replace: { threshold } })
|
||||
: t('{{threshold}}, not passing', { replace: { threshold } })
|
||||
}
|
||||
icon={isPassing ? 'check' : 'times'}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='expand'>
|
||||
<ReferendumVotes
|
||||
change={changeAye}
|
||||
count={voteCountAye}
|
||||
isAye
|
||||
isWinning={isPassing}
|
||||
total={votedAye}
|
||||
votes={allAye}
|
||||
/>
|
||||
<ReferendumVotes
|
||||
change={changeNay}
|
||||
count={voteCountNay}
|
||||
isAye={false}
|
||||
isWinning={!isPassing}
|
||||
total={votedNay}
|
||||
votes={allNay}
|
||||
/>
|
||||
</td>
|
||||
<td className='media--1000 middle chart'>
|
||||
<Progress
|
||||
total={totalCalculated}
|
||||
value={votedAye}
|
||||
/>
|
||||
</td>
|
||||
<td className='badge'>
|
||||
<Icon
|
||||
color={hasVoted ? (hasVotedAye ? 'green' : 'red') : 'gray'}
|
||||
icon='asterisk'
|
||||
/>
|
||||
</td>
|
||||
<td className='actions'>
|
||||
<Button.Group>
|
||||
{!image?.proposal && (
|
||||
<PreImageButton imageHash={imageHash} />
|
||||
)}
|
||||
<Voting
|
||||
proposal={image?.proposal}
|
||||
referendumId={index}
|
||||
/>
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
</Button.Group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={`${className} ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}>
|
||||
<td />
|
||||
<td
|
||||
className='columar'
|
||||
colSpan={100}
|
||||
>
|
||||
<Columar is100>
|
||||
<Columar.Column>
|
||||
<LinkExternal
|
||||
data={index}
|
||||
type='democracyReferendum'
|
||||
withTitle
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Referendum);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveReferendumVote } from '@pezkuwi/api-derive/types';
|
||||
import type { Vote } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressMini } from '@pezkuwi/react-components';
|
||||
|
||||
interface Props {
|
||||
vote: DeriveReferendumVote;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizing = ['0.1x', '1x', '2x', '3x', '4x', '5x', '6x'];
|
||||
|
||||
function voteLabel ({ conviction }: Vote, isDelegating: boolean): string {
|
||||
return `${sizing[conviction.toNumber()]}${isDelegating ? '/d' : ''} - `;
|
||||
}
|
||||
|
||||
function ReferendumVote ({ vote: { accountId, balance, isDelegating, vote } }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<AddressMini
|
||||
balance={balance}
|
||||
labelBalance={voteLabel(vote, isDelegating)}
|
||||
value={accountId}
|
||||
withBalance
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ReferendumVote);
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveReferendumVote } from '@pezkuwi/api-derive/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ExpanderScroll } from '@pezkuwi/react-components';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_TEN, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import ReferendumVote from './ReferendumVote.js';
|
||||
|
||||
interface Props {
|
||||
change: BN;
|
||||
className?: string;
|
||||
count: number;
|
||||
isAye: boolean;
|
||||
isWinning: boolean;
|
||||
total: BN;
|
||||
votes: DeriveReferendumVote[];
|
||||
}
|
||||
|
||||
const LOCKS = [1, 10, 20, 30, 40, 50, 60];
|
||||
|
||||
function ReferendumVotes ({ className, count, isAye, total, votes }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sorted = useMemo(
|
||||
() => votes.sort((a, b) => {
|
||||
const ta = a.balance.muln(LOCKS[a.vote.conviction.toNumber()]).div(BN_TEN);
|
||||
const tb = b.balance.muln(LOCKS[b.vote.conviction.toNumber()]).div(BN_TEN);
|
||||
|
||||
return tb.cmp(ta);
|
||||
}),
|
||||
[votes]
|
||||
);
|
||||
|
||||
const renderVotes = useCallback(
|
||||
() => sorted.map((vote) => (
|
||||
<ReferendumVote
|
||||
key={vote.accountId.toString()}
|
||||
vote={vote}
|
||||
/>
|
||||
)),
|
||||
[sorted]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpanderScroll
|
||||
className={className}
|
||||
empty={votes && t('No voters')}
|
||||
// help={change.gtn(0) && (
|
||||
// <>
|
||||
// <FormatBalance value={change} />
|
||||
// <p>{isWinning
|
||||
// ? t('The amount this total can be reduced by to change the referendum outcome. This assumes changes to the convictions of the existing votes, with no additional turnout.')
|
||||
// : t('The amount this total should be increased by to change the referendum outcome. This assumes additional turnout with new votes at 1x conviction.')
|
||||
// }</p>
|
||||
// </>
|
||||
// )}
|
||||
// helpIcon={isWinning ? 'arrow-circle-down' : 'arrow-circle-up'}
|
||||
renderChildren={votes.length ? renderVotes : undefined}
|
||||
summary={
|
||||
<>
|
||||
{isAye
|
||||
? t('Aye {{count}}', { replace: { count: count ? ` (${formatNumber(count)})` : '' } })
|
||||
: t('Nay {{count}}', { replace: { count: count ? ` (${formatNumber(count)})` : '' } })
|
||||
}
|
||||
<div><FormatBalance value={total} /></div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ReferendumVotes);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveReferendumExt } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Referendum from './Referendum.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
referendums?: DeriveReferendumExt[];
|
||||
}
|
||||
|
||||
function Referendums ({ className = '', referendums }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('referenda'), 'start', 2],
|
||||
[t('remaining'), 'media--1200'],
|
||||
[t('activate'), 'media--1400'],
|
||||
[t('turnout'), 'media--1400'],
|
||||
[undefined, 'badge'],
|
||||
[t('votes'), 'expand'],
|
||||
[undefined, 'media--1000'],
|
||||
[undefined, undefined, 2]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={referendums && t('No active referendums')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{referendums?.map((referendum): React.ReactNode => (
|
||||
<Referendum
|
||||
key={referendum.index.toString()}
|
||||
value={referendum}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Referendums);
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveProposalImage } from '@pezkuwi/api-derive/types';
|
||||
import type { AccountId, Balance } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { ProposedAction } from '@pezkuwi/react-params';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
deposit?: Balance;
|
||||
depositors: AccountId[];
|
||||
image?: DeriveProposalImage;
|
||||
proposalId: BN | number;
|
||||
}
|
||||
|
||||
function Seconding ({ deposit, depositors, image, proposalId }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [isSecondingOpen, toggleSeconding] = useToggle();
|
||||
|
||||
if (!hasAccounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSecondingOpen && (
|
||||
<Modal
|
||||
header={t('Endorse proposal')}
|
||||
onClose={toggleSeconding}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The proposal is in the queue for future referendums. One proposal from this list will move forward to voting.')}>
|
||||
<ProposedAction
|
||||
idNumber={proposalId}
|
||||
proposal={image?.proposal}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('Endorsing a proposal that indicates your backing for the proposal. Proposals with greater interest moves up the queue for potential next referendums.')}>
|
||||
<InputAddress
|
||||
label={t('endorse with account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The deposit will be locked for the lifetime of the proposal.')}>
|
||||
<InputBalance
|
||||
defaultValue={deposit || api.consts.democracy.minimumDeposit}
|
||||
isDisabled
|
||||
label={t('deposit required')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!accountId}
|
||||
label={t('Endorse')}
|
||||
onStart={toggleSeconding}
|
||||
params={
|
||||
api.tx.democracy.second.meta.args.length === 2
|
||||
? [proposalId, depositors.length]
|
||||
: [proposalId]
|
||||
}
|
||||
tx={api.tx.democracy.second}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='toggle-off'
|
||||
label={t('Endorse')}
|
||||
onClick={toggleSeconding}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Seconding);
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useBestNumber, useCall, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE, BN_THREE, BN_TWO, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
referendumCount?: number;
|
||||
}
|
||||
|
||||
const optMulti = {
|
||||
defaultValue: [undefined, undefined] as [BN | undefined, BN | undefined]
|
||||
};
|
||||
|
||||
function Summary ({ referendumCount }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const activeProposals = useCall<unknown[]>(api.derive.democracy.proposals);
|
||||
const bestNumber = useBestNumber();
|
||||
const [publicPropCount, referendumTotal] = useCallMulti<[BN | undefined, BN | undefined]>([
|
||||
api.query.democracy.publicPropCount,
|
||||
api.query.democracy.referendumCount
|
||||
], optMulti);
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
<CardSummary label={t('proposals')}>
|
||||
{activeProposals
|
||||
? formatNumber(activeProposals.length)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('total')}>
|
||||
{publicPropCount
|
||||
? formatNumber(publicPropCount)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
</section>
|
||||
<section>
|
||||
<CardSummary label={t('referenda')}>
|
||||
{referendumCount !== undefined
|
||||
? formatNumber(referendumCount)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('total')}>
|
||||
{referendumTotal
|
||||
? formatNumber(referendumTotal)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
</section>
|
||||
{api.consts.democracy.launchPeriod && (
|
||||
<section className='media--1100'>
|
||||
<CardSummary
|
||||
label={t('launch period')}
|
||||
progress={{
|
||||
isBlurred: !bestNumber,
|
||||
total: api.consts.democracy.launchPeriod,
|
||||
value: bestNumber
|
||||
? bestNumber.mod(api.consts.democracy.launchPeriod).iadd(BN_ONE)
|
||||
: api.consts.democracy.launchPeriod.mul(BN_TWO).div(BN_THREE),
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Compact, Option } from '@pezkuwi/types';
|
||||
import type { ProposalIndex, TreasuryProposal } from '@pezkuwi/types/interfaces';
|
||||
import type { TypeDef } from '@pezkuwi/types/types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import Params from '@pezkuwi/react-params';
|
||||
import { getTypeDef } from '@pezkuwi/types/create';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: Compact<ProposalIndex>;
|
||||
}
|
||||
|
||||
interface Param {
|
||||
name: string;
|
||||
type: TypeDef;
|
||||
}
|
||||
|
||||
interface Value {
|
||||
isValid: boolean;
|
||||
value: TreasuryProposal;
|
||||
}
|
||||
|
||||
interface ParamState {
|
||||
params: Param[];
|
||||
values: Value[];
|
||||
}
|
||||
|
||||
const DEFAULT_PARAMS: ParamState = { params: [], values: [] };
|
||||
|
||||
const OPT_PROP = {
|
||||
transform: (optProp: Option<TreasuryProposal>) => optProp.unwrapOr(null)
|
||||
};
|
||||
|
||||
function TreasuryCell ({ className = '', value }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [proposalId] = useState(() => value.unwrap());
|
||||
const proposal = useCall<TreasuryProposal | null>(api.query.treasury.proposals, [proposalId], OPT_PROP);
|
||||
const [{ params, values }, setExtracted] = useState<ParamState>(DEFAULT_PARAMS);
|
||||
|
||||
useEffect((): void => {
|
||||
proposal && setExtracted({
|
||||
params: [{
|
||||
name: 'proposal',
|
||||
type: getTypeDef('TreasuryProposal')
|
||||
}],
|
||||
values: [{
|
||||
isValid: true,
|
||||
value: proposal
|
||||
}]
|
||||
});
|
||||
}, [proposal]);
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Params
|
||||
isDisabled
|
||||
params={params}
|
||||
values={values}
|
||||
>
|
||||
<InputAddress
|
||||
defaultValue={proposal.beneficiary}
|
||||
isDisabled
|
||||
label={t('beneficiary')}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={proposal.value}
|
||||
isDisabled
|
||||
label={t('payout')}
|
||||
/>
|
||||
</Params>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TreasuryCell);
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PropIndex, Proposal } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, ConvictionDropdown, Modal, TxButton, VoteAccount, VoteValue } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { ProposedAction } from '@pezkuwi/react-params';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
proposal?: Proposal;
|
||||
referendumId: PropIndex;
|
||||
}
|
||||
|
||||
function Voting ({ proposal, referendumId }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [balance, setBalance] = useState<BN | undefined>();
|
||||
const [conviction, setConviction] = useState(1);
|
||||
const [isVotingOpen, toggleVoting] = useToggle();
|
||||
|
||||
const isCurrentVote = useMemo(
|
||||
() => !!api.query.democracy.votingOf,
|
||||
[api]
|
||||
);
|
||||
|
||||
if (!hasAccounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDisabled = isCurrentVote ? !balance : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVotingOpen && (
|
||||
<Modal
|
||||
header={t('Vote on proposal')}
|
||||
onClose={toggleVoting}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('If this proposal is passed, the changes will be applied via dispatch and the deposit returned.')}>
|
||||
<ProposedAction
|
||||
idNumber={referendumId}
|
||||
proposal={proposal}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The vote will be recorded for this account. If another account delegated to this one, the delegated votes will also be counted.')}>
|
||||
<VoteAccount onChange={setAccountId} />
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
hint={
|
||||
<>
|
||||
<p>{t('The balance associated with the vote will be locked as per the conviction specified and will not be available for transfer during this period.')}</p>
|
||||
<p>{t('Conviction locks do overlap and are not additive, meaning that funds locked during a previous vote can be locked again.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isCurrentVote && (
|
||||
<VoteValue
|
||||
accountId={accountId}
|
||||
autoFocus
|
||||
onChange={setBalance}
|
||||
/>
|
||||
)}
|
||||
<ConvictionDropdown
|
||||
label={t('conviction')}
|
||||
onChange={setConviction}
|
||||
value={conviction}
|
||||
voteLockingPeriod={
|
||||
api.consts.democracy.voteLockingPeriod ||
|
||||
api.consts.democracy.enactmentPeriod
|
||||
}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='ban'
|
||||
isDisabled={isDisabled}
|
||||
label={t('Vote Nay')}
|
||||
onStart={toggleVoting}
|
||||
params={
|
||||
isCurrentVote
|
||||
? [referendumId, { Standard: { balance, vote: { aye: false, conviction } } }]
|
||||
: [referendumId, { aye: false, conviction }]
|
||||
}
|
||||
tx={api.tx.democracy.vote}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
isDisabled={isDisabled}
|
||||
label={t('Vote Aye')}
|
||||
onStart={toggleVoting}
|
||||
params={
|
||||
isCurrentVote
|
||||
? [referendumId, { Standard: { balance, vote: { aye: true, conviction } } }]
|
||||
: [referendumId, { aye: true, conviction }]
|
||||
}
|
||||
tx={api.tx.democracy.vote}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='check-to-slot'
|
||||
label={t('Vote')}
|
||||
onClick={toggleVoting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Voting);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveReferendumExt } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Externals from './Externals.js';
|
||||
import PreImage from './PreImage.js';
|
||||
import Proposals from './Proposals.js';
|
||||
import Propose from './Propose.js';
|
||||
import Referendums from './Referendums.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Overview ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isPreimageOpen, togglePreimage] = useToggle();
|
||||
const [isProposeOpen, togglePropose] = useToggle();
|
||||
const referendums = useCall<DeriveReferendumExt[]>(api.derive.democracy.referendums);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary referendumCount={referendums?.length} />
|
||||
<Button.Group>
|
||||
{api.tx.democracy.notePreimage && (
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Submit preimage')}
|
||||
onClick={togglePreimage}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Submit proposal')}
|
||||
onClick={togglePropose}
|
||||
/>
|
||||
</Button.Group>
|
||||
{isPreimageOpen && (
|
||||
<PreImage onClose={togglePreimage} />
|
||||
)}
|
||||
{isProposeOpen && (
|
||||
<Propose onClose={togglePropose} />
|
||||
)}
|
||||
<Referendums referendums={referendums} />
|
||||
<Proposals />
|
||||
<Externals />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
|
||||
import Overview from './Overview/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
export { default as useCounter } from './useCounter.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
}
|
||||
|
||||
function DemocracyApp ({ basePath }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
}
|
||||
], [t]);
|
||||
|
||||
return (
|
||||
<main className='democracy--App'>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={items}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path={basePath}>
|
||||
<Route
|
||||
element={
|
||||
<Overview />
|
||||
}
|
||||
index
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DemocracyApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-democracy');
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { VoteThreshold } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { approxChanges } from './util.js';
|
||||
|
||||
interface Result {
|
||||
changeAye: BN;
|
||||
changeNay: BN;
|
||||
}
|
||||
|
||||
function useChangeCalcImpl (threshold: VoteThreshold, votedAye: BN, votedNay: BN, votedTotal: BN): Result {
|
||||
const { api } = useApi();
|
||||
const sqrtElectorate = useCall<BN>(api.derive.democracy.sqrtElectorate);
|
||||
const [result, setResult] = useState<Result>({ changeAye: BN_ZERO, changeNay: BN_ZERO });
|
||||
|
||||
useEffect((): void => {
|
||||
sqrtElectorate && setResult(
|
||||
approxChanges(threshold, sqrtElectorate, { votedAye, votedNay, votedTotal })
|
||||
);
|
||||
}, [sqrtElectorate, threshold, votedAye, votedNay, votedTotal]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default createNamedHook('useChangeCalc', useChangeCalcImpl);
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall, useIsMountedRef } from '@pezkuwi/react-hooks';
|
||||
|
||||
function useCounterImpl (): number {
|
||||
const { hasAccounts } = useAccounts();
|
||||
const { api, isApiReady } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const proposals = useCall<unknown[]>(isApiReady && hasAccounts && api.derive.democracy?.proposals);
|
||||
const referenda = useCall<unknown[]>(isApiReady && hasAccounts && api.derive.democracy?.referendumsActive);
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect((): void => {
|
||||
mountedRef.current && setCounter(
|
||||
(proposals?.length || 0) +
|
||||
(referenda?.length || 0)
|
||||
);
|
||||
}, [mountedRef, proposals, referenda]);
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
export default createNamedHook('useCounter', useCounterImpl);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
||||
|
||||
import { calcPassing } from '@pezkuwi/api-derive/democracy/util';
|
||||
import { TypeRegistry } from '@pezkuwi/types/create';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
import { approxChanges } from './util.js';
|
||||
|
||||
const ACTUAL = {
|
||||
sqrtElectorate: new BN('2949443240'),
|
||||
votedAye: new BN('358406690000000000'),
|
||||
votedNay: new BN('18942000000000000'),
|
||||
votedTotal: new BN('136099900000000000')
|
||||
};
|
||||
|
||||
const registry = new TypeRegistry();
|
||||
|
||||
describe('approxChanges', (): void => {
|
||||
it('approximates where the points are', (): void => {
|
||||
const threshold = registry.createType('VoteThreshold', 0);
|
||||
const { changeAye, changeNay } = approxChanges(threshold, ACTUAL.sqrtElectorate, ACTUAL);
|
||||
|
||||
expect(
|
||||
calcPassing(threshold, ACTUAL.sqrtElectorate, { ...ACTUAL, votedAye: ACTUAL.votedAye.sub(changeAye) })
|
||||
).toBe(false);
|
||||
expect(
|
||||
calcPassing(threshold, ACTUAL.sqrtElectorate, { ...ACTUAL, votedNay: ACTUAL.votedNay.add(changeNay) })
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-democracy authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { VoteThreshold } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { calcPassing } from '@pezkuwi/api-derive/democracy/util';
|
||||
import { BN, BN_ONE, BN_TEN, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
interface Approx {
|
||||
changeAye: BN;
|
||||
changeNay: BN;
|
||||
}
|
||||
|
||||
interface ApproxState {
|
||||
votedAye: BN;
|
||||
votedNay: BN;
|
||||
votedTotal: BN;
|
||||
}
|
||||
|
||||
const ONEMIN = new BN(-1);
|
||||
const DIVISOR = new BN(2);
|
||||
|
||||
/**
|
||||
* This is where we tweak the input values, based on what was specified, be it the input number
|
||||
* or the direction and turnout adjustments
|
||||
*
|
||||
* @param votes The votes that should be adjusted, will be either aye/nay
|
||||
* @param total The actual total of applied votes (same as turnout from derived)
|
||||
* @param change The actual change value we want to affect
|
||||
* @param inc The increment to apply here
|
||||
* @param totalInc The increment for the total. 0 for conviction-only changes, 1 of 1x added conviction vote
|
||||
* @param direction The direction, either increment (1) or decrement (-1)
|
||||
*/
|
||||
function getDiffs (votes: BN, total: BN, change: BN, inc: BN, totalInc: 0 | 0.1 | 1, direction: 1 | -1): [BN, BN, BN] {
|
||||
// setup
|
||||
const multiplier = direction === 1 ? BN_ONE : ONEMIN;
|
||||
const voteChange = change.add(inc);
|
||||
|
||||
// since we allow 0.1 as well, we first multiply by 10, before dividing by the same
|
||||
const totalChange = BN_ONE.muln(totalInc * 10).mul(voteChange).div(BN_TEN);
|
||||
|
||||
// return the change, vote with change applied and the total with the same. For the total we don't want
|
||||
// to go negative (total votes/turnout), since will do sqrt on it (and negative is non-sensical anyway)
|
||||
return [
|
||||
voteChange,
|
||||
votes.add(multiplier.mul(voteChange)),
|
||||
BN.max(BN_ZERO, total.add(multiplier.mul(totalChange)))
|
||||
];
|
||||
}
|
||||
|
||||
// loop changes over aye, using the diffs above, returning when an outcome change is made
|
||||
function calcChangeAye (threshold: VoteThreshold, sqrtElectorate: BN, { votedAye, votedNay, votedTotal }: ApproxState, isPassing: boolean, changeAye: BN, inc: BN): BN {
|
||||
while (true) {
|
||||
// if this one is passing, we only adjust the convictions (since it goes down), if it is failing
|
||||
// we assume new votes needs to be added, do those at 1x conviction
|
||||
const [newChangeAye, newAye, newTotal] = getDiffs(votedAye, votedTotal, changeAye, inc, isPassing ? 0 : 1, isPassing ? -1 : 1);
|
||||
const newResult = calcPassing(threshold, sqrtElectorate, { votedAye: newAye, votedNay, votedTotal: newTotal });
|
||||
|
||||
if (newResult !== isPassing) {
|
||||
return changeAye;
|
||||
}
|
||||
|
||||
changeAye = newChangeAye;
|
||||
}
|
||||
}
|
||||
|
||||
// loop changes over nay, using the diffs above, returning when an outcome change is made
|
||||
function calcChangeNay (threshold: VoteThreshold, sqrtElectorate: BN, { votedAye, votedNay, votedTotal }: ApproxState, isPassing: boolean, changeNay: BN, inc: BN): BN {
|
||||
while (true) {
|
||||
// if this one is passing, we only adjust the convictions (since it goes down), if it is failing
|
||||
// we assume new votes needs to be added, do those at 1x conviction
|
||||
// NOTE: We use isPassing here, so it is reversed from what we find in the aye calc
|
||||
const [newChangeNay, newNay, newTotal] = getDiffs(votedNay, votedTotal, changeNay, inc, isPassing ? 1 : 0, isPassing ? 1 : -1);
|
||||
const newResult = calcPassing(threshold, sqrtElectorate, { votedAye, votedNay: newNay, votedTotal: newTotal });
|
||||
|
||||
if (newResult !== isPassing) {
|
||||
return changeNay;
|
||||
}
|
||||
|
||||
changeNay = newChangeNay;
|
||||
}
|
||||
}
|
||||
|
||||
// The magic happens here
|
||||
export function approxChanges (threshold: VoteThreshold, sqrtElectorate: BN, state: ApproxState): Approx {
|
||||
const isPassing = calcPassing(threshold, sqrtElectorate, state);
|
||||
|
||||
// simple case, we have an aye > nay to determine passing
|
||||
if (threshold.isSimpleMajority) {
|
||||
const change = isPassing
|
||||
? state.votedAye.sub(state.votedNay)
|
||||
: state.votedNay.sub(state.votedAye);
|
||||
|
||||
return {
|
||||
changeAye: state.votedNay.isZero()
|
||||
? BN_ZERO
|
||||
: change,
|
||||
changeNay: state.votedAye.isZero()
|
||||
? BN_ZERO
|
||||
: change
|
||||
};
|
||||
}
|
||||
|
||||
let changeAye = BN_ZERO;
|
||||
let changeNay = BN_ZERO;
|
||||
let inc = state.votedTotal.div(DIVISOR);
|
||||
|
||||
// - starting from a large increment (total/2) see if that changes the outcome
|
||||
// - keep dividing by 2, each time adding just enough to _not_ make the state change
|
||||
// - continue the process, until we have the smallest increment
|
||||
// - on the last iteration, we add the increment, since we push over the line
|
||||
while (!inc.isZero()) {
|
||||
// calc the applied changes based on current increment
|
||||
changeAye = calcChangeAye(threshold, sqrtElectorate, state, isPassing, changeAye, inc);
|
||||
changeNay = calcChangeNay(threshold, sqrtElectorate, state, isPassing, changeNay, inc);
|
||||
|
||||
// move down one level
|
||||
const nextInc = inc.div(DIVISOR);
|
||||
|
||||
// on the final round (no more inc reductions), add the last increment to push it over the line
|
||||
if (nextInc.isZero()) {
|
||||
changeAye = changeAye.add(inc);
|
||||
changeNay = changeNay.add(inc);
|
||||
}
|
||||
|
||||
inc = nextInc;
|
||||
}
|
||||
|
||||
// - When the other vote is zero, it is not useful to show the decrease, since it ends up at all
|
||||
// - Always ensure that we don't go above max available (generally should be covered by above)
|
||||
return {
|
||||
changeAye: state.votedNay.isZero()
|
||||
? BN_ZERO
|
||||
: isPassing
|
||||
? BN.min(changeAye, state.votedAye)
|
||||
: changeAye,
|
||||
changeNay: state.votedAye.isZero()
|
||||
? BN_ZERO
|
||||
: isPassing
|
||||
? changeNay
|
||||
: BN.min(changeNay, state.votedNay)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user