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:
Jaco 2022-12-08 09:40:48 +02:00 committed by GitHub
parent 14794ebcd9
commit bf69fbb625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 547 additions and 54 deletions

View File

@ -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}

View File

@ -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>
</>
);
}

View File

@ -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]

View File

@ -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}

View File

@ -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>[];

View File

@ -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]
);
}

View File

@ -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]
);

View 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
});
});
});

View File

@ -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 };
}