Crowdloan withdrawals (#4966)
* Crowdloan withdrawals * cleanups * Modal * withdraw batch & single
This commit is contained in:
parent
3a6693fe0a
commit
b341581f51
@ -19,7 +19,7 @@ interface Props {
|
||||
raised: Balance;
|
||||
}
|
||||
|
||||
function FundContribute ({ cap, className, paraId, raised }: Props): React.ReactElement<Props> {
|
||||
function Contribute ({ cap, className, paraId, raised }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
@ -103,4 +103,4 @@ function FundContribute ({ cap, className, paraId, raised }: Props): React.React
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(FundContribute);
|
||||
export default React.memo(Contribute);
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type BN from 'bn.js';
|
||||
import type { StorageKey } from '@polkadot/types';
|
||||
import type { EventRecord, ParaId } from '@polkadot/types/interfaces';
|
||||
import type { Campaign, LeasePeriod } from '../types';
|
||||
|
||||
@ -14,7 +15,8 @@ import { formatNumber } from '@polkadot/util';
|
||||
import { encodeAddress } from '@polkadot/util-crypto';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import FundContribute from './FundContribute';
|
||||
import Contribute from './Contribute';
|
||||
import Withdraw from './Withdraw';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BN;
|
||||
@ -25,16 +27,28 @@ interface Props {
|
||||
}
|
||||
|
||||
interface Contributions {
|
||||
uniqueCount?: number;
|
||||
myAccounts?: string[];
|
||||
uniqueKeys: string[];
|
||||
myAccounts: string[];
|
||||
}
|
||||
|
||||
const NO_CONTRIB: Contributions = { myAccounts: [], uniqueKeys: [] };
|
||||
|
||||
function extractContributors (allAccounts: string[], keys: StorageKey[]): Contributions {
|
||||
const uniqueKeys = keys.map((k) => k.toHex());
|
||||
const contributors = keys.map((k) => encodeAddress(k));
|
||||
|
||||
return {
|
||||
myAccounts: contributors.filter((c) => allAccounts.includes(c)),
|
||||
uniqueKeys
|
||||
};
|
||||
}
|
||||
|
||||
function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKey, info: { cap, depositor, end, firstSlot, lastSlot, raised, retiring }, isCapped, isEnded, isRetired, isWinner, paraId, retireEnd } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const [{ myAccounts, uniqueCount }, setContributors] = useState<Contributions>({});
|
||||
const trigger = useEventTrigger([api.events.crowdloan.Contributed], useCallback(
|
||||
const { allAccounts, isAccount } = useAccounts();
|
||||
const [{ myAccounts, uniqueKeys }, setContributors] = useState<Contributions>(NO_CONTRIB);
|
||||
const trigger = useEventTrigger([api.events.crowdloan.Contributed, api.events.crowdloan.Withdrew], useCallback(
|
||||
({ event: { data: [, fundIndex] } }: EventRecord) =>
|
||||
(fundIndex as ParaId).eq(paraId),
|
||||
[paraId]
|
||||
@ -44,24 +58,15 @@ function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKe
|
||||
trigger &&
|
||||
api.rpc.childstate
|
||||
.getKeys(childKey, '0x')
|
||||
.then((keys) => setContributors((): Contributions => {
|
||||
const contributors = keys.map((k) => encodeAddress(k));
|
||||
|
||||
return {
|
||||
myAccounts: contributors.filter((c) => allAccounts.includes(c)),
|
||||
uniqueCount: contributors.length
|
||||
};
|
||||
}))
|
||||
.then((keys) => setContributors(
|
||||
extractContributors(allAccounts, keys))
|
||||
)
|
||||
.catch(console.error);
|
||||
}, [allAccounts, api, childKey, trigger]);
|
||||
|
||||
const isDepositor = useMemo(
|
||||
(): boolean => {
|
||||
const address = depositor.toString();
|
||||
|
||||
return allAccounts.some((a) => a === address);
|
||||
},
|
||||
[allAccounts, depositor]
|
||||
() => isAccount(depositor.toString()),
|
||||
[depositor, isAccount]
|
||||
);
|
||||
|
||||
const [blocksLeft, retiringLeft] = useMemo(
|
||||
@ -87,8 +92,14 @@ function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKe
|
||||
[cap, raised]
|
||||
);
|
||||
|
||||
const isLeaseOver = !!leasePeriod && (
|
||||
isWinner
|
||||
? leasePeriod.currentPeriod.gt(lastSlot)
|
||||
: leasePeriod.currentPeriod.gt(firstSlot)
|
||||
);
|
||||
const canContribute = isOngoing && blocksLeft && !isCapped && !isWinner && retiring.isFalse;
|
||||
const canDissolve = raised.isZero() || (isRetired && (!isWinner || !leasePeriod || lastSlot.lt(leasePeriod.currentPeriod)));
|
||||
const canDissolve = raised.isZero() || (isRetired && isLeaseOver);
|
||||
const canWithdraw = canDissolve || (!!(bestNumber && bestNumber.gt(end)) && isLeaseOver);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
@ -140,17 +151,24 @@ function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKe
|
||||
<div>{percentage}</div>
|
||||
</td>
|
||||
<td className='number media--1100'>
|
||||
{uniqueCount && (
|
||||
formatNumber(uniqueCount)
|
||||
{uniqueKeys.length !== 0 && (
|
||||
formatNumber(uniqueKeys.length)
|
||||
)}
|
||||
</td>
|
||||
<td className='badge'>
|
||||
<Icon
|
||||
color={myAccounts?.length ? 'green' : 'gray'}
|
||||
color={myAccounts.length ? 'green' : 'gray'}
|
||||
icon='asterisk'
|
||||
/>
|
||||
</td>
|
||||
<td className='button'>
|
||||
{canWithdraw && uniqueKeys.length !== 0 && (
|
||||
<Withdraw
|
||||
allAccounts={uniqueKeys}
|
||||
myAccounts={myAccounts}
|
||||
paraId={paraId}
|
||||
/>
|
||||
)}
|
||||
{canDissolve && (
|
||||
<TxButton
|
||||
accountId={depositor}
|
||||
@ -166,7 +184,7 @@ function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKe
|
||||
/>
|
||||
)}
|
||||
{isOngoing && canContribute && (
|
||||
<FundContribute
|
||||
<Contribute
|
||||
cap={cap}
|
||||
paraId={paraId}
|
||||
raised={raised}
|
||||
|
||||
153
packages/page-parachains/src/Crowdloan/Withdraw.tsx
Normal file
153
packages/page-parachains/src/Crowdloan/Withdraw.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright 2017-2021 @polkadot/app-parachains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { ParaId } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
allAccounts: string[];
|
||||
className?: string;
|
||||
myAccounts: string[];
|
||||
paraId: ParaId;
|
||||
}
|
||||
|
||||
function createExtrinsics (api: ApiPromise, addresses: string[], paraId: ParaId, maxPayouts: number): SubmittableExtrinsic<'promise'>[] {
|
||||
return addresses
|
||||
.reduce((batches: SubmittableExtrinsic<'promise'>[][], address): SubmittableExtrinsic<'promise'>[][] => {
|
||||
const tx = api.tx.crowdloan.withdraw(address, paraId);
|
||||
const batch = batches[batches.length - 1];
|
||||
|
||||
if (batch.length >= maxPayouts) {
|
||||
batches.push([tx]);
|
||||
} else {
|
||||
batch.push(tx);
|
||||
}
|
||||
|
||||
return batches;
|
||||
}, [[]])
|
||||
.reduce((extrinsics: SubmittableExtrinsic<'promise'>[], batch): SubmittableExtrinsic<'promise'>[] => {
|
||||
if (batch.length === 1) {
|
||||
extrinsics.push(batch[0]);
|
||||
} else if (isFunction(api.tx.utility?.batch)) {
|
||||
extrinsics.push(api.tx.utility.batch(batch));
|
||||
} else {
|
||||
extrinsics.push(...batch);
|
||||
}
|
||||
|
||||
return extrinsics;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function Withdraw ({ allAccounts, className, myAccounts, paraId }: 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 [extrinsics, setExtrinsics] = useState<SubmittableExtrinsic<'promise'>[] | null>(null);
|
||||
const [batchSize, setBatchSize] = useState(0);
|
||||
const [withdrawType, setWithdrawType] = useState(0);
|
||||
|
||||
const typeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t<string>('Withdraw for all contributed accounts'),
|
||||
value: 0
|
||||
},
|
||||
myAccounts.length && {
|
||||
text: t<string>('Withdraw only for my accounts'),
|
||||
value: 1
|
||||
}
|
||||
].filter((o) => !!o),
|
||||
[myAccounts, t]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
api.tx.crowdloan
|
||||
.withdraw(allAccounts[0], paraId)
|
||||
.paymentInfo(allAccounts[0])
|
||||
.then((info) => setBatchSize(
|
||||
info.weight.isZero()
|
||||
? 128
|
||||
: Math.floor(
|
||||
api.consts.system.blockWeights.maxBlock
|
||||
.muln(64) // 65% of the block weight on a single extrinsic (64 for safety)
|
||||
.div(info.weight)
|
||||
.toNumber() / 100
|
||||
)
|
||||
))
|
||||
.catch(console.error);
|
||||
}, [api, allAccounts, paraId]);
|
||||
|
||||
useEffect((): void => {
|
||||
const addresses = withdrawType === 0
|
||||
? allAccounts
|
||||
: myAccounts;
|
||||
|
||||
console.log('batchSize=', batchSize);
|
||||
|
||||
setExtrinsics(() =>
|
||||
batchSize && addresses.length
|
||||
? createExtrinsics(api, allAccounts, paraId, batchSize)
|
||||
: null
|
||||
);
|
||||
}, [api, allAccounts, batchSize, myAccounts, paraId, withdrawType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='minus'
|
||||
isDisabled={!hasAccounts}
|
||||
label={t<string>('Withdraw')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t<string>('Withdraw from fund')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t<string>('This account will be used to send the transaction.')}>
|
||||
<InputAddress
|
||||
label={t<string>('requesting from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t<string>('The type of withdrawal, all accounts or only own accounts (if available)')}>
|
||||
<Dropdown
|
||||
label={t<string>('withdrawal type')}
|
||||
onChange={setWithdrawType}
|
||||
options={typeOptions}
|
||||
value={withdrawType}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsics}
|
||||
icon='credit-card'
|
||||
isDisabled={!extrinsics || !extrinsics.length || !accountId}
|
||||
label={t<string>('Withdraw')}
|
||||
onStart={toggleOpen}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Withdraw);
|
||||
@ -13,16 +13,18 @@ interface UseAccounts {
|
||||
isAccount: (address: string) => boolean;
|
||||
}
|
||||
|
||||
const EMPTY: UseAccounts = { allAccounts: [], hasAccounts: false, isAccount: () => false };
|
||||
|
||||
export function useAccounts (): UseAccounts {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<UseAccounts>({ allAccounts: [], hasAccounts: false, isAccount: () => false });
|
||||
const [state, setState] = useState<UseAccounts>(EMPTY);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const subscription = keyring.accounts.subject.subscribe((accounts): void => {
|
||||
if (mountedRef.current) {
|
||||
const allAccounts = accounts ? Object.keys(accounts) : [];
|
||||
const hasAccounts = allAccounts.length !== 0;
|
||||
const isAccount = (address: string): boolean => allAccounts.includes(address);
|
||||
const isAccount = (address: string) => allAccounts.includes(address);
|
||||
|
||||
setState({ allAccounts, hasAccounts, isAccount });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user