Sort cowdloan via status, no winner contribute (#4946)

This commit is contained in:
Jaco Greeff 2021-03-26 11:49:40 +01:00 committed by GitHub
parent bfcb94d5ae
commit f0b9dbcf24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 117 additions and 49 deletions

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { BlockNumber } from '@polkadot/types/interfaces';
import type { Campaign } from './types';
import React, { useMemo } from 'react';
@ -22,7 +21,7 @@ interface Props {
value: Campaign;
}
function Fund ({ bestNumber, className, isOngoing, value: { info: { cap, depositor, end, firstSlot, lastSlot, raised, retiring }, isEnded, paraId } }: Props): React.ReactElement<Props> {
function Fund ({ bestNumber, className, isOngoing, value: { info: { cap, depositor, end, firstSlot, lastSlot, raised, retiring }, isCapped, isEnded, isWinner, paraId } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
@ -43,17 +42,14 @@ function Fund ({ bestNumber, className, isOngoing, value: { info: { cap, deposit
// TODO Dissolve should look at retirement and the actual period
const [percentage, isCapped] = useMemo(
() => [
cap.isZero()
? '100.00%'
: `${(raised.muln(10000).div(cap).toNumber() / 100).toFixed(2)}%`,
cap.sub(raised).lt(api.consts.crowdloan.minContribution as BlockNumber)
],
[api, cap, raised]
const percentage = useMemo(
() => cap.isZero()
? '100.00%'
: `${(raised.muln(10000).div(cap).toNumber() / 100).toFixed(2)}%`,
[cap, raised]
);
const canContribute = blocksLeft && !isCapped && retiring.isFalse;
const canContribute = blocksLeft && !isCapped && !isWinner && retiring.isFalse;
const canDissolve = raised.isZero();
return (
@ -63,11 +59,13 @@ function Fund ({ bestNumber, className, isOngoing, value: { info: { cap, deposit
<td>
{retiring.isTrue
? t<string>('Retiring')
: blocksLeft
? isCapped
? t<string>('Capped')
: t<string>('Active')
: t<string>('Ended')
: isWinner
? t<string>('Winner')
: blocksLeft
? isCapped
? t<string>('Capped')
: t<string>('Active')
: t<string>('Ended')
}
</td>
<td className='address'><AddressMini value={depositor} /></td>

View File

@ -1,11 +1,23 @@
// Copyright 2017-2021 @polkadot/app-parachains authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { FundInfo, ParaId } from '@polkadot/types/interfaces';
import type { AccountId, BalanceOf, FundInfo, ParaId } from '@polkadot/types/interfaces';
export interface Campaign {
info: FundInfo;
isCapped?: boolean;
isEnded?: boolean;
isWinner?: boolean;
key: string;
paraId: ParaId;
}
export interface LeaseLease {
accountId: AccountId;
balance: BalanceOf;
}
export interface Lease {
leases: LeaseLease[];
paraId: ParaId;
}

View File

@ -2,14 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@polkadot/types';
import type { BlockNumber, FundInfo, ParaId } from '@polkadot/types/interfaces';
import type { Campaign } from './types';
import type { AccountId, BalanceOf, BlockNumber, FundInfo, ParaId } from '@polkadot/types/interfaces';
import type { ITuple } from '@polkadot/types/types';
import type { Campaign, Lease } from './types';
import BN from 'bn.js';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useApi, useBestNumber, useCallMulti, useEventTrigger } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { useApi, useBestNumber, useCall, useEventTrigger } from '@polkadot/react-hooks';
import { BN_ZERO, stringToU8a, u8aConcat, u8aEq } from '@polkadot/util';
interface Result {
activeCap: BN;
@ -27,18 +28,47 @@ const EMPTY: Result = {
totalRaised: BN_ZERO
};
// extract fund info from the available paraIds
function extractCampaigns (optFunds: Option<FundInfo>[], paraIds: ParaId[]): Campaign[] | null {
return optFunds && paraIds.length === optFunds.length
? paraIds
.map((paraId, i) => ({ info: optFunds[i].unwrapOr(null), key: paraId.toString(), paraId }))
.filter((fund): fund is Campaign => !!fund.info)
.sort((a, b) => a.info.firstSlot.cmp(b.info.firstSlot) || a.info.lastSlot.cmp(b.info.lastSlot) || a.paraId.cmp(b.paraId))
: null;
const PREFIX = stringToU8a('modlpy/cfund');
function hasLease (paraId: ParaId, leases: Lease[]): boolean {
const info = leases.find((l) => paraId.eq(l.paraId));
if (info) {
const paraAccountId = u8aConcat(PREFIX, paraId.toU8a());
return info.leases.some(({ accountId }) =>
u8aEq(paraAccountId, accountId.toU8a().slice(0, paraAccountId.length))
);
}
return false;
}
// map into a campaign
function updateCampaign (bestNumber: BN, minContribution: BN, data: Campaign, leases: Lease[]): Campaign {
if (!data.isEnded && bestNumber.gt(data.info.end)) {
data.isEnded = true;
}
if (!data.isCapped && data.info.cap.sub(data.info.raised).lt(minContribution)) {
data.isCapped = true;
}
if (!data.isWinner && hasLease(data.paraId, leases)) {
data.isWinner = true;
}
return data;
}
function isFundUpdated (bestNumber: BlockNumber, minContribution: BN, { info: { cap, end, raised }, isCapped, isEnded, isWinner, paraId }: Campaign, leases: Lease[]): boolean {
return (!isEnded && bestNumber.gt(end)) ||
(!isCapped && cap.sub(raised).lt(minContribution)) ||
(!isWinner && hasLease(paraId, leases));
}
// compare the current campaigns against the previous, manually adding ending and calculating the new totals
function createResult (bestNumber: BlockNumber, funds: Campaign[], prev: Result): Result {
function createResult (bestNumber: BlockNumber, minContribution: BN, funds: Campaign[], leases: Lease[], prev: Result): Result {
const [activeRaised, activeCap, totalRaised, totalCap] = funds.reduce(([ar, ac, tr, tc], { info: { cap, end, raised } }) => [
bestNumber.gt(end) ? ar : ar.iadd(raised),
bestNumber.gt(end) ? ac : ac.iadd(cap),
@ -53,7 +83,8 @@ function createResult (bestNumber: BlockNumber, funds: Campaign[], prev: Result)
!prev.funds || prev.funds.length !== funds.length ||
hasNewActiveCap || hasNewActiveRaised ||
hasNewTotalCap || hasNewTotalRaised ||
funds.some(({ info: { end }, isEnded }) => !isEnded && bestNumber.gt(end));
funds.some((c) => isFundUpdated(bestNumber, minContribution, c, leases)) ||
true;
if (!hasChanged) {
return prev;
@ -66,13 +97,23 @@ function createResult (bestNumber: BlockNumber, funds: Campaign[], prev: Result)
activeRaised: hasNewActiveRaised
? activeRaised
: prev.activeRaised,
funds: funds.map((data): Campaign => {
if (!data.isEnded && bestNumber.gt(data.info.end)) {
data.isEnded = true;
}
return data;
}),
funds: funds
.map((c) => updateCampaign(bestNumber, minContribution, c, leases))
.sort((a, b) =>
a.isWinner !== b.isWinner
? a.isWinner
? 1
: -1
: a.isCapped !== b.isCapped
? a.isCapped
? 1
: -1
: a.isEnded !== b.isEnded
? a.isEnded
? 1
: -1
: 0
),
totalCap: hasNewTotalCap
? totalCap
: prev.totalCap,
@ -82,20 +123,37 @@ function createResult (bestNumber: BlockNumber, funds: Campaign[], prev: Result)
};
}
const optFundMulti = {
transform: ([[paraIds], optFunds]: [[ParaId[]], Option<FundInfo>[]]): Campaign[] =>
paraIds
.map((paraId, i): [ParaId, FundInfo | null] => [paraId, optFunds[i].unwrapOr(null)])
.filter((v): v is [ParaId, FundInfo] => !!v[1])
.map(([paraId, info]) => ({ info, key: paraId.toString(), paraId }))
.sort((a, b) => a.info.firstSlot.cmp(b.info.firstSlot) || a.info.lastSlot.cmp(b.info.lastSlot) || a.paraId.cmp(b.paraId)),
withParamsTransform: true
};
const optLeaseMulti = {
transform: ([[paraIds], leases]: [[ParaId[]], Option<ITuple<[AccountId, BalanceOf]>>[][]]): Lease[] =>
paraIds.map((paraId, i): Lease => ({
leases: leases[i]
.map((o) => o.unwrapOr(null))
.filter((v): v is ITuple<[AccountId, BalanceOf]> => !!v)
.map(([accountId, balance]) => ({ accountId, balance })),
paraId
})),
withParamsTransform: true
};
export default function useFunds (): Result {
const { api } = useApi();
const bestNumber = useBestNumber();
const [paraIds, setParaIds] = useState<ParaId[]>([]);
const trigger = useEventTrigger([api.events.crowdloan.Created]);
const optFunds = useCallMulti<Option<FundInfo>[]>(paraIds.map((id) => [api.query.crowdloan.funds, id]));
const campaigns = useCall<Campaign[]>(api.query.crowdloan.funds.multi, [paraIds], optFundMulti);
const leases = useCall<Lease[]>(api.query.slots.leases.multi, [paraIds], optLeaseMulti);
const [result, setResult] = useState<Result>(EMPTY);
// we actually want to split this further info completed and ongoing
const campaigns = useMemo(
() => extractCampaigns(optFunds, paraIds),
[optFunds, paraIds]
);
// on event triggers, update the available paraIds
useEffect((): void => {
trigger &&
@ -109,10 +167,10 @@ export default function useFunds (): Result {
// here we manually add the actual ending status and calculate the totals
useEffect((): void => {
bestNumber && campaigns && setResult((prev) =>
createResult(bestNumber, campaigns, prev)
bestNumber && campaigns && leases && setResult((prev) =>
createResult(bestNumber, api.consts.crowdloan.minContribution as BlockNumber, campaigns, leases, prev)
);
}, [bestNumber, campaigns]);
}, [api, bestNumber, campaigns, leases]);
return result;
}