diff --git a/packages/page-referenda/src/Referenda/Group.tsx b/packages/page-referenda/src/Referenda/Group.tsx index 8ca72e2aec..975c698aa6 100644 --- a/packages/page-referenda/src/Referenda/Group.tsx +++ b/packages/page-referenda/src/Referenda/Group.tsx @@ -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 { +function Group ({ activeIssuance, className, isMember, members, palletReferenda, palletVote, ranks, referenda, trackId, trackName, tracks }: Props): React.ReactElement { const { t } = useTranslation(); const { api, specName } = useApi(); @@ -40,16 +40,18 @@ function Group ({ className, isMember, members, palletReferenda, palletVote, ran empty={referenda && t('No active referenda')} header={[ [trackName ? <>{trackName}
{trackInfo?.text}
: t('referenda'), 'start', 7], - [undefined, undefined, 1] + [undefined, undefined, 3] ]} key={ trackName ? `track:${trackName}` : 'untracked' } + withCollapsibleRows > {referenda && referenda.map((r) => ( > = { +const COMPONENTS: Record> = { 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 { - 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('minimum approval'), + t('current approval (failing)'), + t('current approval (passing)') + ], + [ + t('minimum support'), + t('current support (failing)'), + t('current support (passing)') + ] + ], + [t] + ); + return ( - - -

{formatNumber(id)}

- - - - + + +

{formatNumber(id)}

+ + - - + + + + + {chartProps && ( + + )} + + + + {chartProps && isExpanded && ( + + + + + + + + + + + )} + + ); } diff --git a/packages/page-referenda/src/Referenda/Submit.tsx b/packages/page-referenda/src/Referenda/Submit.tsx index c032eb2891..9dd7ac2aec 100644 --- a/packages/page-referenda/src/Referenda/Submit.tsx +++ b/packages/page-referenda/src/Referenda/Submit.tsx @@ -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] diff --git a/packages/page-referenda/src/Referenda/index.tsx b/packages/page-referenda/src/Referenda/index.tsx index 93619f322d..759658713a 100644 --- a/packages/page-referenda/src/Referenda/index.tsx +++ b/packages/page-referenda/src/Referenda/index.tsx @@ -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 { const { t } = useTranslation(); + const { api } = useApi(); + const totalIssuance = useCall(api.query.balances.totalIssuance); + const inactiveIssuance = useCall(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('All active/available tracks'), value: -1 }].concat( grouped @@ -82,6 +91,7 @@ function Referenda ({ className, members, palletReferenda, palletVote, ranks }: {filtered.map(({ key, referenda, trackId, trackName }: ReferendaGroup) => ( 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 | Record[]; diff --git a/packages/page-referenda/src/useReferenda.ts b/packages/page-referenda/src/useReferenda.ts index 35972e8ff0..963c48fc07 100644 --- a/packages/page-referenda/src/useReferenda.ts +++ b/packages/page-referenda/src/useReferenda.ts @@ -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(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] ); } diff --git a/packages/page-referenda/src/useTracks.ts b/packages/page-referenda/src/useTracks.ts index b87b027bc8..ce465985d4 100644 --- a/packages/page-referenda/src/useTracks.ts +++ b/packages/page-referenda/src/useTracks.ts @@ -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] ); diff --git a/packages/page-referenda/src/util.spec.ts b/packages/page-referenda/src/util.spec.ts new file mode 100644 index 0000000000..d81b70a799 --- /dev/null +++ b/packages/page-referenda/src/util.spec.ts @@ -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 + }); + }); +}); diff --git a/packages/page-referenda/src/util.ts b/packages/page-referenda/src/util.ts index fab449ff0e..5e85e0ed96 100644 --- a/packages/page-referenda/src/util.ts +++ b/packages/page-referenda/src/util.ts @@ -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(100); + const support = new Array(100); + const x = new Array(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 }; +}