Crowdloan contribution (#4819)
* Crowdload contribution * Adjust * Add percentage raised
This commit is contained in:
parent
d20acb2ebc
commit
e566bc321f
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
102
packages/page-crowdloan/src/FundContribute.tsx
Normal file
102
packages/page-crowdloan/src/FundContribute.tsx
Normal 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);
|
||||
@ -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
|
||||
|
||||
@ -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[]>([]);
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user