Crowdloan contribution (#4819)

* Crowdload contribution

* Adjust

* Add percentage raised
This commit is contained in:
Jaco Greeff 2021-03-11 11:47:10 +01:00 committed by GitHub
parent d20acb2ebc
commit e566bc321f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 19 deletions

View File

@ -10,6 +10,8 @@ import { AddressMini, ParaLink } from '@polkadot/react-components';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import FundContribute from './FundContribute';
interface Props {
bestNumber?: BN;
className?: string;
@ -18,21 +20,41 @@ interface Props {
}
function Fund ({ bestNumber, className, info: { cap, depositor, end, raised }, paraId }: Props): React.ReactElement<Props> {
const remaining = useMemo(() => bestNumber && end.sub(bestNumber), [bestNumber, end]);
const remaining = useMemo(
() => bestNumber && end.gt(bestNumber) && end.sub(bestNumber),
[bestNumber, end]
);
const percentage = useMemo(
() => `${(raised.muln(10000).div(cap).toNumber() / 100).toFixed(2)}%`,
[cap, raised]
);
return (
<tr className={className}>
<td className='number'><h1>{formatNumber(paraId)}</h1></td>
<td><ParaLink id={paraId} /></td>
<td className='address'><AddressMini value={depositor} /></td>
<td className='number'><FormatBalance value={raised} /></td>
<td className='number'><FormatBalance value={cap} /></td>
<td className='number'>
<FormatBalance value={raised} />
<div>{percentage}</div>
</td>
<td className='number'>{remaining && (
<>
<BlockToTime blocks={remaining} />
#{formatNumber(end)}
</>
)}</td>
<td className='button'>
{remaining && (
<FundContribute
cap={cap}
paraId={paraId}
raised={raised}
/>
)}
</td>
</tr>
);
}

View File

@ -122,6 +122,7 @@ function FundAdd ({ bestNumber, className }: Props): React.ReactElement<Props> {
accountId={accountId}
icon='plus'
isDisabled={!paraId?.gt(BN_ZERO) || !cap?.gt(BN_ZERO) || !firstSlot?.gt(BN_ZERO) || isEndError || isLastError}
label={t<string>('Add')}
onStart={toggleOpen}
params={[paraId, cap, firstSlot, lastSlot, endBlock, null]}
tx={api.tx.crowdloan.create}

View File

@ -0,0 +1,102 @@
// Copyright 2017-2021 @polkadot/app-crowdloan authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
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 { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { useTranslation } from './translate';
interface Props {
cap: Balance;
className?: string;
paraId: ParaId;
raised: Balance;
}
function FundContribute ({ cap, className, paraId, raised }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { hasAccounts } = useAccounts();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [amount, setAmount] = useState<BN | undefined>();
const maxAmount = useMemo(
() => cap.sub(raised),
[cap, raised]
);
// TODO verifier signature
// TODO check against min contribution
const isAmountError = !amount?.gt(BN_ZERO);
return (
<>
<Button
icon='plus'
isDisabled={!hasAccounts}
label={t<string>('Contribute')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
className={className}
header={t<string>('Contribute to campaign')}
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<InputAddress
label={t<string>('contribute from')}
onChange={setAccountId}
type='account'
value={accountId}
/>
</Modal.Column>
<Modal.Column>
{t<string>('This account will contribute to the crowdloan.')}
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputBalance
autoFocus
isError={isAmountError}
isZeroable={false}
label={t<string>('contribution')}
maxValue={maxAmount}
onChange={setAmount}
/>
</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
accountId={accountId}
icon='plus'
isDisabled={isAmountError}
label={t<string>('Contribute')}
onStart={toggleOpen}
params={[paraId, amount, null]}
tx={api.tx.crowdloan.contribute}
/>
</Modal.Actions>
</Modal>
)}
</>
);
}
export default React.memo(FundContribute);

View File

@ -32,9 +32,10 @@ function Funds ({ bestNumber, className, paraIds }: Props): React.ReactElement<P
const headerRef = useRef([
[t('campaigns'), 'start', 3],
[t('raised'), 'number'],
[t('cap'), 'number'],
[t('end'), 'number']
[t('raised'), 'number'],
[t('end'), 'number'],
[undefined, 'button']
]);
// we actually want to split this further info completed and ongoing

View File

@ -21,7 +21,7 @@ interface Props {
function Overview ({ className, proposals }: Props): React.ReactElement<Props> {
const { api } = useApi();
const paraIds = useCall<ParaId[]>(api.query.paras?.parachains);
const sessionTrigger = useEventTrigger([api.events.session.NewSession]);
const sessionTrigger = useEventTrigger([api.events.session.NewSession, api.events.registrar?.Registered]);
const mountedRef = useIsMountedRef();
const [upcomingIds, setUpcomingIds] = useState<ParaId[]>([]);

View File

@ -8,7 +8,7 @@ import React, { useCallback, useState } from 'react';
import { Button, Input, InputAddress, InputBalance, InputFile, InputNumber, InputWasm, MarkWarning, Modal, TxButton } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { BN_TEN, BN_THOUSAND, compactAddLength } from '@polkadot/util';
import { BN_TEN, BN_THOUSAND, BN_ZERO, compactAddLength } from '@polkadot/util';
import { useTranslation } from '../translate';
@ -49,7 +49,7 @@ function Propose ({ className, onClose }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [accountId, setAccountId] = useState<string | null>(null);
const [name, setName] = useState('');
const [paraId, setParaId] = useState(() => new BN(Date.now() % 131072));
const [paraId, setParaId] = useState<BN | undefined>();
const [balance, setBalance] = useState(() => BN_THOUSAND.mul(BN_TEN.pow(new BN(api.registry.chainDecimals[0]))));
const [validators, setValidators] = useState<string[]>(['']);
const [{ isWasmValid, wasm }, setWasm] = useState<CodeState>({ isWasmValid: false, wasm: null });
@ -113,8 +113,8 @@ function Propose ({ className, onClose }: Props): React.ReactElement<Props> {
onChange={setName}
/>
<InputNumber
defaultValue={paraId.toString()}
label={t<string>('requested id (random id pre-filled)')}
isZeroable={false}
label={t<string>('requested id')}
onChange={setParaId}
/>
<InputBalance
@ -187,7 +187,7 @@ function Propose ({ className, onClose }: Props): React.ReactElement<Props> {
<TxButton
accountId={accountId}
icon='plus'
isDisabled={!isWasmValid || !genesisState || !isNameValid || !validators.length}
isDisabled={!isWasmValid || !genesisState || !isNameValid || !validators.length || !paraId?.gt(BN_ZERO)}
onStart={onClose}
params={[paraId, name, wasm, genesisState, validators, balance]}
tx={api.tx.proposeParachain?.proposeParachain}

View File

@ -1,6 +1,8 @@
// Copyright 2017-2021 @polkadot/app-parachains authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import React, { useCallback, useState } from 'react';
import { InputAddress, InputFile, InputNumber, InputWasm, Modal, TxButton } from '@polkadot/react-components';
@ -23,7 +25,7 @@ function RegisterThread ({ className, onClose }: Props): React.ReactElement<Prop
const { t } = useTranslation();
const { api } = useApi();
const [accountId, setAccountId] = useState<string | null>(null);
const [paraId, setParaId] = useState(BN_ZERO);
const [paraId, setParaId] = useState<BN | undefined>();
const [{ isWasmValid, wasm }, setWasm] = useState<CodeState>({ isWasmValid: false, wasm: null });
const [genesisState, setGenesisState] = useState<Uint8Array | null>(null);
@ -60,6 +62,8 @@ function RegisterThread ({ className, onClose }: Props): React.ReactElement<Prop
<Modal.Columns>
<Modal.Column>
<InputNumber
autoFocus
isZeroable={false}
label={t<string>('parachain id')}
onChange={setParaId}
/>
@ -100,7 +104,7 @@ function RegisterThread ({ className, onClose }: Props): React.ReactElement<Prop
<TxButton
accountId={accountId}
icon='plus'
isDisabled={!isWasmValid || !genesisState}
isDisabled={!isWasmValid || !genesisState || !paraId?.gt(BN_ZERO)}
onStart={onClose}
params={[paraId, genesisState, wasm]}
tx={api.tx.registrar.register}

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { AugmentedEvent } from '@polkadot/api/types';
import type { Vec } from '@polkadot/types';
import type { EventRecord } from '@polkadot/types/interfaces';
import { useEffect, useState } from 'react';
@ -14,22 +15,20 @@ import { useIsMountedRef } from './useIsMountedRef';
type EventCheck = AugmentedEvent<'promise'> | string | false | undefined | null;
export function useEventTrigger (checks: EventCheck[]): number {
export function useEventTrigger (checks: EventCheck[]): string {
const { api } = useApi();
const [trigger, setTrigger] = useState(() => Date.now());
const [trigger, setTrigger] = useState('0x00');
const mountedRef = useIsMountedRef();
const eventRecords = useCall<EventRecord[]>(api.query.system.events);
const eventRecords = useCall<Vec<EventRecord>>(api.query.system.events);
useEffect((): void => {
const count = eventRecords && eventRecords.filter(({ event }) =>
mountedRef.current && eventRecords && eventRecords.filter(({ event }) =>
event && checks.some((check) => check && (
isString(check)
? event.section === check
: check.is(event)
))
).length;
mountedRef.current && count && setTrigger(Date.now());
).length && setTrigger(() => eventRecords.createdAtHash?.toHex() || '0x00');
}, [eventRecords, checks, mountedRef]);
return trigger;