Calculations for referenda curves (#8417)
* Calculations for referenda curves * clamping * Curve tests * Adjust * Display actual time left * totalEligible * Protected div * Remove decidingEnd override * Calc full curve graph * Full graph * Display graphs * Pass through eligibleIssuance * Rename activeIssuance * Dual graphs * Adjust color interleave
This commit is contained in:
parent
14794ebcd9
commit
bf69fbb625
@ -1,9 +1,8 @@
|
||||
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletReferenda, PalletVote, ReferendaGroup } from '../types';
|
||||
import type { PalletReferenda, PalletVote, ReferendaGroup, TrackDescription } from '../types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -16,16 +15,17 @@ import { getTrackInfo } from '../util';
|
||||
import Referendum from './Referendum';
|
||||
|
||||
interface Props extends ReferendaGroup {
|
||||
activeIssuance?: BN;
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
members?: string[];
|
||||
palletReferenda: PalletReferenda;
|
||||
palletVote: PalletVote;
|
||||
ranks?: BN[];
|
||||
tracks?: [BN, PalletReferendaTrackInfo][] | undefined;
|
||||
tracks?: TrackDescription[] | undefined;
|
||||
}
|
||||
|
||||
function Group ({ className, isMember, members, palletReferenda, palletVote, ranks, referenda, trackId, trackName, tracks }: Props): React.ReactElement<Props> {
|
||||
function Group ({ activeIssuance, className, isMember, members, palletReferenda, palletVote, ranks, referenda, trackId, trackName, tracks }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api, specName } = useApi();
|
||||
|
||||
@ -40,16 +40,18 @@ function Group ({ className, isMember, members, palletReferenda, palletVote, ran
|
||||
empty={referenda && t<string>('No active referenda')}
|
||||
header={[
|
||||
[trackName ? <>{trackName}<div>{trackInfo?.text}</div></> : t('referenda'), 'start', 7],
|
||||
[undefined, undefined, 1]
|
||||
[undefined, undefined, 3]
|
||||
]}
|
||||
key={
|
||||
trackName
|
||||
? `track:${trackName}`
|
||||
: 'untracked'
|
||||
}
|
||||
withCollapsibleRows
|
||||
>
|
||||
{referenda && referenda.map((r) => (
|
||||
<Referendum
|
||||
activeIssuance={activeIssuance}
|
||||
isMember={isMember}
|
||||
key={r.key}
|
||||
members={members}
|
||||
|
||||
@ -1,43 +1,164 @@
|
||||
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ReferendumProps as Props } from '../types';
|
||||
import type { PalletConvictionVotingTally, PalletRankedCollectiveTally, PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { BaseReferendumProps as Props, CurveGraph, ReferendumProps } from '../types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { LinkExternal } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
import { Chart, Columar, ExpandButton, LinkExternal } from '@polkadot/react-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
import { BN_MILLION, BN_THOUSAND, formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Killed from './RefKilled';
|
||||
import Ongoing from './RefOngoing';
|
||||
import Tuple from './RefTuple';
|
||||
|
||||
const Components: Record<string, React.ComponentType<Props>> = {
|
||||
const COMPONENTS: Record<string, React.ComponentType<ReferendumProps>> = {
|
||||
Killed,
|
||||
Ongoing
|
||||
};
|
||||
|
||||
const COLORS = ['#8c8c8c', '#9c3333', '#339c33'];
|
||||
const PT_CUR = 0;
|
||||
const PT_NEG = 1;
|
||||
const PT_POS = 2;
|
||||
|
||||
const OPTIONS = {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
aspectRatio: 2.5,
|
||||
maintainAspectRatio: true
|
||||
};
|
||||
|
||||
interface ChartResult {
|
||||
colors: string[];
|
||||
labels: string[];
|
||||
options: typeof OPTIONS;
|
||||
values: number[][];
|
||||
}
|
||||
|
||||
function getChartProps (totalEligible: BN, isConvictionVote: boolean, info: PalletReferendaReferendumInfoConvictionVotingTally | PalletReferendaReferendumInfoRankedCollectiveTally, trackGraph: CurveGraph): ChartResult[] | null {
|
||||
if (totalEligible && isConvictionVote && info.isOngoing) {
|
||||
const ongoing = info.asOngoing;
|
||||
|
||||
if (ongoing.deciding.isSome) {
|
||||
const { approval, support, x } = trackGraph;
|
||||
const { deciding, tally } = ongoing;
|
||||
const { since } = deciding.unwrap();
|
||||
const currentSupport = isConvictionVote
|
||||
? (tally as PalletConvictionVotingTally).support
|
||||
: (tally as PalletRankedCollectiveTally).bareAyes;
|
||||
const labels: string[] = [];
|
||||
const values: number[][][] = [[[], [], []], [[], [], []]];
|
||||
|
||||
for (let i = 0; i < approval.length; i++) {
|
||||
labels.push(formatNumber(since.add(x[i])));
|
||||
|
||||
const appr = approval[i].div(BN_MILLION).toNumber() / 10;
|
||||
const appc = tally.ayes.isZero() ? 0 : tally.ayes.mul(BN_THOUSAND).div(tally.ayes.add(tally.nays)).toNumber() / 10;
|
||||
|
||||
values[0][PT_CUR][i] = appr;
|
||||
values[0][appc < appr ? PT_NEG : PT_POS][i] = appc;
|
||||
|
||||
const supr = support[i].div(BN_MILLION).toNumber() / 10;
|
||||
const supc = totalEligible.isZero() ? 0 : currentSupport.mul(BN_THOUSAND).div(totalEligible).toNumber() / 10;
|
||||
|
||||
values[1][PT_CUR][i] = supr;
|
||||
values[1][supc < supr ? PT_NEG : PT_POS][i] = supc;
|
||||
}
|
||||
|
||||
return [
|
||||
{ colors: COLORS, labels, options: OPTIONS, values: values[0] },
|
||||
{ colors: COLORS, labels, options: OPTIONS, values: values[1] }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function Referendum (props: Props): React.ReactElement<Props> {
|
||||
const { className, palletReferenda, value: { id, info } } = props;
|
||||
const { t } = useTranslation();
|
||||
const { activeIssuance, className = '', palletReferenda, value: { id, info, isConvictionVote, trackGraph } } = props;
|
||||
const [isExpanded, toggleExpanded] = useToggle(false);
|
||||
|
||||
const Component = useMemo(
|
||||
() => Components[info.type] || Tuple,
|
||||
() => COMPONENTS[info.type] || Tuple,
|
||||
[info]
|
||||
);
|
||||
|
||||
const chartProps = useMemo(
|
||||
() => activeIssuance && trackGraph && getChartProps(activeIssuance, isConvictionVote, info, trackGraph),
|
||||
[activeIssuance, info, isConvictionVote, trackGraph]
|
||||
);
|
||||
|
||||
const chartLegend = useMemo(
|
||||
() => [
|
||||
[
|
||||
t<string>('minimum approval'),
|
||||
t<string>('current approval (failing)'),
|
||||
t<string>('current approval (passing)')
|
||||
],
|
||||
[
|
||||
t<string>('minimum support'),
|
||||
t<string>('current support (failing)'),
|
||||
t<string>('current support (passing)')
|
||||
]
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='number'>
|
||||
<h1>{formatNumber(id)}</h1>
|
||||
</td>
|
||||
<Component {...props} />
|
||||
<td className='links media--1000'>
|
||||
<LinkExternal
|
||||
data={id}
|
||||
type={palletReferenda}
|
||||
<>
|
||||
<tr className={`${className}${chartProps && isExpanded ? ' noBorder' : ''}`}>
|
||||
<td className='number'>
|
||||
<h1>{formatNumber(id)}</h1>
|
||||
</td>
|
||||
<Component
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
{...props}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<td className='links media--1000'>
|
||||
<LinkExternal
|
||||
data={id}
|
||||
type={palletReferenda}
|
||||
/>
|
||||
</td>
|
||||
<td className='links media--1000'>
|
||||
{chartProps && (
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleExpanded}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={`${className} ${chartProps && isExpanded ? 'isExpanded' : 'isCollapsed'}`}>
|
||||
{chartProps && isExpanded && (
|
||||
<td colSpan={10}>
|
||||
<Columar>
|
||||
<Columar.Column>
|
||||
<Chart.Line
|
||||
legends={chartLegend[0]}
|
||||
{...chartProps[0]}
|
||||
/>
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
<Chart.Line
|
||||
legends={chartLegend[1]}
|
||||
{...chartProps[1]}
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RawParam } from '@polkadot/react-params/types';
|
||||
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
import type { PalletReferenda } from '../types';
|
||||
import type { PalletReferenda, TrackDescription } from '../types';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -26,7 +25,7 @@ interface Props {
|
||||
isMember: boolean;
|
||||
members?: string[];
|
||||
palletReferenda: PalletReferenda;
|
||||
tracks?: [BN, PalletReferendaTrackInfo][];
|
||||
tracks?: TrackDescription[];
|
||||
}
|
||||
|
||||
interface HashState {
|
||||
@ -130,8 +129,8 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
);
|
||||
|
||||
const trackOpts = useMemo(
|
||||
() => tracks && tracks.map(([id, track]) => ({
|
||||
text: getTrackName(id, track),
|
||||
() => tracks && tracks.map(({ id, info }) => ({
|
||||
text: getTrackName(id, info),
|
||||
value: id.toNumber()
|
||||
})),
|
||||
[tracks]
|
||||
|
||||
@ -9,7 +9,8 @@ import styled from 'styled-components';
|
||||
|
||||
import AddPreimage from '@polkadot/app-preimages/Preimages/Add';
|
||||
import { Button, Dropdown } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import useReferenda from '../useReferenda';
|
||||
@ -30,11 +31,19 @@ interface Props {
|
||||
|
||||
function Referenda ({ className, members, palletReferenda, palletVote, ranks }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const totalIssuance = useCall<BN | undefined>(api.query.balances.totalIssuance);
|
||||
const inactiveIssuance = useCall<BN | undefined>(api.query.balances.inactiveIssuance);
|
||||
const { allAccounts } = useAccounts();
|
||||
const [grouped, tracks] = useReferenda(palletReferenda);
|
||||
const summary = useSummary(palletReferenda, grouped);
|
||||
const [trackSelected, setTrackSelected] = useState(-1);
|
||||
|
||||
const activeIssuance = useMemo(
|
||||
() => totalIssuance && totalIssuance.sub(inactiveIssuance || BN_ZERO),
|
||||
[inactiveIssuance, totalIssuance]
|
||||
);
|
||||
|
||||
const trackOpts = useMemo(
|
||||
() => [{ text: t<string>('All active/available tracks'), value: -1 }].concat(
|
||||
grouped
|
||||
@ -82,6 +91,7 @@ function Referenda ({ className, members, palletReferenda, palletVote, ranks }:
|
||||
</Button.Group>
|
||||
{filtered.map(({ key, referenda, trackId, trackName }: ReferendaGroup) => (
|
||||
<Group
|
||||
activeIssuance={activeIssuance}
|
||||
isMember={isMember}
|
||||
key={key}
|
||||
members={members}
|
||||
|
||||
@ -11,6 +11,7 @@ export type PalletVote = 'convictionVoting' | 'rankedCollective' | 'fellowshipCo
|
||||
export interface ReferendaGroup {
|
||||
key: string;
|
||||
track?: PalletReferendaTrackInfo;
|
||||
trackGraph?: CurveGraph;
|
||||
trackId?: BN;
|
||||
trackName?: string;
|
||||
referenda?: Referendum[];
|
||||
@ -21,18 +22,22 @@ export interface ReferendaGroupKnown extends ReferendaGroup {
|
||||
}
|
||||
|
||||
export interface Referendum {
|
||||
decidingEnd?: BN;
|
||||
id: BN;
|
||||
info: PalletReferendaReferendumInfoConvictionVotingTally | PalletReferendaReferendumInfoRankedCollectiveTally;
|
||||
isConvictionVote: boolean;
|
||||
key: string;
|
||||
track?: PalletReferendaTrackInfo;
|
||||
trackId?: BN;
|
||||
trackGraph?: CurveGraph;
|
||||
}
|
||||
|
||||
export interface ReferendumProps {
|
||||
export interface BaseReferendumProps {
|
||||
className?: string;
|
||||
activeIssuance?: BN;
|
||||
isMember: boolean;
|
||||
members?: string[];
|
||||
onExpand?: () => void;
|
||||
palletReferenda: PalletReferenda;
|
||||
palletVote: PalletVote;
|
||||
ranks?: BN[];
|
||||
@ -40,12 +45,29 @@ export interface ReferendumProps {
|
||||
value: Referendum;
|
||||
}
|
||||
|
||||
export interface ReferendumProps extends BaseReferendumProps {
|
||||
isExpanded: boolean;
|
||||
toggleExpanded: () => void;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
deciding?: BN;
|
||||
refActive?: number;
|
||||
refCount?: BN;
|
||||
}
|
||||
|
||||
export interface CurveGraph {
|
||||
approval: BN[];
|
||||
support: BN[];
|
||||
x: BN[];
|
||||
}
|
||||
|
||||
export interface TrackDescription {
|
||||
graph: CurveGraph;
|
||||
id: BN;
|
||||
info: PalletReferendaTrackInfo;
|
||||
}
|
||||
|
||||
export interface TrackInfo {
|
||||
compare?: (input: BN) => boolean;
|
||||
origin: Record<string, string> | Record<string, string>[];
|
||||
|
||||
@ -2,9 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally, PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletReferenda, ReferendaGroup, ReferendaGroupKnown, Referendum } from './types';
|
||||
import type { PalletReferenda, ReferendaGroup, ReferendaGroupKnown, Referendum, TrackDescription } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@ -13,11 +12,7 @@ import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import useReferendaIds from './useReferendaIds';
|
||||
import useTracks from './useTracks';
|
||||
import { getTrackName } from './util';
|
||||
|
||||
function isConvictionVote (info: Referendum['info']): info is PalletReferendaReferendumInfoConvictionVotingTally {
|
||||
return info.isOngoing && !(info as PalletReferendaReferendumInfoRankedCollectiveTally).asOngoing.tally.bareAyes && !!(info as PalletReferendaReferendumInfoConvictionVotingTally).asOngoing.tally.support;
|
||||
}
|
||||
import { calcDecidingEnd, getTrackName, isConvictionVote } from './util';
|
||||
|
||||
function sortOngoing (a: Referendum, b: Referendum): number {
|
||||
const ao = a.info.asOngoing;
|
||||
@ -89,8 +84,8 @@ const OPT_MULTI = {
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo][]): ReferendaGroup[] {
|
||||
if (!referenda) {
|
||||
function group (totalIssuance?: BN, referenda?: Referendum[], tracks?: TrackDescription[]): ReferendaGroup[] {
|
||||
if (!referenda || !totalIssuance) {
|
||||
// return an empty group when we have no referenda
|
||||
return [{ key: 'empty' }];
|
||||
} else if (!tracks) {
|
||||
@ -107,12 +102,23 @@ function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo
|
||||
|
||||
// only ongoing have tracks
|
||||
const trackInfo = ref.info.isOngoing
|
||||
? tracks.find(([id]) => id.eq(ref.info.asOngoing.track))
|
||||
? tracks.find(({ id }) => id.eq(ref.info.asOngoing.track))
|
||||
: undefined;
|
||||
|
||||
if (trackInfo) {
|
||||
ref.trackId = trackInfo[0];
|
||||
ref.track = trackInfo[1];
|
||||
ref.trackGraph = trackInfo.graph;
|
||||
ref.trackId = trackInfo.id;
|
||||
ref.track = trackInfo.info;
|
||||
|
||||
if (ref.isConvictionVote && ref.info.isOngoing) {
|
||||
const { deciding, tally } = ref.info.asOngoing;
|
||||
|
||||
if (deciding.isSome) {
|
||||
const { since } = deciding.unwrap();
|
||||
|
||||
ref.decidingEnd = calcDecidingEnd(totalIssuance, tally, trackInfo.info, since);
|
||||
}
|
||||
}
|
||||
|
||||
const group = grouped.find(({ track }) => ref.track === track);
|
||||
|
||||
@ -122,6 +128,7 @@ function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo
|
||||
key: `track:${ref.trackId.toString()}`,
|
||||
referenda: [ref],
|
||||
track: ref.track,
|
||||
trackGraph: ref.trackGraph,
|
||||
trackId: ref.trackId,
|
||||
trackName: getTrackName(ref.trackId, ref.track)
|
||||
});
|
||||
@ -149,8 +156,9 @@ function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo
|
||||
return grouped.sort(sortGroups);
|
||||
}
|
||||
|
||||
function useReferendaImpl (palletReferenda: PalletReferenda): [ReferendaGroup[], [BN, PalletReferendaTrackInfo][] | undefined] {
|
||||
function useReferendaImpl (palletReferenda: PalletReferenda): [ReferendaGroup[], TrackDescription[] | undefined] {
|
||||
const { api, isApiReady } = useApi();
|
||||
const totalIssuance = useCall<BN>(isApiReady && api.query.balances.totalIssuance);
|
||||
const ids = useReferendaIds(palletReferenda);
|
||||
const tracks = useTracks(palletReferenda);
|
||||
const referenda = useCall(isApiReady && ids && ids.length !== 0 && api.query[palletReferenda as 'referenda'].referendumInfoFor.multi, [ids], OPT_MULTI);
|
||||
@ -159,10 +167,10 @@ function useReferendaImpl (palletReferenda: PalletReferenda): [ReferendaGroup[],
|
||||
() => [
|
||||
(ids && ids.length === 0)
|
||||
? [{ key: 'referenda', referenda: [] }]
|
||||
: group(referenda, tracks),
|
||||
: group(totalIssuance, referenda, tracks),
|
||||
tracks
|
||||
],
|
||||
[ids, referenda, tracks]
|
||||
[ids, referenda, totalIssuance, tracks]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,18 +3,32 @@
|
||||
|
||||
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletReferenda } from './types';
|
||||
import type { PalletReferenda, TrackDescription } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi } from '@polkadot/react-hooks';
|
||||
|
||||
function useTracksImpl (palletReferenda: PalletReferenda): [BN, PalletReferendaTrackInfo][] | undefined {
|
||||
import { calcCurves } from './util';
|
||||
|
||||
function expandTracks (tracks?: [BN, PalletReferendaTrackInfo][]): TrackDescription[] | undefined {
|
||||
if (tracks) {
|
||||
return tracks.map(([id, info]) => ({
|
||||
graph: calcCurves(info),
|
||||
id,
|
||||
info
|
||||
}));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function useTracksImpl (palletReferenda: PalletReferenda): TrackDescription[] | undefined {
|
||||
const { api, isApiReady } = useApi();
|
||||
|
||||
return useMemo(
|
||||
() => isApiReady
|
||||
? api.consts[palletReferenda as 'referenda'] && api.consts[palletReferenda as 'referenda'].tracks
|
||||
? expandTracks(api.consts[palletReferenda as 'referenda'] && api.consts[palletReferenda as 'referenda'].tracks)
|
||||
: undefined,
|
||||
[api, isApiReady, palletReferenda]
|
||||
);
|
||||
|
||||
133
packages/page-referenda/src/util.spec.ts
Normal file
133
packages/page-referenda/src/util.spec.ts
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletReferendaCurve } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
import { BN_BILLION, BN_ZERO, bnToBn } from '@polkadot/util';
|
||||
|
||||
import { curveDelay, curveThreshold } from './util';
|
||||
|
||||
function curveLinear (ceil: BN | string | number, floor: BN | string | number, length: BN | string | number): PalletReferendaCurve {
|
||||
return {
|
||||
asLinearDecreasing: {
|
||||
ceil: bnToBn(ceil),
|
||||
floor: bnToBn(floor),
|
||||
length: bnToBn(length)
|
||||
},
|
||||
isLinearDecreasing: true,
|
||||
isReciprocal: false,
|
||||
isSteppedDecreasing: false
|
||||
} as PalletReferendaCurve;
|
||||
}
|
||||
|
||||
function curveReciprocal (factor: BN | string | number, xOffset: BN | string | number, yOffset: BN | string | number): PalletReferendaCurve {
|
||||
return {
|
||||
asReciprocal: {
|
||||
factor: bnToBn(factor),
|
||||
xOffset: bnToBn(xOffset),
|
||||
yOffset: bnToBn(yOffset)
|
||||
},
|
||||
isLinearDecreasing: false,
|
||||
isReciprocal: true,
|
||||
isSteppedDecreasing: false
|
||||
} as PalletReferendaCurve;
|
||||
}
|
||||
|
||||
// We don't currently have curves on Kusama for this, so needs a check
|
||||
// function curveStepped (begin: BN | string | number, end: BN | string | number, period: BN | string | number, step: BN | string | number): PalletReferendaCurve {
|
||||
// return {
|
||||
// asSteppedDecreasing: {
|
||||
// begin: bnToBn(begin),
|
||||
// end: bnToBn(end),
|
||||
// period: bnToBn(period),
|
||||
// step: bnToBn(step)
|
||||
// },
|
||||
// isLinearDecreasing: false,
|
||||
// isReciprocal: false,
|
||||
// isSteppedDecreasing: true
|
||||
// } as PalletReferendaCurve;
|
||||
// }
|
||||
|
||||
function percentValue (percent: number): BN {
|
||||
return BN_BILLION.muln(percent).divn(100);
|
||||
}
|
||||
|
||||
describe('curveThreshold', (): void => {
|
||||
describe('LinearDecreasing', (): void => {
|
||||
// linear support curve from root track on Kusama
|
||||
const rootCurve = curveLinear(BN_BILLION.divn(2), BN_ZERO, BN_BILLION);
|
||||
|
||||
it('has a correct starting point', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(0), bnToBn(672)).toString()
|
||||
).toEqual(percentValue(50).toString());
|
||||
});
|
||||
|
||||
it('has the correct midpoint', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(672 / 2), bnToBn(672)).toString()
|
||||
).toEqual(percentValue(25).toString());
|
||||
});
|
||||
|
||||
it('has a correct ending point', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(672), bnToBn(672)).toString()
|
||||
).toEqual(percentValue(0).toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reciprocal', (): void => {
|
||||
// linear approval curve from root track on Kusama
|
||||
const rootCurve = curveReciprocal(222_222_224, 333_333_335, 333_333_332);
|
||||
|
||||
it('has a correct starting point', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(0), bnToBn(672)).toString()
|
||||
).toEqual(percentValue(100).toString());
|
||||
});
|
||||
|
||||
it('has the correct midpoints', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(12), bnToBn(672)).toString()
|
||||
).toEqual('966101697'); // 96.61%
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(336), bnToBn(672)).toString()
|
||||
).toEqual(percentValue(60).toString());
|
||||
});
|
||||
|
||||
it('has a correct ending point', (): void => {
|
||||
expect(
|
||||
curveThreshold(rootCurve, bnToBn(672), bnToBn(672)).toString()
|
||||
).toEqual('499999999');
|
||||
});
|
||||
});
|
||||
|
||||
// describe('Stepped', (): void => {
|
||||
|
||||
// });
|
||||
});
|
||||
|
||||
describe('curveDelay', (): void => {
|
||||
describe('LinearDecreasing', (): void => {
|
||||
// linear support curve from root track on Kusama
|
||||
const rootCurve = curveLinear(BN_BILLION.divn(2), BN_ZERO, BN_BILLION);
|
||||
|
||||
it('has the correct result for 25%', (): void => {
|
||||
expect(
|
||||
curveDelay(rootCurve, bnToBn(25), bnToBn(100)).toString()
|
||||
).toEqual('500000000'); // 50% through
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reciprocal', (): void => {
|
||||
// linear approval curve from root track on Kusama
|
||||
const rootCurve = curveReciprocal(222_222_224, 333_333_335, 333_333_332);
|
||||
|
||||
it('has the correct result for 75%', (): void => {
|
||||
expect(
|
||||
curveDelay(rootCurve, bnToBn(75), bnToBn(100)).toString()
|
||||
).toEqual('200000000'); // 20% through
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,12 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { TrackInfo } from './types';
|
||||
import type { PalletConvictionVotingTally, PalletRankedCollectiveTally, PalletReferendaCurve, PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally, PalletReferendaTrackInfo } from '@polkadot/types/lookup';
|
||||
import type { CurveGraph, TrackDescription, TrackInfo } from './types';
|
||||
|
||||
import { getGovernanceTracks } from '@polkadot/apps-config';
|
||||
import { formatNumber, stringPascalCase } from '@polkadot/util';
|
||||
import { BN, BN_BILLION, BN_ONE, BN_ZERO, bnMax, bnMin, formatNumber, stringPascalCase } from '@polkadot/util';
|
||||
|
||||
const CALC_CURVE_LENGTH = 100;
|
||||
|
||||
export function getTrackName (trackId: BN, { name }: PalletReferendaTrackInfo): string {
|
||||
return `${
|
||||
@ -21,15 +22,15 @@ export function getTrackName (trackId: BN, { name }: PalletReferendaTrackInfo):
|
||||
}`;
|
||||
}
|
||||
|
||||
export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda: string, tracks?: [BN, PalletReferendaTrackInfo][], trackId?: number): TrackInfo | undefined {
|
||||
export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda: string, tracks?: TrackDescription[], trackId?: number): TrackInfo | undefined {
|
||||
let info: TrackInfo | undefined;
|
||||
|
||||
if (tracks && trackId !== undefined) {
|
||||
const originMap = getGovernanceTracks(api, specName, palletReferenda);
|
||||
const trackInfo = tracks.find(([id]) => id.eqn(trackId));
|
||||
const trackInfo = tracks.find(({ id }) => id.eqn(trackId));
|
||||
|
||||
if (trackInfo && originMap) {
|
||||
const trackName = trackInfo[1].name.toString();
|
||||
const trackName = trackInfo.info.name.toString();
|
||||
|
||||
info = originMap.find(({ id, name }) =>
|
||||
id === trackId &&
|
||||
@ -40,3 +41,186 @@ export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
export function isConvictionTally (tally: PalletRankedCollectiveTally | PalletConvictionVotingTally): tally is PalletConvictionVotingTally {
|
||||
return !!(tally as PalletConvictionVotingTally).support && !(tally as PalletRankedCollectiveTally).bareAyes;
|
||||
}
|
||||
|
||||
export function isConvictionVote (info: PalletReferendaReferendumInfoConvictionVotingTally | PalletReferendaReferendumInfoRankedCollectiveTally): info is PalletReferendaReferendumInfoConvictionVotingTally {
|
||||
return info.isOngoing && isConvictionTally(info.asOngoing.tally);
|
||||
}
|
||||
|
||||
export function curveThreshold (curve: PalletReferendaCurve, input: BN, div: BN): BN {
|
||||
// if divisor is zero, we return the max
|
||||
if (div.isZero()) {
|
||||
return BN_BILLION;
|
||||
}
|
||||
|
||||
const x = input.mul(BN_BILLION).div(div);
|
||||
|
||||
if (curve.isLinearDecreasing) {
|
||||
const { ceil, floor, length } = curve.asLinearDecreasing;
|
||||
|
||||
// *ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor))
|
||||
// NOTE: We first multiply, then divide (since we work with fractions)
|
||||
return ceil.sub(
|
||||
bnMin(x, length)
|
||||
.mul(ceil.sub(floor))
|
||||
.div(length)
|
||||
);
|
||||
} else if (curve.isSteppedDecreasing) {
|
||||
const { begin, end, period, step } = curve.asSteppedDecreasing;
|
||||
|
||||
// (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end)
|
||||
return bnMax(
|
||||
end,
|
||||
begin.sub(
|
||||
bnMin(
|
||||
begin,
|
||||
step
|
||||
.mul(x)
|
||||
.div(period)
|
||||
)
|
||||
)
|
||||
);
|
||||
} else if (curve.asReciprocal) {
|
||||
const { factor, xOffset, yOffset } = curve.asReciprocal;
|
||||
|
||||
// factor
|
||||
// .checked_rounding_div(FixedI64::from(x) + *x_offset, Low)
|
||||
// .map(|yp| (yp + *y_offset).into_clamped_perthing())
|
||||
// .unwrap_or_else(Perbill::one)
|
||||
return bnMin(
|
||||
BN_BILLION,
|
||||
factor
|
||||
.mul(BN_BILLION)
|
||||
.div(x.add(xOffset))
|
||||
.add(yOffset)
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown curve found ${curve.type}`);
|
||||
}
|
||||
|
||||
export function curveDelay (curve: PalletReferendaCurve, input: BN, div: BN): BN {
|
||||
// if divisor is zero, we return the max
|
||||
if (div.isZero()) {
|
||||
return BN_BILLION;
|
||||
}
|
||||
|
||||
const y = input.mul(BN_BILLION).div(div);
|
||||
|
||||
if (curve.isLinearDecreasing) {
|
||||
const { ceil, floor, length } = curve.asLinearDecreasing;
|
||||
|
||||
// if y < *floor {
|
||||
// Perbill::one()
|
||||
// } else if y > *ceil {
|
||||
// Perbill::zero()
|
||||
// } else {
|
||||
// (*ceil - y).saturating_div(*ceil - *floor, Up).saturating_mul(*length)
|
||||
// }
|
||||
return y.lt(floor)
|
||||
? BN_BILLION
|
||||
: y.gt(ceil)
|
||||
? BN_ZERO
|
||||
: bnMin(
|
||||
BN_BILLION,
|
||||
bnMax(
|
||||
BN_ZERO,
|
||||
ceil
|
||||
.sub(y)
|
||||
.mul(length)
|
||||
.div(ceil.sub(floor))
|
||||
)
|
||||
);
|
||||
} else if (curve.isSteppedDecreasing) {
|
||||
const { begin, end, period, step } = curve.asSteppedDecreasing;
|
||||
|
||||
// if y < *end {
|
||||
// Perbill::one()
|
||||
// } else {
|
||||
// period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step))
|
||||
// }
|
||||
return y.lt(end)
|
||||
? BN_BILLION
|
||||
: bnMin(
|
||||
BN_BILLION,
|
||||
bnMax(
|
||||
BN_ZERO,
|
||||
period
|
||||
.mul(
|
||||
begin
|
||||
.sub(bnMin(y, begin))
|
||||
.add(
|
||||
step.isZero()
|
||||
? step
|
||||
: step.sub(BN_ONE)
|
||||
)
|
||||
)
|
||||
.div(step)
|
||||
)
|
||||
);
|
||||
} else if (curve.asReciprocal) {
|
||||
const { factor, xOffset, yOffset } = curve.asReciprocal;
|
||||
|
||||
// let y = FixedI64::from(y);
|
||||
// let maybe_term = factor.checked_rounding_div(y - *y_offset, High);
|
||||
// maybe_term
|
||||
// .and_then(|term| (term - *x_offset).try_into_perthing().ok())
|
||||
// .unwrap_or_else(Perbill::one)
|
||||
return bnMin(
|
||||
BN_BILLION,
|
||||
bnMax(
|
||||
BN_ZERO,
|
||||
factor
|
||||
.mul(BN_BILLION)
|
||||
.div(y.sub(yOffset))
|
||||
.sub(xOffset)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown curve found ${curve.type}`);
|
||||
}
|
||||
|
||||
export function calcDecidingEnd (totalEligible: BN, tally: PalletRankedCollectiveTally | PalletConvictionVotingTally, { decisionPeriod, minApproval, minSupport }: PalletReferendaTrackInfo, since: BN): BN | undefined {
|
||||
const support = isConvictionTally(tally)
|
||||
? tally.support
|
||||
: tally.bareAyes;
|
||||
|
||||
return since.add(
|
||||
decisionPeriod
|
||||
.mul(
|
||||
bnMax(
|
||||
curveDelay(minApproval, tally.ayes, tally.ayes.add(tally.nays)),
|
||||
curveDelay(minSupport, support, totalEligible)
|
||||
)
|
||||
)
|
||||
.div(BN_BILLION)
|
||||
);
|
||||
}
|
||||
|
||||
export function calcCurves ({ decisionPeriod, minApproval, minSupport }: PalletReferendaTrackInfo): CurveGraph {
|
||||
const approval = new Array<BN>(100);
|
||||
const support = new Array<BN>(100);
|
||||
const x = new Array<BN>(100);
|
||||
const step = decisionPeriod.divn(CALC_CURVE_LENGTH);
|
||||
const last = CALC_CURVE_LENGTH - 1;
|
||||
let current = new BN(0);
|
||||
|
||||
for (let i = 0; i < last; i++) {
|
||||
approval[i] = curveThreshold(minApproval, current, decisionPeriod);
|
||||
support[i] = curveThreshold(minSupport, current, decisionPeriod);
|
||||
x[i] = current;
|
||||
|
||||
current = current.add(step);
|
||||
}
|
||||
|
||||
// since we may be lossy with the step, we explicitly calc the final point at 100%
|
||||
approval[last] = curveThreshold(minApproval, decisionPeriod, decisionPeriod);
|
||||
support[last] = curveThreshold(minSupport, decisionPeriod, decisionPeriod);
|
||||
x[last] = decisionPeriod;
|
||||
|
||||
return { approval, support, x };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user