Initial display for auction bids (#4828)
* Initial display for auction bids * Dedupe bids
This commit is contained in:
parent
27b97600c4
commit
09c1fb716d
@ -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;
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
35
packages/page-parachains/src/Auctions/WinBlock.tsx
Normal file
35
packages/page-parachains/src/Auctions/WinBlock.tsx
Normal 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);
|
||||
47
packages/page-parachains/src/Auctions/WinRange.tsx
Normal file
47
packages/page-parachains/src/Auctions/WinRange.tsx
Normal 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);
|
||||
@ -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}
|
||||
|
||||
16
packages/page-parachains/src/Auctions/types.ts
Normal file
16
packages/page-parachains/src/Auctions/types.ts
Normal 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[];
|
||||
}
|
||||
66
packages/page-parachains/src/Auctions/useWinningData.ts
Normal file
66
packages/page-parachains/src/Auctions/useWinningData.ts
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -209,6 +209,10 @@ export default React.memo(styled(Body)`
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.noBorder td {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ui--Button-Group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
34
packages/react-hooks/src/useExtrinsicTrigger.ts
Normal file
34
packages/react-hooks/src/useExtrinsicTrigger.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user