Adjust fund retrieval (#4949)

* Adjust fund retrieval

* Naming

* Adjust
This commit is contained in:
Jaco Greeff 2021-03-26 15:18:53 +01:00 committed by GitHub
parent ad5df51605
commit b30c84de65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 93 deletions

View File

@ -24,6 +24,8 @@ interface Props {
}
interface Option {
firstSlot: number;
lastSlot: number;
text: string;
value: number;
}
@ -39,19 +41,29 @@ function Bid ({ auctionInfo, className, lastWinners, ownedIds }: Props): React.R
const [isOpen, toggleOpen] = useToggle();
const rangeOpts = useMemo(
(): Option[] =>
auctionInfo.leasePeriod
? RANGES.map(([first, last], value): Option => ({
text: `${formatNumber(auctionInfo.leasePeriod?.addn(first))} - ${formatNumber(auctionInfo.leasePeriod?.addn(last))}`,
value
}))
: [],
(): Option[] => {
if (!auctionInfo.leasePeriod) {
return [];
}
const leasePeriod = auctionInfo.leasePeriod.toNumber();
return RANGES.map(([first, last], value): Option => ({
firstSlot: leasePeriod + first,
lastSlot: leasePeriod + last,
text: `${formatNumber(leasePeriod + first)} - ${formatNumber(leasePeriod + last)}`,
value
}));
},
[auctionInfo]
);
const currentWinner = useMemo(
() => lastWinners && lastWinners.winners.find(({ range: [first, last] }) => RANGES[range][0] === first && RANGES[range][1] === last),
[lastWinners, range]
() => lastWinners && lastWinners.winners.find(({ firstSlot, lastSlot }) =>
rangeOpts[range].firstSlot === firstSlot &&
rangeOpts[range].lastSlot === lastSlot
),
[lastWinners, range, rangeOpts]
);
const isAmountLess = !!(amount && currentWinner) && amount.lte(currentWinner.value);

View File

@ -22,7 +22,7 @@ interface Props {
value: WinnerData;
}
function WinRanges ({ auctionInfo, blockNumber, className = '', isFirst, isLatest, value: { accountId, paraId, range, value } }: Props): React.ReactElement<Props> {
function WinRanges ({ auctionInfo, blockNumber, className = '', isFirst, isLatest, value: { accountId, firstSlot, lastSlot, paraId, value } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
@ -38,9 +38,9 @@ function WinRanges ({ auctionInfo, blockNumber, className = '', isFirst, isLates
<td className='number'><h1>{formatNumber(paraId)}</h1></td>
<td className='badge'><ParaLink id={paraId} /></td>
<td className='address'><AddressMini value={accountId} /></td>
<td className='all number'>{auctionInfo.leasePeriod && (
<Digits value={`${formatNumber(auctionInfo.leasePeriod.addn(range[0]))} - ${formatNumber(auctionInfo.leasePeriod.addn(range[1]))}`} />
)}</td>
<td className='all number'>
<Digits value={`${formatNumber(firstSlot)} - ${formatNumber(lastSlot)}`} />
</td>
<td className='number'><FormatBalance value={value} /></td>
</tr>
);

View File

@ -6,8 +6,9 @@ import type { AccountId, BalanceOf, ParaId } from '@polkadot/types/interfaces';
export interface WinnerData {
accountId: AccountId;
firstSlot: number;
lastSlot: number;
paraId: ParaId;
range: [number, number];
value: BalanceOf;
}

View File

@ -21,28 +21,30 @@ function isNewWinners (a: WinnerData[], b: WinnerData[]): boolean {
}
function isNewOrdering (a: WinnerData[], b: WinnerData[]): boolean {
return a.length !== b.length || a.some(({ paraId, range }, index) =>
return a.length !== b.length || a.some(({ firstSlot, lastSlot, paraId }, index) =>
!paraId.eq(b[index].paraId) ||
range[0] !== b[index].range[0] ||
range[1] !== b[index].range[1]
firstSlot !== b[index].firstSlot ||
lastSlot !== b[index].lastSlot
);
}
function extractWinners (optData: Option<WinningData>): WinnerData[] {
function extractWinners (auctionInfo: AuctionInfo, optData: Option<WinningData>): WinnerData[] {
return optData.isNone
? []
: optData.unwrap().reduce<WinnerData[]>((winners, optEntry, index): WinnerData[] => {
if (optEntry.isSome) {
const [accountId, paraId, value] = optEntry.unwrap();
const period = auctionInfo.leasePeriod?.toNumber() || 0;
const [first, last] = RANGES[index];
winners.push({ accountId, paraId, range: RANGES[index], value });
winners.push({ accountId, firstSlot: first + period, lastSlot: last + period, paraId, value });
}
return winners;
}, []);
}
function createWinning (endBlock: BlockNumber | null, blockOffset: BN | null | undefined, winners: WinnerData[]): Winning {
function createWinning ({ endBlock }: AuctionInfo, blockOffset: BN | null | undefined, winners: WinnerData[]): Winning {
return {
blockNumber: endBlock && blockOffset
? blockOffset.add(endBlock)
@ -53,24 +55,24 @@ function createWinning (endBlock: BlockNumber | null, blockOffset: BN | null | u
};
}
function extractData (endBlock: BlockNumber | null, values: [StorageKey<[BlockNumber]>, Option<WinningData>][]): Winning[] {
function extractData (auctionInfo: AuctionInfo, values: [StorageKey<[BlockNumber]>, Option<WinningData>][]): Winning[] {
return values
.sort(([{ args: [a] }], [{ args: [b] }]) => a.cmp(b))
.reduce((all: Winning[], [{ args: [blockOffset] }, optData]): Winning[] => {
const winners = extractWinners(optData);
const winners = extractWinners(auctionInfo, optData);
winners.length && (
all.length === 0 ||
isNewWinners(winners, all[all.length - 1].winners)
) && all.push(createWinning(endBlock, blockOffset, winners));
) && all.push(createWinning(auctionInfo, blockOffset, winners));
return all;
}, [])
.reverse();
}
function mergeCurrent (prev: Winning[] | null, optCurrent: Option<WinningData>, endBlock: BlockNumber, blockOffset: BN): Winning[] | null {
const current = createWinning(endBlock, blockOffset, extractWinners(optCurrent));
function mergeCurrent (auctionInfo: AuctionInfo, prev: Winning[] | null, optCurrent: Option<WinningData>, blockOffset: BN): Winning[] | null {
const current = createWinning(auctionInfo, blockOffset, extractWinners(auctionInfo, optCurrent));
if (current.winners.length) {
if (!prev || !prev.length) {
@ -91,10 +93,10 @@ function mergeCurrent (prev: Winning[] | null, optCurrent: Option<WinningData>,
return prev;
}
function mergeFirst (prev: Winning[] | null, optFirstData: Option<WinningData>): Winning[] | null {
function mergeFirst (auctionInfo: AuctionInfo, prev: Winning[] | null, optFirstData: Option<WinningData>): Winning[] | null {
if (prev && prev.length <= 1) {
const updated: Winning[] = prev || [];
const firstEntry = createWinning(null, null, extractWinners(optFirstData));
const firstEntry = createWinning(auctionInfo, null, extractWinners(auctionInfo, optFirstData));
if (!firstEntry.winners.length) {
return updated;
@ -122,34 +124,32 @@ export default function useWinningData (auctionInfo: AuctionInfo): Winning[] | n
// should be fired once, all entries as an initial round
useEffect((): void => {
allEntries && setResult(
extractData(auctionInfo.endBlock, allEntries)
extractData(auctionInfo, allEntries)
);
}, [allEntries, auctionInfo]);
// when block 0 changes, update (typically in non-ending-period, static otherwise)
useEffect((): void => {
optFirstData && setResult((prev) =>
mergeFirst(prev, optFirstData)
mergeFirst(auctionInfo, prev, optFirstData)
);
}, [optFirstData]);
}, [auctionInfo, optFirstData]);
// on a bid event, get the new entry (assuming the event really triggered, i.e. not just a block)
// and add it to the list when not duplicated. Additionally we cleanup after ourselves when endBlock
// gets cleared
useEffect((): void => {
const { endBlock } = auctionInfo;
if (!endBlock) {
if (!auctionInfo.endBlock) {
setResult((prev) => prev && prev.length ? [] : prev);
} else if (bestNumber && bestNumber.gt(endBlock) && triggerRef.current !== trigger) {
const blockOffset = bestNumber.sub(endBlock).iadd(BN_ONE);
} else if (bestNumber && bestNumber.gt(auctionInfo.endBlock) && triggerRef.current !== trigger) {
const blockOffset = bestNumber.sub(auctionInfo.endBlock).iadd(BN_ONE);
triggerRef.current = trigger;
api.query.auctions
?.winning<Option<WinningData>>(blockOffset)
.then((optCurrent) => setResult((prev) =>
mergeCurrent(prev, optCurrent, endBlock, blockOffset)
mergeCurrent(auctionInfo, prev, optCurrent, blockOffset)
))
.catch(console.error);
}

View File

@ -4,15 +4,15 @@
import type { Option } from '@polkadot/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 type { Campaign } from './types';
import BN from 'bn.js';
import { useEffect, useState } from 'react';
import { useApi, useBestNumber, useCall, useEventTrigger } from '@polkadot/react-hooks';
import { BN_ZERO, stringToU8a, u8aConcat, u8aEq } from '@polkadot/util';
import { BN_ZERO, stringToU8a, u8aConcat } from '@polkadot/util';
interface Result {
interface FundResult {
activeCap: BN;
activeRaised: BN;
funds: Campaign[] | null;
@ -20,7 +20,7 @@ interface Result {
totalRaised: BN;
}
const EMPTY: Result = {
const EMPTY: FundResult = {
activeCap: BN_ZERO,
activeRaised: BN_ZERO,
funds: null,
@ -29,55 +29,39 @@ const EMPTY: Result = {
};
const PREFIX = stringToU8a('modlpy/cfund');
const EMPTY_U8A = new Uint8Array(32);
function hasLease (paraId: ParaId, leases: Lease[]): boolean {
const info = leases.find((l) => paraId.eq(l.paraId));
function isCrowdloadAccount (paraId: ParaId, accountId: AccountId): boolean {
return accountId.eq(u8aConcat(PREFIX, paraId.toU8a(), EMPTY_U8A).subarray(0, 32));
}
if (info) {
const paraAccountId = u8aConcat(PREFIX, paraId.toU8a());
return info.leases.some(({ accountId }) =>
u8aEq(paraAccountId, accountId.toU8a().slice(0, paraAccountId.length))
);
}
return false;
function hasLease (paraId: ParaId, leased: ParaId[]): boolean {
return leased.some((l) => l.eq(paraId));
}
// map into a campaign
function updateCampaign (bestNumber: BN, minContribution: BN, retirementPeriod: 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.retireEnd) {
data.retireEnd = data.info.end.add(retirementPeriod);
}
if (!data.isRetired && bestNumber.gt(data.retireEnd)) {
data.isRetired = true;
}
if (!data.isWinner && hasLease(data.paraId, leases)) {
data.isWinner = true;
}
function updateFund (bestNumber: BN, minContribution: BN, retirementPeriod: BN, data: Campaign, leased: ParaId[]): Campaign {
data.isCapped = data.info.cap.sub(data.info.raised).lt(minContribution);
data.isEnded = bestNumber.gt(data.info.end);
data.retireEnd = data.info.end.add(retirementPeriod);
data.isRetired = bestNumber.gt(data.retireEnd);
data.isWinner = hasLease(data.paraId, leased);
return data;
}
function isFundUpdated (bestNumber: BlockNumber, minContribution: BN, retirementPeriod: BN, { info: { cap, end, raised }, isCapped, isEnded, isRetired, isWinner, paraId }: Campaign, leases: Lease[]): boolean {
return (!isEnded && bestNumber.gt(end)) ||
(!isCapped && cap.sub(raised).lt(minContribution)) ||
(!isRetired && bestNumber.gt(end.add(retirementPeriod))) ||
(!isWinner && hasLease(paraId, leases));
function isFundUpdated (bestNumber: BlockNumber, minContribution: BN, retirementPeriod: BN, { info: { cap, end, raised }, paraId }: Campaign, leased: ParaId[], allPrev: FundResult): boolean {
const prev = allPrev.funds?.find((p) => p.paraId.eq(paraId));
return !prev ||
(!prev.isEnded && bestNumber.gt(end)) ||
(!prev.isCapped && cap.sub(raised).lt(minContribution)) ||
(!prev.isRetired && bestNumber.gt(end.add(retirementPeriod))) ||
(!prev.isWinner && hasLease(paraId, leased));
}
// compare the current campaigns against the previous, manually adding ending and calculating the new totals
function createResult (bestNumber: BlockNumber, minContribution: BN, retirementPeriod: BN, funds: Campaign[], leases: Lease[], prev: Result): Result {
function createResult (bestNumber: BlockNumber, minContribution: BN, retirementPeriod: BN, funds: Campaign[], leased: ParaId[], prev: FundResult): FundResult {
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),
@ -90,10 +74,8 @@ function createResult (bestNumber: BlockNumber, minContribution: BN, retirementP
const hasNewTotalRaised = !prev.totalRaised.eq(totalRaised);
const hasChanged =
!prev.funds || prev.funds.length !== funds.length ||
hasNewActiveCap || hasNewActiveRaised ||
hasNewTotalCap || hasNewTotalRaised ||
funds.some((c) => isFundUpdated(bestNumber, minContribution, retirementPeriod, c, leases)) ||
true;
hasNewActiveCap || hasNewActiveRaised || hasNewTotalCap || hasNewTotalRaised ||
funds.some((c) => isFundUpdated(bestNumber, minContribution, retirementPeriod, c, leased, prev));
if (!hasChanged) {
return prev;
@ -107,7 +89,7 @@ function createResult (bestNumber: BlockNumber, minContribution: BN, retirementP
? activeRaised
: prev.activeRaised,
funds: funds
.map((c) => updateCampaign(bestNumber, minContribution, retirementPeriod, c, leases))
.map((c) => updateFund(bestNumber, minContribution, retirementPeriod, c, leased))
.sort((a, b) =>
a.isWinner !== b.isWinner
? a.isWinner
@ -141,31 +123,40 @@ const optFundMulti = {
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)),
.map(([paraId, info]): Campaign => ({
info,
key: paraId.toString(),
paraId
}))
.sort((a, b) =>
a.info.end.cmp(b.info.end) ||
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]
transform: ([[paraIds], leases]: [[ParaId[]], Option<ITuple<[AccountId, BalanceOf]>>[][]]): ParaId[] =>
paraIds.filter((paraId, i) =>
leases[i]
.map((o) => o.unwrapOr(null))
.filter((v): v is ITuple<[AccountId, BalanceOf]> => !!v)
.map(([accountId, balance]) => ({ accountId, balance })),
paraId
})),
.filter(([accountId]) => isCrowdloadAccount(paraId, accountId))
.length !== 0
),
withParamsTransform: true
};
export default function useFunds (): Result {
export default function useFunds (): FundResult {
const { api } = useApi();
const bestNumber = useBestNumber();
const [paraIds, setParaIds] = useState<ParaId[]>([]);
const trigger = useEventTrigger([api.events.crowdloan.Created]);
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);
const leases = useCall<ParaId[]>(api.query.slots.leases.multi, [paraIds], optLeaseMulti);
const [result, setResult] = useState<FundResult>(EMPTY);
// on event triggers, update the available paraIds
useEffect((): void => {