diff --git a/packages/page-parachains/src/Crowdloan/FundContribute.tsx b/packages/page-parachains/src/Crowdloan/Contribute.tsx similarity index 96% rename from packages/page-parachains/src/Crowdloan/FundContribute.tsx rename to packages/page-parachains/src/Crowdloan/Contribute.tsx index 5e1213b67f..16717b03ca 100644 --- a/packages/page-parachains/src/Crowdloan/FundContribute.tsx +++ b/packages/page-parachains/src/Crowdloan/Contribute.tsx @@ -19,7 +19,7 @@ interface Props { raised: Balance; } -function FundContribute ({ cap, className, paraId, raised }: Props): React.ReactElement { +function Contribute ({ cap, className, paraId, raised }: Props): React.ReactElement { 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); diff --git a/packages/page-parachains/src/Crowdloan/Fund.tsx b/packages/page-parachains/src/Crowdloan/Fund.tsx index fedbe9fef2..16c1ec241f 100644 --- a/packages/page-parachains/src/Crowdloan/Fund.tsx +++ b/packages/page-parachains/src/Crowdloan/Fund.tsx @@ -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 { const { t } = useTranslation(); const { api } = useApi(); - const { allAccounts } = useAccounts(); - const [{ myAccounts, uniqueCount }, setContributors] = useState({}); - const trigger = useEventTrigger([api.events.crowdloan.Contributed], useCallback( + const { allAccounts, isAccount } = useAccounts(); + const [{ myAccounts, uniqueKeys }, setContributors] = useState(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 ( @@ -140,17 +151,24 @@ function Fund ({ bestNumber, className, isOngoing, leasePeriod, value: { childKe
{percentage}
- {uniqueCount && ( - formatNumber(uniqueCount) + {uniqueKeys.length !== 0 && ( + formatNumber(uniqueKeys.length) )} + {canWithdraw && uniqueKeys.length !== 0 && ( + + )} {canDissolve && ( )} {isOngoing && canContribute && ( - [] { + 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 { + const { t } = useTranslation(); + const { api } = useApi(); + const { hasAccounts } = useAccounts(); + const [isOpen, toggleOpen] = useToggle(); + const [accountId, setAccountId] = useState(null); + const [extrinsics, setExtrinsics] = useState[] | null>(null); + const [batchSize, setBatchSize] = useState(0); + const [withdrawType, setWithdrawType] = useState(0); + + const typeOptions = useMemo( + () => [ + { + text: t('Withdraw for all contributed accounts'), + value: 0 + }, + myAccounts.length && { + text: t('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 ( + <> +