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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -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);
+49
View File
@@ -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);
+8
View File
@@ -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);
+26
View File
@@ -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);
+33
View File
@@ -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);
});
});
+143
View File
@@ -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)
};
}