Initial display for auction bids (#4828)

* Initial display for auction bids

* Dedupe bids
This commit is contained in:
Jaco Greeff 2021-03-12 13:08:25 +01:00 committed by GitHub
parent 27b97600c4
commit 09c1fb716d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 253 additions and 33 deletions

View File

@ -209,10 +209,6 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
visibility: visible;
}
&.noBorder td {
border-bottom: 1px solid transparent;
}
.description-column {
max-width: 200px;

View File

@ -6,10 +6,10 @@ import type { AuctionIndex, BlockNumber, LeasePeriodOf } from '@polkadot/types/i
import React, { useRef } from 'react';
import { Table } from '@polkadot/react-components';
import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
import Bid from './Bid';
import useWinningData from './useWinningData';
import WinBlock from './WinBlock';
interface Props {
auctionInfo: [LeasePeriodOf, BlockNumber] | null;
@ -19,32 +19,31 @@ interface Props {
function Auction ({ auctionInfo, className, numAuctions }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const winningData = useWinningData();
const headerRef = useRef([
[t('auctions'), 'start', 1],
[t('duration'), 'number'],
[t('lease period'), 'number'],
[t('winning'), 'number', 3],
[]
[t('winners'), 'start', 4],
[t('range'), 'start'],
[t('value'), 'number']
]);
return (
<Table
className={className}
empty={numAuctions && t<string>('No ongoing auction')}
empty={winningData && numAuctions && auctionInfo
? t<string>('No winners in this auction')
: t<string>('No ongoing auction')
}
header={headerRef.current}
>
{auctionInfo && numAuctions && (
<tr>
<td className='number'><h1>{formatNumber(numAuctions)}</h1></td>
<td className='number all'>{formatNumber(auctionInfo[1])}</td>
<td className='number'>{formatNumber(auctionInfo[0])}</td>
<td colSpan={3} />
<td className='button'>
<Bid id={numAuctions} />
</td>
</tr>
)}
{winningData?.map((value, count) => (
<WinBlock
isEven={!!(count % 2)}
key={value.blockNumber.toString()}
latestBlockNumber={winningData[0].blockNumber}
value={value}
/>
))}
</Table>
);
}

View File

@ -14,7 +14,7 @@ import { useTranslation } from '../translate';
interface Props {
className?: string;
id: AuctionIndex;
id: AuctionIndex | null;
}
function Bid ({ className, id }: Props): React.ReactElement<Props> {
@ -34,7 +34,7 @@ function Bid ({ className, id }: Props): React.ReactElement<Props> {
<>
<Button
icon='plus'
isDisabled={!hasAccounts}
isDisabled={!hasAccounts || !id}
label={t<string>('Bid')}
onClick={toggleOpen}
/>

View File

@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/app-auctions authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AuctionIndex } from '@polkadot/types/interfaces';
import type { AuctionIndex, BlockNumber, LeasePeriodOf } from '@polkadot/types/interfaces';
import React from 'react';
@ -11,11 +11,12 @@ import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
auctionInfo: [LeasePeriodOf, BlockNumber] | null;
className?: string;
numAuctions?: AuctionIndex | null;
}
function Summary ({ className, numAuctions }: Props): React.ReactElement<Props> {
function Summary ({ auctionInfo, className, numAuctions }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
@ -23,6 +24,16 @@ function Summary ({ className, numAuctions }: Props): React.ReactElement<Props>
<CardSummary label={t<string>('auctions')}>
{formatNumber(numAuctions)}
</CardSummary>
{auctionInfo && (
<section>
<CardSummary label={t<string>('period')}>
{formatNumber(auctionInfo[0])}
</CardSummary>
<CardSummary label={t<string>('duration')}>
{formatNumber(auctionInfo[1])}
</CardSummary>
</section>
)}
</SummaryBox>
);
}

View File

@ -0,0 +1,35 @@
// Copyright 2017-2021 @polkadot/app-crowdloan authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber } from '@polkadot/types/interfaces';
import type { Winning } from './types';
import React from 'react';
import WinRange from './WinRange';
interface Props {
className?: string;
isEven: boolean;
latestBlockNumber: BlockNumber;
value: Winning;
}
function WinBlock ({ className = '', isEven, latestBlockNumber, value: { blockNumber, winners } }: Props): React.ReactElement<Props> {
return (
<>
{winners.map((value, index) => (
<WinRange
blockNumber={blockNumber}
className={`${className} ${isEven ? 'isEven' : 'isOdd'} ${index === (winners.length - 1) ? '' : 'noBorder'}`}
isFirst={index === 0}
key={index}
latestBlockNumber={latestBlockNumber}
value={value}
/>
))}
</>
);
}
export default React.memo(WinBlock);

View File

@ -0,0 +1,47 @@
// Copyright 2017-2021 @polkadot/app-crowdloan authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber } from '@polkadot/types/interfaces';
import type { WinnerData } from './types';
import React from 'react';
import { AddressMini, ParaLink } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
blockNumber: BlockNumber;
className?: string;
isFirst: boolean;
latestBlockNumber: BlockNumber;
value: WinnerData;
}
function WinRanges ({ blockNumber, className = '', isFirst, latestBlockNumber, value: { accountId, paraId, range, value } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<tr className={className}>
<td>
{isFirst && (
<h1>
{blockNumber.eq(latestBlockNumber)
? t<string>('latest')
: <>-{formatNumber(latestBlockNumber.sub(blockNumber))}</>
}
</h1>
)}
</td>
<td className='number'><h1>{formatNumber(paraId)}</h1></td>
<td><ParaLink id={paraId} /></td>
<td className='address'><AddressMini value={accountId} /></td>
<td>{range}</td>
<td className='number'><FormatBalance value={value} /></td>
</tr>
);
}
export default React.memo(WinRanges);

View File

@ -7,9 +7,11 @@ import type { ITuple } from '@polkadot/types/types';
import React from 'react';
import { Button } from '@polkadot/react-components';
import { useApi, useCallMulti } from '@polkadot/react-hooks';
import Auction from './Auction';
import Bid from './Bid';
import Summary from './Summary';
interface Props {
@ -41,7 +43,13 @@ function Auctions ({ className }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Summary numAuctions={numAuctions} />
<Summary
auctionInfo={auctionInfo}
numAuctions={numAuctions}
/>
<Button.Group>
<Bid id={auctionInfo && numAuctions} />
</Button.Group>
<Auction
auctionInfo={auctionInfo}
numAuctions={numAuctions}

View File

@ -0,0 +1,16 @@
// Copyright 2017-2021 @polkadot/app-crowdloan authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId, BalanceOf, BlockNumber, ParaId } from '@polkadot/types/interfaces';
export interface WinnerData {
accountId: AccountId;
paraId: ParaId;
range: string;
value: BalanceOf;
}
export interface Winning {
blockNumber: BlockNumber;
winners: WinnerData[];
}

View File

@ -0,0 +1,66 @@
// Copyright 2017-2021 @polkadot/app-crowdloan authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option, StorageKey } from '@polkadot/types';
import type { BlockNumber, WinningData } from '@polkadot/types/interfaces';
import type { WinnerData, Winning } from './types';
import { useEffect, useState } from 'react';
import { useApi, useExtrinsicTrigger } from '@polkadot/react-hooks';
const RANGES = [
'0-0', '0-1', '0-2', '0-3',
'1-1', '1-2', '1-3',
'2-2', '2-3',
'3-3'
];
function flattenData (winning: Winning[]): Winning[] {
return winning
.sort((a, b) => b.blockNumber.cmp(a.blockNumber))
.filter(({ winners }, index, winning) =>
index === 0 ||
JSON.stringify({ winners }) !== JSON.stringify({ winners: winning[index - 1].winners })
);
}
function extractData (values: [StorageKey<[BlockNumber]>, Option<WinningData>][]): Winning[] {
return flattenData(
values.reduce<Winning[]>((winning, [{ args: [blockNumber] }, optData]): Winning[] => {
if (optData.isSome) {
const winners = optData.unwrap().reduce<WinnerData[]>((winners, optEntry, index): WinnerData[] => {
if (optEntry.isSome) {
const [accountId, paraId, value] = optEntry.unwrap();
winners.push({ accountId, paraId, range: RANGES[index], value });
}
return winners;
}, []);
if (winners.length) {
winning.push({ blockNumber, winners });
}
}
return winning;
}, [])
);
}
export default function useWinningData (): Winning[] | null {
const { api } = useApi();
const [winning, setWinning] = useState<Winning[] | null>(null);
const trigger = useExtrinsicTrigger([api.tx.auctions.bid]);
useEffect((): void => {
trigger &&
api.query.auctions.winning
.entries<Option<WinningData>, [BlockNumber]>()
.then((values) => setWinning(extractData(values)))
.catch(console.error);
}, [api, trigger]);
return winning;
}

View File

@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { UInt } from '@polkadot/types';
import type { Balance, ParaId } from '@polkadot/types/interfaces';
import React, { useMemo, useState } from 'react';
import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
import { Button, InputAddress, InputBalance, MarkWarning, Modal, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { BN_ZERO, formatBalance } from '@polkadot/util';
import { useTranslation } from '../translate';
@ -33,9 +34,9 @@ function FundContribute ({ cap, className, paraId, raised }: Props): React.React
);
// TODO verifier signature
// TODO check against min contribution
const isAmountError = !amount?.gt(BN_ZERO);
const isAmountBelow = !amount || amount.lt(api.consts.crowdloan.minContribution as UInt);
const isAmountError = !amount || !amount.gt(BN_ZERO) || isAmountBelow;
return (
<>
@ -75,12 +76,14 @@ function FundContribute ({ cap, className, paraId, raised }: Props): React.React
maxValue={maxAmount}
onChange={setAmount}
/>
{isAmountBelow && (
<MarkWarning content={t<string>('The amount is less than the minimum allowed contribution of {{min}}', { replace: { min: formatBalance(api.consts.crowdloan.minContribution as UInt) } })} />
)}
</Modal.Column>
<Modal.Column>
{t<string>('The amount to contribute. Should be less than the remaining value and more than the minimum contribution amount.')}
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton

View File

@ -209,6 +209,10 @@ export default React.memo(styled(Body)`
background: transparent;
}
&.noBorder td {
border-bottom: 1px solid transparent;
}
.ui--Button-Group {
margin: 0;
}

View File

@ -16,6 +16,7 @@ export { useContracts } from './useContracts';
export { useFormField } from './useFormField';
export { useDebounce } from './useDebounce';
export { useEventTrigger } from './useEventTrigger';
export { useExtrinsicTrigger } from './useExtrinsicTrigger';
export { useFavorites } from './useFavorites';
export { useIncrement } from './useIncrement';
export { calcInflation, useInflation } from './useInflation';

View File

@ -0,0 +1,34 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsicFunction } from '@polkadot/api/types';
import type { SignedBlockExtended } from '@polkadot/api-derive/type';
import { useEffect, useState } from 'react';
import { isString } from '@polkadot/util';
import { useApi } from './useApi';
import { useCall } from './useCall';
import { useIsMountedRef } from './useIsMountedRef';
type ExtrinsicCheck = SubmittableExtrinsicFunction<'promise'> | string | false | undefined | null;
export function useExtrinsicTrigger (checks: ExtrinsicCheck[]): string {
const { api } = useApi();
const [trigger, setTrigger] = useState('0x00');
const mountedRef = useIsMountedRef();
const block = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks);
useEffect((): void => {
mountedRef.current && block && block.extrinsics && block.extrinsics.filter(({ extrinsic }) =>
extrinsic && checks.some((check) => check && (
isString(check)
? extrinsic.method.section === check
: check.is(extrinsic)
))
).length && setTrigger(() => block.createdAtHash?.toHex() || '0x00');
}, [block, checks, mountedRef]);
return trigger;
}