mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-13 21:01:06 +00:00
fix: update extension packages and fix type compatibility for pezkuwi-sdk
- Update @pezkuwi/extension-inject to ^0.62.13 with proper /types exports - Update @pezkuwi/extension-dapp to ^0.62.13 - Update @pezkuwi/extension-compat-metamask to ^0.62.13 - Fix IconTheme type to include 'bizinikiwi' and 'pezkuwi' themes - Fix endpoint array issues (getTeleports -> direct array references) - Add type assertions for external package compatibility (acala, moonbeam, parallel) - Fix subspace.ts dynamic class typing - Fix conviction type in page-referenda - Update Pallet type names to Pezpallet prefix across codebase - Define InjectedExtension types locally for module resolution - Add styled-components DefaultTheme augmentation - Add react-copy-to-clipboard type declaration for React 18 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { AuctionInfo, Campaign, Campaigns, WinnerData, Winning } from '../types.js';
|
||||
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { useLeaseRangeMax } from '../useLeaseRanges.js';
|
||||
import WinRange from './WinRange.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
campaigns: Campaigns;
|
||||
className?: string;
|
||||
winningData?: Winning[];
|
||||
}
|
||||
|
||||
function Auction ({ auctionInfo, campaigns, className, winningData }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const rangeMax = useLeaseRangeMax();
|
||||
const newRaise = useCall<ParaId[]>(api.query.crowdloan.newRaise);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('bids'), 'start', 3],
|
||||
[t('bidder'), 'address'],
|
||||
[t('crowdloan')],
|
||||
[t('leases')],
|
||||
[t('value')]
|
||||
]);
|
||||
|
||||
const loans = useMemo(
|
||||
(): Campaign[] | undefined => {
|
||||
if (newRaise && auctionInfo?.leasePeriod && campaigns.funds) {
|
||||
const leasePeriodStart = auctionInfo.leasePeriod;
|
||||
const leasePeriodEnd = leasePeriodStart.add(rangeMax);
|
||||
|
||||
return campaigns.funds
|
||||
.filter(({ firstSlot, isWinner, lastSlot, paraId }) =>
|
||||
!isWinner &&
|
||||
newRaise.some((n) => n.eq(paraId)) &&
|
||||
firstSlot.gte(leasePeriodStart) &&
|
||||
lastSlot.lte(leasePeriodEnd)
|
||||
)
|
||||
.sort((a, b) => b.value.cmp(a.value));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[auctionInfo, campaigns, newRaise, rangeMax]
|
||||
);
|
||||
|
||||
const interleave = useCallback(
|
||||
(winners: WinnerData[], asIs: boolean): WinnerData[] => {
|
||||
if (asIs || !newRaise || !auctionInfo?.leasePeriod || !loans) {
|
||||
return winners;
|
||||
}
|
||||
|
||||
return winners
|
||||
.concat(...loans.filter(({ firstSlot, lastSlot, paraId, value }) =>
|
||||
!winners.some((w) =>
|
||||
w.firstSlot.eq(firstSlot) &&
|
||||
w.lastSlot.eq(lastSlot)
|
||||
) &&
|
||||
!loans.some((e) =>
|
||||
!paraId.eq(e.paraId) &&
|
||||
firstSlot.eq(e.firstSlot) &&
|
||||
lastSlot.eq(e.lastSlot) &&
|
||||
value.lt(e.value)
|
||||
)
|
||||
))
|
||||
.map((w): WinnerData =>
|
||||
loans.find(({ firstSlot, lastSlot, value }) =>
|
||||
w.firstSlot.eq(firstSlot) &&
|
||||
w.lastSlot.eq(lastSlot) &&
|
||||
w.value.lt(value)
|
||||
) || w
|
||||
)
|
||||
.sort((a, b) =>
|
||||
a.firstSlot.eq(b.firstSlot)
|
||||
? a.lastSlot.cmp(b.lastSlot)
|
||||
: a.firstSlot.cmp(b.firstSlot)
|
||||
);
|
||||
},
|
||||
[auctionInfo, loans, newRaise]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={
|
||||
newRaise && auctionInfo?.numAuctions && winningData && (
|
||||
auctionInfo.endBlock && !winningData.length
|
||||
? t('No winners in this auction')
|
||||
: t('No ongoing auction')
|
||||
)
|
||||
}
|
||||
header={headerRef.current}
|
||||
noBodyTag
|
||||
>
|
||||
{auctionInfo?.leasePeriod && winningData && loans && (
|
||||
winningData.length
|
||||
? winningData.map(({ blockNumber, winners }, round) => (
|
||||
<tbody key={round}>
|
||||
{interleave(winners, round !== 0 || winningData.length !== 1).map((value, index) => (
|
||||
<WinRange
|
||||
auctionInfo={auctionInfo}
|
||||
blockNumber={blockNumber}
|
||||
isFirst={index === 0}
|
||||
isLatest={round === 0}
|
||||
key={`${blockNumber.toString()}:${value.key}`}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
))
|
||||
: (loans.length !== 0) && (
|
||||
<tbody key='latest-crowd'>
|
||||
{interleave([], false).map((value, index) => (
|
||||
<WinRange
|
||||
auctionInfo={auctionInfo}
|
||||
isFirst={index === 0}
|
||||
isLatest
|
||||
key={`latest-crowd:${value.key}`}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
)
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Auction);
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BlockNumber } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { AuctionInfo, OwnedId, OwnerInfo, Winning } from '../types.js';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useBestNumber, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import InputOwner from '../InputOwner.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { useLeaseRanges } from '../useLeaseRanges.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
className?: string;
|
||||
lastWinners?: Winning;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
interface Option {
|
||||
firstSlot: number;
|
||||
lastSlot: number;
|
||||
text: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const EMPTY_OWNER: OwnerInfo = { accountId: null, paraId: 0 };
|
||||
|
||||
function Bid ({ auctionInfo, className, lastWinners, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const bestNumber = useBestNumber();
|
||||
const ranges = useLeaseRanges();
|
||||
const [{ accountId, paraId }, setOwnerInfo] = useState<OwnerInfo>(EMPTY_OWNER);
|
||||
const [amount, setAmount] = useState<BN | undefined>(BN_ZERO);
|
||||
const [range, setRange] = useState(0);
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
|
||||
const rangeOpts = useMemo(
|
||||
(): Option[] => {
|
||||
if (!auctionInfo?.leasePeriod) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const leasePeriod = auctionInfo.leasePeriod.toNumber();
|
||||
|
||||
return ranges.map(([first, last], value): Option => ({
|
||||
firstSlot: leasePeriod + first,
|
||||
lastSlot: leasePeriod + last,
|
||||
text: `${formatNumber(leasePeriod + first)} - ${formatNumber(leasePeriod + last)}`,
|
||||
value
|
||||
}));
|
||||
},
|
||||
[auctionInfo, ranges]
|
||||
);
|
||||
|
||||
const currentWinner = useMemo(
|
||||
() => lastWinners?.winners.find(({ firstSlot, lastSlot }) =>
|
||||
firstSlot.eqn(rangeOpts[range].firstSlot) &&
|
||||
lastSlot.eqn(rangeOpts[range].lastSlot)
|
||||
),
|
||||
[lastWinners, range, rangeOpts]
|
||||
);
|
||||
|
||||
const isAmountLess = !!(amount && currentWinner) && amount.lte(currentWinner.value);
|
||||
const isAmountError = !amount || amount.isZero() || isAmountLess;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!ownedIds.length || !hasAccounts || !auctionInfo?.numAuctions || !auctionInfo.leasePeriod || !auctionInfo.endBlock || !api.consts.auctions || bestNumber?.gte(auctionInfo.endBlock.add(api.consts.auctions.endingPeriod as BlockNumber))}
|
||||
label={t('Bid')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Place bid')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputOwner
|
||||
onChange={setOwnerInfo}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
<Modal.Columns hint={t('The first and last lease period for this bid. The last lease period should be after the first with the maximum determined by the auction config.')}>
|
||||
<Dropdown
|
||||
label={t('bid period range (first lease - last lease)')}
|
||||
onChange={setRange}
|
||||
options={rangeOpts}
|
||||
value={range}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={
|
||||
<>
|
||||
<p>{t('The amount to bid for this teyrchain lease period range.')}</p>
|
||||
<p>{t('The bid should be more than the current range winner to be accepted and influence the auction outcome.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
isError={isAmountError}
|
||||
isZeroable={false}
|
||||
label={t('bid amount')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={currentWinner?.value}
|
||||
isDisabled
|
||||
key={range}
|
||||
label={t('current range winning bid')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!paraId || isAmountError || !auctionInfo?.leasePeriod}
|
||||
label={t('Bid')}
|
||||
onStart={toggleOpen}
|
||||
params={[paraId, auctionInfo?.numAuctions, auctionInfo?.leasePeriod?.addn(ranges[range][0]), auctionInfo?.leasePeriod?.addn(ranges[range][1]), amount]}
|
||||
tx={api.tx.auctions.bid}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Bid);
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
import type { Balance, BlockNumber } from '@pezkuwi/types/interfaces';
|
||||
import type { AuctionInfo, Winning } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useBestNumber, useCall } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_ONE, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
className?: string;
|
||||
lastWinners?: Winning;
|
||||
}
|
||||
|
||||
function Summary ({ auctionInfo, className, lastWinners }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const bestNumber = useBestNumber();
|
||||
const totalIssuance = useCall<Balance>(api.query.balances?.totalIssuance);
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<section>
|
||||
<CardSummary label={t('auctions')}>
|
||||
{auctionInfo
|
||||
? formatNumber(auctionInfo.numAuctions)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('active')}>
|
||||
{auctionInfo
|
||||
? auctionInfo.leasePeriod
|
||||
? t('yes')
|
||||
: t('no')
|
||||
: <span className='--tmp'>{t('no')}</span>
|
||||
}
|
||||
</CardSummary>
|
||||
</section>
|
||||
{auctionInfo && (
|
||||
<>
|
||||
<section>
|
||||
{auctionInfo.leasePeriod && (
|
||||
<CardSummary label={t('first - last')}>
|
||||
{formatNumber(auctionInfo.leasePeriod)} - {formatNumber(auctionInfo.leasePeriod.add(api.consts.auctions.leasePeriodsPerSlot as u32).isub(BN_ONE))}
|
||||
</CardSummary>
|
||||
)}
|
||||
{totalIssuance && lastWinners && (
|
||||
<CardSummary
|
||||
label={t('total')}
|
||||
progress={{
|
||||
hideValue: true,
|
||||
total: totalIssuance,
|
||||
value: lastWinners.total,
|
||||
withTime: true
|
||||
}}
|
||||
>
|
||||
<FormatBalance
|
||||
value={lastWinners.total}
|
||||
withSi
|
||||
/>
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
{auctionInfo?.endBlock && bestNumber && (
|
||||
bestNumber.lt(auctionInfo.endBlock)
|
||||
? (
|
||||
<CardSummary
|
||||
label={t('end period at')}
|
||||
progress={{
|
||||
hideGraph: true,
|
||||
total: auctionInfo.endBlock,
|
||||
value: bestNumber,
|
||||
withTime: true
|
||||
}}
|
||||
>
|
||||
#{formatNumber(auctionInfo.endBlock)}
|
||||
</CardSummary>
|
||||
)
|
||||
: (
|
||||
<CardSummary
|
||||
label={t('ending period')}
|
||||
progress={{
|
||||
total: api.consts.auctions?.endingPeriod as BlockNumber,
|
||||
value: bestNumber.sub(auctionInfo.endBlock),
|
||||
withTime: true
|
||||
}}
|
||||
></CardSummary>
|
||||
)
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { AuctionInfo, WinnerData } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressMini, ParaLink, Table } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo: AuctionInfo;
|
||||
blockNumber?: BN;
|
||||
className?: string;
|
||||
isFirst: boolean;
|
||||
isLatest: boolean;
|
||||
value: WinnerData;
|
||||
}
|
||||
|
||||
function WinRanges ({ auctionInfo, blockNumber, className = '', isFirst, isLatest, value: { accountId, firstSlot, isCrowdloan, lastSlot, paraId, value } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td>
|
||||
{isFirst && (
|
||||
<h1>{isLatest
|
||||
? t('latest')
|
||||
: <>#{formatNumber((!blockNumber || blockNumber.isZero()) ? auctionInfo.endBlock : blockNumber)}</>
|
||||
}</h1>
|
||||
)}
|
||||
</td>
|
||||
<Table.Column.Id value={paraId} />
|
||||
<td className='badge'><ParaLink id={paraId} /></td>
|
||||
<td className='address'><AddressMini value={accountId} /></td>
|
||||
<td className='all number'>{isCrowdloan ? t('Yes') : t('No')}</td>
|
||||
<td className='all number together'>
|
||||
{firstSlot.eq(lastSlot)
|
||||
? formatNumber(firstSlot)
|
||||
: `${formatNumber(firstSlot)} - ${formatNumber(lastSlot)}`
|
||||
}
|
||||
</td>
|
||||
<Table.Column.Balance value={value} />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(WinRanges);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AuctionInfo, Campaigns, OwnedId, Winning } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button, MarkWarning } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Auction from './Auction.js';
|
||||
import Bid from './Bid.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
campaigns: Campaigns;
|
||||
className?: string;
|
||||
ownedIds: OwnedId[];
|
||||
winningData?: Winning[];
|
||||
}
|
||||
|
||||
function Auctions ({ auctionInfo, campaigns, className, ownedIds, winningData }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const lastWinners = winningData?.[0];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<MarkWarning
|
||||
className='warning centered'
|
||||
content={t('Auctions will be deprecated in favor of Coretime. When Coretime is active in Pezkuwi, this page will be removed.')}
|
||||
/>
|
||||
<Summary
|
||||
auctionInfo={auctionInfo}
|
||||
lastWinners={lastWinners}
|
||||
/>
|
||||
<Button.Group>
|
||||
<Bid
|
||||
auctionInfo={auctionInfo}
|
||||
lastWinners={lastWinners}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Auction
|
||||
auctionInfo={auctionInfo}
|
||||
campaigns={campaigns}
|
||||
winningData={winningData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Auctions);
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { MarkWarning, styled } from '@pezkuwi/react-components';
|
||||
import { useStakingAsyncApis } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
const BannerAssetHubMigration = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isRelayChain } = useStakingAsyncApis();
|
||||
|
||||
if (!isRelayChain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledBanner
|
||||
className='warning centered'
|
||||
withIcon={false}
|
||||
>
|
||||
<p>
|
||||
{t('After the Asset Hub migration, crowdloan related activity has been transferred to the `ah_ops` pallet on Asset Hub. You can claim your funds from there, or visit ')}
|
||||
<a
|
||||
href='https://pezkuwi-crowdloan.com/'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{t('the Pezkuwi Crowdloan UI')}
|
||||
</a>
|
||||
{t(' for more details.')}
|
||||
</p>
|
||||
</StyledBanner>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledBanner = styled(MarkWarning)`
|
||||
border: 1px solid #ffc107;
|
||||
background: #ffc10720;
|
||||
font-size: 1rem !important;
|
||||
`;
|
||||
|
||||
export default React.memo(BannerAssetHubMigration);
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { Balance, BalanceOf, BlockNumber, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, InputBalance, MarkWarning, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { formatBalance, isHex } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
cap: Balance;
|
||||
className?: string;
|
||||
needsSignature: boolean;
|
||||
paraId: ParaId;
|
||||
raised: Balance;
|
||||
}
|
||||
|
||||
// 0x, <enum byte>, hex data
|
||||
const VALID_LENGTHS = [
|
||||
2 + 2 + (64 * 2),
|
||||
2 + 2 + (65 * 2)
|
||||
];
|
||||
|
||||
function verifySignature (api: ApiPromise, signature: string | null): boolean {
|
||||
if (isHex(signature) && VALID_LENGTHS.includes(signature.length)) {
|
||||
try {
|
||||
api.createType('MultiSignature', signature);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function Contribute ({ cap, className, needsSignature, 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 [signature, setSignature] = useState<string | null>(null);
|
||||
|
||||
const isSignatureError = useMemo(
|
||||
() => needsSignature && !verifySignature(api, signature),
|
||||
[api, needsSignature, signature]
|
||||
);
|
||||
|
||||
const remaining = cap.sub(raised);
|
||||
const isAmountBelow = !amount || amount.lt(api.consts.crowdloan.minContribution as BalanceOf);
|
||||
const isAmountOver = !!(amount && amount.gt(remaining));
|
||||
const isAmountError = isAmountBelow || isAmountOver;
|
||||
const minContribution = api.consts.crowdloan.minContribution as BlockNumber;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!hasAccounts}
|
||||
label={t('Contribute')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Contribute to fund')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will contribute to the crowdloan.')}>
|
||||
<InputAddress
|
||||
label={t('contribute from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount to contribute from this account.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={api.consts.crowdloan.minContribution as BalanceOf}
|
||||
isError={isAmountError}
|
||||
isZeroable={false}
|
||||
label={t('contribution')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
{isAmountBelow && (
|
||||
<MarkWarning content={t('The amount is less than the minimum allowed contribution of {{value}}', { replace: { value: formatBalance(minContribution) } })} />
|
||||
)}
|
||||
{isAmountOver && (
|
||||
<MarkWarning content={t('The amount is more than the remaining contribution needed {{value}}', { replace: { value: formatBalance(remaining) } })} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
{needsSignature && (
|
||||
<Modal.Columns hint={t('The verifier signature that is to be associated with this contribution.')}>
|
||||
<Input
|
||||
isError={isSignatureError}
|
||||
label={t('verifier signature')}
|
||||
onChange={setSignature}
|
||||
placeholder={t('0x...')}
|
||||
/>
|
||||
{isSignatureError && (
|
||||
<MarkWarning content={t('The hex-encoded verifier signature should be provided to you by the team running the crowdloan (based on the information you provide).')} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
)}
|
||||
<Modal.Columns hint={t('The above contribution should more than minimum contribution amount and less than the remaining value.')}>
|
||||
<InputBalance
|
||||
defaultValue={api.consts.crowdloan.minContribution as BalanceOf}
|
||||
isDisabled
|
||||
label={t('minimum allowed')}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={remaining}
|
||||
isDisabled
|
||||
label={t('remaining till cap')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={isAmountError || isSignatureError}
|
||||
label={t('Contribute')}
|
||||
onStart={toggleOpen}
|
||||
params={[paraId, amount, signature]}
|
||||
tx={api.tx.crowdloan.contribute}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Contribute);
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { Campaign, LeasePeriod } from '../types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AddressMini, Button, Expander, Icon, InputAddress, Modal, ParaLink, Table, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useParaEndpoints, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { CallExpander } from '@pezkuwi/react-params';
|
||||
import { Available, BlockToTime, FormatBalance } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Contribute from './Contribute.js';
|
||||
import Refund from './Refund.js';
|
||||
import useContributions from './useContributions.js';
|
||||
|
||||
interface Props {
|
||||
bestHash?: string;
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
isOngoing?: boolean;
|
||||
leasePeriod?: LeasePeriod;
|
||||
value: Campaign;
|
||||
}
|
||||
|
||||
interface LastChange {
|
||||
prevHash: string;
|
||||
prevLength: number;
|
||||
}
|
||||
|
||||
function Fund ({ bestHash, bestNumber, className = '', isOngoing, leasePeriod, value: { info: { cap, depositor, end, firstPeriod, lastPeriod, raised, verifier }, isCapped, isEnded, isWinner, paraId } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { isAccount } = useAccounts();
|
||||
const endpoints = useParaEndpoints(paraId);
|
||||
const { blockHash, contributorsHex, hasLoaded, myAccounts, myAccountsHex, myContributions } = useContributions(paraId);
|
||||
const [lastChange, setLastChange] = useState<LastChange>(() => ({ prevHash: '', prevLength: 0 }));
|
||||
|
||||
const isDepositor = useMemo(
|
||||
() => isAccount(depositor.toString()),
|
||||
[depositor, isAccount]
|
||||
);
|
||||
|
||||
const blocksLeft = useMemo(
|
||||
() => bestNumber && end.gt(bestNumber)
|
||||
? end.sub(bestNumber)
|
||||
: null,
|
||||
[bestNumber, end]
|
||||
);
|
||||
|
||||
const percentage = useMemo(
|
||||
() => cap.isZero()
|
||||
? '100.00%'
|
||||
: `${(raised.muln(10000).div(cap).toNumber() / 100).toFixed(2)}%`,
|
||||
[cap, raised]
|
||||
);
|
||||
|
||||
const hasEnded = !blocksLeft && !!leasePeriod && (
|
||||
isWinner
|
||||
? leasePeriod.currentPeriod.gt(lastPeriod)
|
||||
: leasePeriod.currentPeriod.gt(firstPeriod)
|
||||
);
|
||||
const canContribute = isOngoing && !isCapped && !isWinner && !!blocksLeft;
|
||||
const canDissolve = raised.isZero();
|
||||
const canWithdraw = !raised.isZero() && hasEnded;
|
||||
const homepage = endpoints.length !== 0 && endpoints[0].homepage;
|
||||
|
||||
useEffect((): void => {
|
||||
setLastChange((prev): LastChange => {
|
||||
const prevLength = contributorsHex.length;
|
||||
|
||||
return prev.prevLength !== prevLength
|
||||
? { prevHash: blockHash, prevLength }
|
||||
: prev;
|
||||
});
|
||||
}, [contributorsHex, blockHash]);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<Table.Column.Id value={paraId} />
|
||||
<td className='badge'><ParaLink id={paraId} /></td>
|
||||
<td className='media--800'>
|
||||
{isWinner
|
||||
? t('Winner')
|
||||
: blocksLeft
|
||||
? isCapped
|
||||
? t('Capped')
|
||||
: isOngoing
|
||||
? t('Active')
|
||||
: t('Past')
|
||||
: t('Ended')
|
||||
}
|
||||
</td>
|
||||
<td className='address media--2000'><AddressMini value={depositor} /></td>
|
||||
<td className='all number together media--1200'>
|
||||
{blocksLeft && (
|
||||
<BlockToTime value={blocksLeft} />
|
||||
)}
|
||||
#{formatNumber(end)}
|
||||
</td>
|
||||
<td className='number all together'>
|
||||
{firstPeriod.eq(lastPeriod)
|
||||
? formatNumber(firstPeriod)
|
||||
: `${formatNumber(firstPeriod)} - ${formatNumber(lastPeriod)}`
|
||||
}
|
||||
</td>
|
||||
<td className='number together'>
|
||||
<FormatBalance
|
||||
value={raised}
|
||||
withCurrency={false}
|
||||
/> / <FormatBalance value={cap} />
|
||||
<div>{percentage}</div>
|
||||
{myAccounts.length !== 0 && (
|
||||
<Expander
|
||||
summary={t('My contributions ({{count}})', { replace: { count: myAccounts.length } })}
|
||||
withBreaks
|
||||
>
|
||||
{myAccounts.map((a, index) => (
|
||||
<AddressMini
|
||||
balance={myContributions[myAccountsHex[index]]}
|
||||
key={a}
|
||||
value={a}
|
||||
withBalance
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
)}
|
||||
</td>
|
||||
<td className='number together media--1100'>
|
||||
{hasLoaded
|
||||
? (
|
||||
<>
|
||||
{bestHash && (
|
||||
<Icon
|
||||
color={
|
||||
lastChange.prevHash === bestHash
|
||||
? 'green'
|
||||
: 'transparent'
|
||||
}
|
||||
icon='chevron-up'
|
||||
isPadded
|
||||
/>
|
||||
)}
|
||||
{contributorsHex.length !== 0 && (
|
||||
formatNumber(contributorsHex.length)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: <span className='--tmp'>999</span>
|
||||
}
|
||||
</td>
|
||||
<td className='button media--1000'>
|
||||
{canWithdraw && contributorsHex.length !== 0 && (
|
||||
<Refund paraId={paraId} />
|
||||
)}
|
||||
{canDissolve && (
|
||||
<DissolveCrowdloan
|
||||
hasEnded={hasEnded}
|
||||
isDepositor={isDepositor}
|
||||
isEnded={isEnded}
|
||||
paraId={paraId}
|
||||
/>
|
||||
)}
|
||||
{isOngoing && canContribute && (
|
||||
<Contribute
|
||||
cap={cap}
|
||||
needsSignature={verifier.isSome}
|
||||
paraId={paraId}
|
||||
raised={raised}
|
||||
/>
|
||||
)}
|
||||
{isOngoing && homepage && (
|
||||
<div>
|
||||
<a
|
||||
href={homepage}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>{t('Homepage')}</a>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
interface IDissolveCrowdloan{
|
||||
isEnded?: boolean;
|
||||
paraId: ParaId;
|
||||
isDepositor: boolean;
|
||||
hasEnded: boolean;
|
||||
}
|
||||
|
||||
function DissolveCrowdloan ({ hasEnded, isDepositor, isEnded, paraId }: IDissolveCrowdloan) {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isDissolveOpen, toggleDissolveOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
|
||||
const extrinsic = useMemo(() => api.tx.crowdloan.dissolve(paraId), [api.tx.crowdloan, paraId]);
|
||||
|
||||
return <>
|
||||
{isDissolveOpen && (
|
||||
<Modal
|
||||
header={t('dissolve crowdloan')}
|
||||
onClose={toggleDissolveOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will pay the fees for the dissolving crowdloan')}>
|
||||
<InputAddress
|
||||
label={t('send from account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={<span className='label'>{t('transferable')}</span>}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<CallExpander
|
||||
isExpanded
|
||||
isHeader
|
||||
value={extrinsic}
|
||||
withHash
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='check'
|
||||
isDisabled={!(isDepositor || hasEnded)}
|
||||
label={t('Submit')}
|
||||
onStart={toggleDissolveOpen}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='times'
|
||||
isDisabled={!(isDepositor || hasEnded)}
|
||||
label={
|
||||
isEnded
|
||||
? t('Close')
|
||||
: t('Cancel')
|
||||
}
|
||||
onClick={toggleDissolveOpen}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default React.memo(Fund);
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { AuctionInfo, LeasePeriod, OwnedId, OwnerInfo } from '../types.js';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputBalance, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import InputOwner from '../InputOwner.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { useLeaseRanges } from '../useLeaseRanges.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
leasePeriod?: LeasePeriod;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
const EMPTY_OWNER: OwnerInfo = { accountId: null, paraId: 0 };
|
||||
|
||||
function FundAdd ({ auctionInfo, bestNumber, className, leasePeriod, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const ranges = useLeaseRanges();
|
||||
const [{ accountId, paraId }, setOwnerInfo] = useState<OwnerInfo>(EMPTY_OWNER);
|
||||
const [cap, setCap] = useState<BN | undefined>();
|
||||
const [endBlock, setEndBlock] = useState<BN | undefined>();
|
||||
const [firstSlot, setFirstSlot] = useState<BN | undefined>();
|
||||
const [lastSlot, setLastSlot] = useState<BN | undefined>();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
|
||||
const maxPeriods = ranges[ranges.length - 1][1] - ranges[0][0];
|
||||
const isEndError = !bestNumber || !endBlock || endBlock.lt(bestNumber);
|
||||
const isFirstError = !firstSlot || (!!leasePeriod && firstSlot.lt(leasePeriod.currentPeriod));
|
||||
const isLastError = !lastSlot || !firstSlot || lastSlot.lt(firstSlot) || lastSlot.gt(firstSlot.addn(maxPeriods));
|
||||
const defaultSlot = (auctionInfo?.leasePeriod || leasePeriod?.currentPeriod.add(BN_ONE) || 1).toString();
|
||||
|
||||
// TODO Add verifier
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!ownedIds.length}
|
||||
label={t('Add fund')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Add campaign')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputOwner
|
||||
onChange={setOwnerInfo}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
<Modal.Columns hint={t('The amount to be raised in this funding campaign.')}>
|
||||
<InputBalance
|
||||
isZeroable={false}
|
||||
label={t('crowdfund cap')}
|
||||
onChange={setCap}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The end block for contributions to this fund.')}>
|
||||
<InputNumber
|
||||
isError={isEndError}
|
||||
label={t('ending block')}
|
||||
onChange={setEndBlock}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
hint={
|
||||
<>
|
||||
<p>{t('The first and last lease periods for this funding campaign.')}</p>
|
||||
<p>{t('The ending lease period should be after the first and a maximum of {{maxPeriods}} periods more than the first', { replace: { maxPeriods } })}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
defaultValue={defaultSlot}
|
||||
isError={isFirstError}
|
||||
label={t('first period')}
|
||||
onChange={setFirstSlot}
|
||||
/>
|
||||
<InputNumber
|
||||
defaultValue={defaultSlot}
|
||||
isError={isLastError}
|
||||
label={t('last period')}
|
||||
onChange={setLastSlot}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!paraId || !cap?.gt(BN_ZERO) || isEndError || isFirstError || isLastError}
|
||||
label={t('Add')}
|
||||
onStart={toggleOpen}
|
||||
params={[paraId, cap, firstSlot, lastSlot, endBlock, null]}
|
||||
tx={api.tx.crowdloan.create}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(FundAdd);
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { Campaign, LeasePeriod } from '../types.js';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
||||
import { MarkWarning, Table } from '@pezkuwi/react-components';
|
||||
import { useBestHash, useIsParasLinked } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Fund from './Fund.js';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
leasePeriod?: LeasePeriod;
|
||||
value: Campaign[] | null;
|
||||
}
|
||||
|
||||
function extractLists (value: Campaign[] | null, leasePeriod?: LeasePeriod): [Campaign[] | null, Campaign[] | null, ParaId[] | null] {
|
||||
const currentPeriod = leasePeriod?.currentPeriod;
|
||||
let active: Campaign[] | null = null;
|
||||
let ended: Campaign[] | null = null;
|
||||
let allIds: ParaId[] | null = null;
|
||||
|
||||
if (value && currentPeriod) {
|
||||
active = value.filter(({ firstSlot, isCapped, isEnded, isWinner }) =>
|
||||
!(isCapped || isEnded || isWinner) &&
|
||||
currentPeriod.lte(firstSlot)
|
||||
);
|
||||
ended = value.filter(({ firstSlot, isCapped, isEnded, isWinner }) =>
|
||||
(isCapped || isEnded || isWinner) ||
|
||||
currentPeriod.gt(firstSlot)
|
||||
);
|
||||
allIds = value.map(({ paraId }) => paraId);
|
||||
}
|
||||
|
||||
return [active, ended, allIds];
|
||||
}
|
||||
|
||||
function sortList (hasLinksMap: Record<string, boolean>, list?: Campaign[] | null): Campaign[] | null | undefined {
|
||||
return list?.sort(({ key: a }, { key: b }): number => {
|
||||
const aKnown = hasLinksMap[a] || false;
|
||||
const bKnown = hasLinksMap[b] || false;
|
||||
|
||||
return aKnown === bKnown
|
||||
? 0
|
||||
: aKnown
|
||||
? -1
|
||||
: 1;
|
||||
});
|
||||
}
|
||||
|
||||
function Funds ({ bestNumber, className, leasePeriod, value }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const bestHash = useBestHash();
|
||||
const [active, ended, allIds] = useMemo(
|
||||
() => extractLists(value, leasePeriod),
|
||||
[leasePeriod, value]
|
||||
);
|
||||
const hasLinksMap = useIsParasLinked(allIds);
|
||||
const [activeSorted, endedSorted] = useMemo(
|
||||
() => [sortList(hasLinksMap, active), sortList(hasLinksMap, ended)],
|
||||
[active, ended, hasLinksMap]
|
||||
);
|
||||
|
||||
const headerActiveRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('ongoing'), 'start', 2],
|
||||
[undefined, 'media--800'],
|
||||
[undefined, 'media--2000'],
|
||||
[t('ending'), 'media--1200'],
|
||||
[t('leases')],
|
||||
[t('raised')],
|
||||
[t('count'), 'media--1100'],
|
||||
[undefined, 'media--1000']
|
||||
]);
|
||||
|
||||
const headedEndedRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('completed'), 'start', 2],
|
||||
[undefined, 'media--800'],
|
||||
[undefined, 'media--2000'],
|
||||
[t('ending'), 'media--1200'],
|
||||
[t('leases')],
|
||||
[t('raised')],
|
||||
[t('count'), 'media--1100'],
|
||||
[undefined, 'media--1000']
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MarkWarning
|
||||
className='warning centered'
|
||||
content={t('Do not transfer any funds directly to a specific account that is associated with a loan or a team. Use the "Contribute" action to record the contribution on-chain using the crowdloan runtime module. When the fund is dissolved, after either the teyrchain lease expires or the loan ending without winning, the full value will be returned to your account by the runtime. Funds sent directly to an account, without using the crowdloan functionality, may not be returned by the receiving account.')}
|
||||
/>
|
||||
<Table
|
||||
className={className}
|
||||
empty={value && activeSorted && t('No active campaigns found')}
|
||||
header={headerActiveRef.current}
|
||||
>
|
||||
{activeSorted?.map((fund) => (
|
||||
<Fund
|
||||
bestHash={bestHash}
|
||||
bestNumber={bestNumber}
|
||||
isOngoing
|
||||
key={fund.accountId}
|
||||
value={fund}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
<Table
|
||||
className={className}
|
||||
empty={value && endedSorted && t('No completed campaigns found')}
|
||||
header={headedEndedRef.current}
|
||||
>
|
||||
{endedSorted?.map((fund) => (
|
||||
<Fund
|
||||
bestNumber={bestNumber}
|
||||
key={fund.accountId}
|
||||
leasePeriod={leasePeriod}
|
||||
value={fund}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Funds);
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
paraId: ParaId;
|
||||
}
|
||||
|
||||
function Refund ({ className, 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);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='minus'
|
||||
isDisabled={!hasAccounts}
|
||||
label={t('Refund')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Withdraw from fund')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will be used to send the transaction.')}>
|
||||
<InputAddress
|
||||
label={t('requesting from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='credit-card'
|
||||
label={t('Refund')}
|
||||
onStart={toggleOpen}
|
||||
params={[paraId]}
|
||||
tx={api.tx.crowdloan.refund}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Refund);
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_THREE, BN_TWO, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
activeCap: BN;
|
||||
activeRaised: BN;
|
||||
className?: string;
|
||||
fundCount?: number;
|
||||
isLoading?: boolean;
|
||||
totalCap: BN;
|
||||
totalRaised: BN;
|
||||
}
|
||||
|
||||
function Summary ({ activeCap, activeRaised, className, fundCount, isLoading, totalCap, totalRaised }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('funds')}>
|
||||
{fundCount === undefined
|
||||
? <span className='--tmp'>99</span>
|
||||
: formatNumber(fundCount)}
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
label={`${t('active raised / cap')}`}
|
||||
progress={{
|
||||
hideValue: true,
|
||||
isBlurred: isLoading,
|
||||
total: isLoading ? BN_THREE : activeCap,
|
||||
value: isLoading ? BN_TWO : activeRaised
|
||||
}}
|
||||
>
|
||||
<span className={isLoading ? '--tmp' : ''}>
|
||||
<FormatBalance
|
||||
value={activeRaised}
|
||||
withCurrency={false}
|
||||
withSi
|
||||
/>
|
||||
/
|
||||
<FormatBalance
|
||||
value={activeCap}
|
||||
withSi
|
||||
/>
|
||||
</span>
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
label={`${t('total raised / cap')}`}
|
||||
progress={{
|
||||
hideValue: true,
|
||||
isBlurred: isLoading,
|
||||
total: isLoading ? BN_THREE : totalCap,
|
||||
value: isLoading ? BN_TWO : totalRaised
|
||||
}}
|
||||
>
|
||||
<span className={isLoading ? '--tmp' : ''}>
|
||||
<FormatBalance
|
||||
value={totalRaised}
|
||||
withCurrency={false}
|
||||
withSi
|
||||
/>
|
||||
/
|
||||
<FormatBalance
|
||||
value={totalCap}
|
||||
withSi
|
||||
/>
|
||||
</span>
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AuctionInfo, Campaigns, LeasePeriod, OwnedId } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button, MarkWarning } from '@pezkuwi/react-components';
|
||||
import { useBestNumber } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import BannerAssetHubMigration from './BannerAssetHubMigration.js';
|
||||
import FundAdd from './FundAdd.js';
|
||||
import Funds from './Funds.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
auctionInfo?: AuctionInfo;
|
||||
campaigns: Campaigns;
|
||||
className?: string;
|
||||
leasePeriod?: LeasePeriod;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
function Crowdloan ({ auctionInfo, campaigns: { activeCap, activeRaised, funds, isLoading, totalCap, totalRaised }, className, leasePeriod, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<BannerAssetHubMigration />
|
||||
<MarkWarning
|
||||
className='warning centered'
|
||||
content={t('Crowdloans will be deprecated in favor of Coretime. When Coretime is active in Pezkuwi, this page will be removed.')}
|
||||
/>
|
||||
<Summary
|
||||
activeCap={activeCap}
|
||||
activeRaised={activeRaised}
|
||||
fundCount={funds?.length}
|
||||
isLoading={isLoading}
|
||||
totalCap={totalCap}
|
||||
totalRaised={totalRaised}
|
||||
/>
|
||||
<Button.Group>
|
||||
<FundAdd
|
||||
auctionInfo={auctionInfo}
|
||||
bestNumber={bestNumber}
|
||||
leasePeriod={leasePeriod}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Funds
|
||||
bestNumber={bestNumber}
|
||||
leasePeriod={leasePeriod}
|
||||
value={funds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Crowdloan);
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveContributions, DeriveOwnContributions } from '@pezkuwi/api-derive/types';
|
||||
import type { Balance, ParaId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { encodeAddress } from '@pezkuwi/util-crypto';
|
||||
|
||||
interface Result extends DeriveContributions {
|
||||
hasLoaded: boolean;
|
||||
myAccounts: string[];
|
||||
myAccountsHex: string[];
|
||||
myContributions: Record<string, Balance>;
|
||||
}
|
||||
|
||||
const NO_CONTRIB: Result = {
|
||||
blockHash: '-',
|
||||
contributorsHex: [],
|
||||
hasLoaded: false,
|
||||
myAccounts: [],
|
||||
myAccountsHex: [],
|
||||
myContributions: {}
|
||||
};
|
||||
|
||||
function useContributionsImpl (paraId: ParaId): Result {
|
||||
const { api } = useApi();
|
||||
const { allAccountsHex } = useAccounts();
|
||||
const [state, setState] = useState<Result>(() => NO_CONTRIB);
|
||||
const derive = useCall<DeriveContributions>(api.derive.crowdloan.contributions, [paraId]);
|
||||
const myContributions = useCall<DeriveOwnContributions>(api.derive.crowdloan.ownContributions, [paraId, state.myAccountsHex]);
|
||||
|
||||
useEffect((): void => {
|
||||
derive && setState((prev): Result => {
|
||||
let myAccountsHex = derive.contributorsHex.filter((h) => allAccountsHex.includes(h));
|
||||
let myAccounts: string[];
|
||||
|
||||
if (myAccountsHex.length === prev.myAccountsHex.length) {
|
||||
myAccountsHex = prev.myAccountsHex;
|
||||
myAccounts = prev.myAccounts;
|
||||
} else {
|
||||
myAccounts = myAccountsHex.map((a) => encodeAddress(a, api.registry.chainSS58));
|
||||
}
|
||||
|
||||
return { ...prev, ...derive, hasLoaded: true, myAccounts, myAccountsHex };
|
||||
});
|
||||
}, [api, allAccountsHex, derive]);
|
||||
|
||||
useEffect((): void => {
|
||||
myContributions && setState((prev) => ({
|
||||
...prev,
|
||||
myContributions
|
||||
}));
|
||||
}, [myContributions]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useContributions', useContributionsImpl);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { OwnedId, OwnerInfo } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Dropdown, InputAddress, MarkError, Modal } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
noCodeCheck?: boolean;
|
||||
onChange: (owner: OwnerInfo) => void;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
function InputOwner ({ noCodeCheck, onChange, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [paraId, setParaId] = useState<number>(0);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(
|
||||
accountId && paraId
|
||||
? { accountId, paraId }
|
||||
: { accountId: null, paraId: 0 }
|
||||
);
|
||||
}, [accountId, onChange, ownedIds, paraId]);
|
||||
|
||||
const owners = useMemo(
|
||||
() => ownedIds.map(({ manager }) => manager),
|
||||
[ownedIds]
|
||||
);
|
||||
|
||||
const optIds = useMemo(
|
||||
() => ownedIds
|
||||
.filter(({ manager }) => manager === accountId)
|
||||
.map(({ paraId }) => ({ text: paraId.toString(), value: paraId.toNumber() })),
|
||||
[accountId, ownedIds]
|
||||
);
|
||||
|
||||
const _setParaId = useCallback(
|
||||
(id: number) => setParaId(
|
||||
noCodeCheck || ownedIds.some(({ hasCode, paraId }) => paraId.eq(id) && hasCode)
|
||||
? id
|
||||
: 0
|
||||
),
|
||||
[noCodeCheck, ownedIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal.Columns hint={
|
||||
<>
|
||||
<p>{t('This account that has been used to register the teyrchain. This will pay all associated fees.')}</p>
|
||||
<p>{t('The teyrchain id is associated with the selected account via parathread registration.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputAddress
|
||||
filter={owners}
|
||||
label={t('teyrchain owner')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
{accountId && (
|
||||
<Dropdown
|
||||
defaultValue={optIds[0].value}
|
||||
key={accountId}
|
||||
label={t('teyrchain id')}
|
||||
onChange={_setParaId}
|
||||
options={optIds}
|
||||
/>
|
||||
)}
|
||||
{!noCodeCheck && !paraId && (
|
||||
<MarkError content={t('Before using this registered paraId, you need to have a WASM validation function registered on-chain')} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(InputOwner);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { LeasePeriod } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { BlockToTime } from '@pezkuwi/react-query';
|
||||
import { BN_ONE, bnToBn } from '@pezkuwi/util';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
leasePeriod?: LeasePeriod | null;
|
||||
value?: number | BN | null;
|
||||
}
|
||||
|
||||
function calcBlocks (leasePeriod?: LeasePeriod | null, value?: number | BN | null): BN | null | undefined | 0 {
|
||||
return leasePeriod && value &&
|
||||
bnToBn(value)
|
||||
.sub(BN_ONE)
|
||||
.imul(leasePeriod.length)
|
||||
.iadd(leasePeriod.remainder);
|
||||
}
|
||||
|
||||
function LeaseBlocks ({ children, className, leasePeriod, value }: Props): React.ReactElement<Props> | null {
|
||||
const blocks = useMemo(
|
||||
() => calcBlocks(leasePeriod, value),
|
||||
[leasePeriod, value]
|
||||
);
|
||||
|
||||
if (!blocks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockToTime
|
||||
className={className}
|
||||
value={blocks}
|
||||
>
|
||||
{children}
|
||||
</BlockToTime>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(LeaseBlocks);
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PezkuwiRuntimeTeyrchainsParasParaLifecycle } from '@pezkuwi/types/lookup';
|
||||
import type { QueuedAction } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { SessionToTime } from '@pezkuwi/react-query';
|
||||
|
||||
interface Props {
|
||||
lifecycle: PezkuwiRuntimeTeyrchainsParasParaLifecycle | null;
|
||||
nextAction?: QueuedAction;
|
||||
}
|
||||
|
||||
function Lifecycle ({ lifecycle, nextAction }: Props): React.ReactElement<Props> | null {
|
||||
return lifecycle && (
|
||||
<>
|
||||
{lifecycle.toString()}
|
||||
{nextAction && (
|
||||
<SessionToTime value={nextAction.sessionIndex} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Lifecycle);
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { LeasePeriod } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { BN_ONE, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import LeaseBlocks from './LeaseBlocks.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
fromFirst?: boolean;
|
||||
leasePeriod?: LeasePeriod;
|
||||
periods: number[];
|
||||
}
|
||||
|
||||
function getMapped (periods: number[], currentPeriod?: BN): string | undefined {
|
||||
return currentPeriod &&
|
||||
periods
|
||||
?.reduce((all: [BN, BN][], period): [BN, BN][] => {
|
||||
const bnp = currentPeriod.addn(period);
|
||||
|
||||
if (!all.length || all[all.length - 1][1].add(BN_ONE).lt(bnp)) {
|
||||
all.push([bnp, bnp]);
|
||||
} else {
|
||||
all[all.length - 1][1] = bnp;
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.map(([a, b]) =>
|
||||
a.eq(b)
|
||||
? formatNumber(a)
|
||||
: `${formatNumber(a)} - ${formatNumber(b)}`
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function Periods ({ className, fromFirst, leasePeriod, periods }: Props): React.ReactElement<Props> {
|
||||
const mapped = useMemo(
|
||||
() => getMapped(periods, leasePeriod?.currentPeriod),
|
||||
[leasePeriod?.currentPeriod, periods]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>{mapped}</div>
|
||||
{periods && (
|
||||
<LeaseBlocks
|
||||
leasePeriod={leasePeriod}
|
||||
value={
|
||||
fromFirst
|
||||
? periods[0]
|
||||
: periods[periods.length - 1] + 1
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Periods);
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { LeasePeriod } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SummarySession from '@pezkuwi/app-explorer/SummarySession';
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { BestFinalized } from '@pezkuwi/react-query';
|
||||
import { BN_THREE, BN_TWO, formatNumber, isNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
leasePeriod?: LeasePeriod;
|
||||
teyrchainCount?: number;
|
||||
proposalCount?: number;
|
||||
upcomingCount?: number;
|
||||
}
|
||||
|
||||
function Summary ({ leasePeriod, proposalCount, teyrchainCount, upcomingCount }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
<CardSummary label={t('teyrchains')}>
|
||||
{isNumber(teyrchainCount)
|
||||
? formatNumber(teyrchainCount)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
className='media--1000'
|
||||
label={t('parathreads')}
|
||||
>
|
||||
{isNumber(upcomingCount)
|
||||
? formatNumber(upcomingCount)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
{isNumber(proposalCount) && (
|
||||
<CardSummary
|
||||
className='media--1000'
|
||||
label={t('proposals')}
|
||||
>
|
||||
{formatNumber(proposalCount)}
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<CardSummary label={t('current lease')}>
|
||||
{leasePeriod
|
||||
? formatNumber(leasePeriod.currentPeriod)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
className='media--1200'
|
||||
label={t('lease period')}
|
||||
progress={{
|
||||
isBlurred: !leasePeriod,
|
||||
total: leasePeriod ? leasePeriod.length : BN_THREE,
|
||||
value: leasePeriod ? leasePeriod.progress : BN_TWO,
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<CardSummary label={t('finalized')}>
|
||||
<BestFinalized />
|
||||
</CardSummary>
|
||||
<SummarySession
|
||||
className='media--1200'
|
||||
withEra={false}
|
||||
/>
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Summary;
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, GroupIndex, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { LeasePeriod, QueuedAction } from '../types.js';
|
||||
import type { EventMapInfo, ValidatorInfo } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AddressMini, Badge, Expander, ParaLink, styled, Table } from '@pezkuwi/react-components';
|
||||
import { BlockToTime } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Lifecycle from './Lifecycle.js';
|
||||
import Periods from './Periods.js';
|
||||
import TeyrchainInfo from './TeyrchainInfo.js';
|
||||
import useParaInfo from './useParaInfo.js';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
id: ParaId;
|
||||
isScheduled?: boolean;
|
||||
lastBacked?: EventMapInfo;
|
||||
lastInclusion?: EventMapInfo;
|
||||
lastTimeout?: EventMapInfo;
|
||||
leasePeriod?: LeasePeriod;
|
||||
nextAction?: QueuedAction;
|
||||
sessionValidators?: AccountId[] | null;
|
||||
validators?: [GroupIndex, ValidatorInfo[]];
|
||||
}
|
||||
|
||||
function renderAddresses (list?: AccountId[], indices?: BN[]): React.ReactElement<unknown>[] | undefined {
|
||||
return list?.map((id, index) => (
|
||||
<AddressMini
|
||||
key={id.toString()}
|
||||
nameExtra={indices && <> {`(${formatNumber(indices[index])})`}</>}
|
||||
value={id}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function Teyrchain ({ bestNumber, className = '', id, lastBacked, lastInclusion, lastTimeout, leasePeriod, nextAction, sessionValidators, validators }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const paraInfo = useParaInfo(id);
|
||||
const [nonBacked, setNonBacked] = useState<AccountId[]>([]);
|
||||
|
||||
const blockDelay = useMemo(
|
||||
() => bestNumber && (
|
||||
lastInclusion
|
||||
? bestNumber.sub(lastInclusion.blockNumber)
|
||||
: paraInfo.watermark
|
||||
? bestNumber.sub(paraInfo.watermark)
|
||||
: undefined
|
||||
),
|
||||
[bestNumber, lastInclusion, paraInfo]
|
||||
);
|
||||
|
||||
const valRender = useCallback(
|
||||
() => renderAddresses(
|
||||
validators?.[1].map(({ validatorId }) => validatorId),
|
||||
validators?.[1].map(({ indexValidator }) => indexValidator)
|
||||
),
|
||||
[validators]
|
||||
);
|
||||
|
||||
const bckRender = useCallback(
|
||||
() => renderAddresses(nonBacked),
|
||||
[nonBacked]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
if (sessionValidators) {
|
||||
if (paraInfo.pendingAvail) {
|
||||
const list = paraInfo.pendingAvail.availabilityVotes.toHuman()
|
||||
.slice(2)
|
||||
.replace(/_/g, '')
|
||||
.split('')
|
||||
.map((c, index) => c === '0' ? sessionValidators[index] : null)
|
||||
.filter((v, index): v is AccountId => !!v && index < sessionValidators.length);
|
||||
|
||||
list.length !== sessionValidators.length && setNonBacked(list);
|
||||
} else {
|
||||
setNonBacked([]);
|
||||
}
|
||||
}
|
||||
}, [paraInfo, sessionValidators]);
|
||||
|
||||
return (
|
||||
<StyledTr className={`${className} ${(lastBacked || lastInclusion || paraInfo.watermark) ? '' : 'isDisabled'}`}>
|
||||
<Table.Column.Id value={id} />
|
||||
<td className='badge together'>
|
||||
{paraInfo.paraInfo?.locked?.isSome && paraInfo.paraInfo?.locked?.unwrap().isFalse
|
||||
? (
|
||||
<Badge
|
||||
color='orange'
|
||||
hover={t('The teyrchain can be modified, replaced, or removed - it\'s neither protected nor in a transitional state')}
|
||||
icon='unlock'
|
||||
/>
|
||||
)
|
||||
: <Badge color='transparent' />
|
||||
}
|
||||
<ParaLink id={id} />
|
||||
</td>
|
||||
<td className='number media--1400'>
|
||||
<Expander
|
||||
className={validators ? '' : '--tmp'}
|
||||
renderChildren={valRender}
|
||||
summary={t('Val. Group {{group}} ({{count}})', {
|
||||
replace: {
|
||||
count: formatNumber(validators?.[1]?.length || 0),
|
||||
group: validators ? validators[0] : 0
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Expander
|
||||
renderChildren={bckRender}
|
||||
summary={t('Non-voters ({{count}})', { replace: { count: formatNumber(nonBacked.length) } })}
|
||||
/>
|
||||
</td>
|
||||
<td className='start together hash media--1500'>
|
||||
<div className='shortHash'>{paraInfo.headHex}</div>
|
||||
</td>
|
||||
<td className='start'>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{paraInfo.updateAt && bestNumber && (paraInfo.lifecycle as any)?.isTeyrchain
|
||||
? (
|
||||
<>
|
||||
{t('Upgrading')}
|
||||
<BlockToTime value={paraInfo.updateAt.sub(bestNumber)} />
|
||||
#{formatNumber(paraInfo.updateAt)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<Lifecycle
|
||||
lifecycle={paraInfo.lifecycle}
|
||||
nextAction={nextAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
<td className='all' />
|
||||
<td className='number'>{blockDelay && <BlockToTime value={blockDelay} />}</td>
|
||||
<td className='number no-pad-left'>
|
||||
{lastInclusion
|
||||
? <a href={`#/explorer/query/${lastInclusion.blockHash}`}>{formatNumber(lastInclusion.blockNumber)}</a>
|
||||
: paraInfo.watermark && formatNumber(paraInfo.watermark)
|
||||
}
|
||||
</td>
|
||||
<td className='number no-pad-left media--900'>
|
||||
{lastBacked &&
|
||||
<a href={`#/explorer/query/${lastBacked.blockHash}`}>{formatNumber(lastBacked.blockNumber)}</a>
|
||||
}
|
||||
</td>
|
||||
<td className='number no-pad-left media--1600'>
|
||||
{lastTimeout &&
|
||||
<a href={`#/explorer/query/${lastTimeout.blockHash}`}>{formatNumber(lastTimeout.blockNumber)}</a>
|
||||
}
|
||||
</td>
|
||||
<td className='number no-pad-left'>
|
||||
<TeyrchainInfo id={id} />
|
||||
</td>
|
||||
<td className='number media--1700'>
|
||||
{formatNumber(paraInfo.qHrmpI)}
|
||||
</td>
|
||||
<td className='number no-pad-left media--1700'>
|
||||
{formatNumber(paraInfo.qHrmpE)}
|
||||
</td>
|
||||
<td className='number together media--1100'>
|
||||
<Periods
|
||||
leasePeriod={leasePeriod}
|
||||
periods={paraInfo.leases}
|
||||
/>
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
&.isDisabled {
|
||||
td {
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
td.badge.together > div {
|
||||
display: inline-block;
|
||||
margin: 0 0.25rem 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Teyrchain);
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import useChainDetails from './useChainDetails.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
id: ParaId;
|
||||
}
|
||||
|
||||
function TeyrchainInfo ({ className, id }: Props): React.ReactElement<Props> {
|
||||
const { bestNumber, runtimeVersion } = useChainDetails(id);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
{bestNumber && <div>{formatNumber(bestNumber)}</div>}
|
||||
{runtimeVersion && <div className='version'><div className='media--1100'>{runtimeVersion.specName.toString()}</div><div className='media--1100'>/</div><div>{runtimeVersion.specVersion.toString()}</div></div>}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.version {
|
||||
font-size: var(--font-size-small);
|
||||
white-space: nowrap;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
max-width: 10em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(TeyrchainInfo);
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { LeasePeriod, QueuedAction, ScheduledProposals } from '../types.js';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useBestNumber, useIsParasLinked } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Teyrchain from './Teyrchain.js';
|
||||
import useEvents from './useEvents.js';
|
||||
import useValidators from './useValidators.js';
|
||||
|
||||
interface Props {
|
||||
actionsQueue: QueuedAction[];
|
||||
ids?: ParaId[];
|
||||
leasePeriod?: LeasePeriod;
|
||||
scheduled?: ScheduledProposals[];
|
||||
}
|
||||
|
||||
function extractScheduledIds (scheduled: ScheduledProposals[] = []): Record<string, boolean> {
|
||||
return scheduled.reduce((all: Record<string, boolean>, { scheduledIds }: ScheduledProposals): Record<string, boolean> =>
|
||||
scheduledIds.reduce((all: Record<string, boolean>, id) => ({
|
||||
...all,
|
||||
[id.toString()]: true
|
||||
}), all), {});
|
||||
}
|
||||
|
||||
function extractActions (actionsQueue: QueuedAction[], knownIds: [ParaId, string][] = []): Record<string, QueuedAction | undefined> {
|
||||
return knownIds.reduce((all: Record<string, QueuedAction | undefined>, [id, key]) => ({
|
||||
...all,
|
||||
[key]: actionsQueue.find(({ paraIds }) => paraIds.some((p) => p.eq(id)))
|
||||
}), {});
|
||||
}
|
||||
|
||||
function extractIds (hasLinksMap: Record<string, boolean>, ids: ParaId[]): [ParaId, string][] | undefined {
|
||||
return ids
|
||||
.map((id): [ParaId, string] => [id, id.toString()])
|
||||
.sort(([aId, aIds], [bId, bIds]): number => {
|
||||
const aKnown = hasLinksMap[aIds] || false;
|
||||
const bKnown = hasLinksMap[bIds] || false;
|
||||
|
||||
return aKnown === bKnown
|
||||
? aId.cmp(bId)
|
||||
: aKnown
|
||||
? -1
|
||||
: 1;
|
||||
});
|
||||
}
|
||||
|
||||
function Teyrchains ({ actionsQueue, ids, leasePeriod, scheduled }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const bestNumber = useBestNumber();
|
||||
const { lastBacked, lastIncluded, lastTimeout } = useEvents();
|
||||
const hasLinksMap = useIsParasLinked(ids);
|
||||
const [validators, validatorMap] = useValidators(ids);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('teyrchains'), 'start', 2],
|
||||
['', 'media--1400'],
|
||||
[t('head'), 'start media--1500'],
|
||||
[t('lifecycle'), 'start'],
|
||||
[],
|
||||
[t('included'), undefined, 2],
|
||||
[t('backed'), 'no-pad-left media--900'],
|
||||
[t('timeout'), 'no-pad-left media--1600'],
|
||||
[t('chain'), 'no-pad-left'],
|
||||
[t('in/out'), 'media--1700', 2],
|
||||
[t('leases'), 'media--1100']
|
||||
]);
|
||||
|
||||
const scheduledIds = useMemo(
|
||||
() => extractScheduledIds(scheduled),
|
||||
[scheduled]
|
||||
);
|
||||
|
||||
const knownIds = useMemo(
|
||||
() => ids && extractIds(hasLinksMap, ids),
|
||||
[ids, hasLinksMap]
|
||||
);
|
||||
|
||||
const nextActions = useMemo(
|
||||
() => extractActions(actionsQueue, knownIds),
|
||||
[actionsQueue, knownIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
empty={knownIds && t('There are no registered teyrchains')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{knownIds?.map(([id, key]): React.ReactNode => (
|
||||
<Teyrchain
|
||||
bestNumber={bestNumber}
|
||||
id={id}
|
||||
isScheduled={scheduledIds[key]}
|
||||
key={key}
|
||||
lastBacked={lastBacked[key]}
|
||||
lastInclusion={lastIncluded[key]}
|
||||
lastTimeout={lastTimeout[key]}
|
||||
leasePeriod={leasePeriod}
|
||||
nextAction={nextActions[key]}
|
||||
sessionValidators={validators}
|
||||
validators={validatorMap[key]}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Teyrchains);
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { LeasePeriod, Proposals, QueuedAction } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Summary from './Summary.js';
|
||||
import Teyrchains from './Teyrchains.js';
|
||||
|
||||
interface Props {
|
||||
actionsQueue: QueuedAction[];
|
||||
className?: string;
|
||||
leasePeriod?: LeasePeriod;
|
||||
paraIds?: ParaId[];
|
||||
proposals?: Proposals;
|
||||
threadIds?: ParaId[];
|
||||
upcomingIds?: ParaId[];
|
||||
}
|
||||
|
||||
function Overview ({ actionsQueue, className, leasePeriod, paraIds, proposals, threadIds }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary
|
||||
leasePeriod={leasePeriod}
|
||||
proposalCount={proposals?.proposalIds.length}
|
||||
teyrchainCount={paraIds?.length}
|
||||
upcomingCount={threadIds?.length}
|
||||
/>
|
||||
<Teyrchains
|
||||
actionsQueue={actionsQueue}
|
||||
ids={paraIds}
|
||||
leasePeriod={leasePeriod}
|
||||
scheduled={proposals?.scheduled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, ParaValidatorIndex } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export interface EventMapInfo {
|
||||
blockHash: string;
|
||||
blockNumber: BN;
|
||||
relayParent: string;
|
||||
}
|
||||
|
||||
export interface ValidatorInfo {
|
||||
indexActive: ParaValidatorIndex;
|
||||
indexValidator: ParaValidatorIndex;
|
||||
validatorId: AccountId;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BlockNumber, Header, ParaId, RuntimeVersion } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { createNamedHook, useCall, useParaApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Result {
|
||||
bestNumber?: BlockNumber;
|
||||
runtimeVersion?: RuntimeVersion;
|
||||
}
|
||||
|
||||
const HDR_OPTS = {
|
||||
transform: (header: Header) => header.number.unwrap()
|
||||
};
|
||||
|
||||
function useChainDetailsImpl (id: ParaId): Result {
|
||||
const { api } = useParaApi(id);
|
||||
|
||||
// We are not using the derive here, we keep this queries to the point to not overload
|
||||
return {
|
||||
bestNumber: useCall<BlockNumber>(api?.rpc.chain.subscribeNewHeads, undefined, HDR_OPTS),
|
||||
runtimeVersion: useCall<RuntimeVersion>(api?.rpc.state.subscribeRuntimeVersion)
|
||||
};
|
||||
}
|
||||
|
||||
export default createNamedHook('useChainDetails', useChainDetailsImpl);
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { SignedBlockExtended } from '@pezkuwi/api-derive/types';
|
||||
import type { Event } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiPrimitivesVstagingCandidateReceiptV2 } from '@pezkuwi/types/lookup';
|
||||
import type { IEvent } from '@pezkuwi/types/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { EventMapInfo } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { stringify } from '@pezkuwi/util';
|
||||
|
||||
type EventMap = Record<string, EventMapInfo>;
|
||||
|
||||
interface Result {
|
||||
lastBacked: EventMap;
|
||||
lastIncluded: EventMap;
|
||||
lastTimeout: EventMap;
|
||||
}
|
||||
|
||||
const EMPTY_EVENTS: Result = { lastBacked: {}, lastIncluded: {}, lastTimeout: {} };
|
||||
|
||||
function includeEntry (map: EventMap, event: Event, blockHash: string, blockNumber: BN): void {
|
||||
try {
|
||||
const { descriptor } = (event as unknown as IEvent<[PezkuwiPrimitivesVstagingCandidateReceiptV2]>).data[0];
|
||||
|
||||
if (descriptor?.paraId) {
|
||||
map[descriptor.paraId.toString()] = {
|
||||
blockHash,
|
||||
blockNumber,
|
||||
relayParent: descriptor.relayParent.toHex()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`${event.section}.${event.method}(${stringify(event.data)}):: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function extractEvents (api: ApiPromise, lastBlock: SignedBlockExtended, prev: Result): Result {
|
||||
const backed: EventMap = {};
|
||||
const included: EventMap = {};
|
||||
const timeout: EventMap = {};
|
||||
const blockNumber = lastBlock.block.header.number.unwrap();
|
||||
const blockHash = lastBlock.block.header.hash.toHex();
|
||||
const paraEvents = (api.events.paraInclusion || api.events.parasInclusion || api.events.inclusion);
|
||||
let wasBacked = false;
|
||||
let wasIncluded = false;
|
||||
let wasTimeout = false;
|
||||
|
||||
paraEvents && lastBlock.events.forEach(({ event, phase }) => {
|
||||
if (phase.isApplyExtrinsic) {
|
||||
if (paraEvents.CandidateBacked.is(event)) {
|
||||
includeEntry(backed, event, blockHash, blockNumber);
|
||||
wasBacked = true;
|
||||
} else if (paraEvents.CandidateIncluded.is(event)) {
|
||||
includeEntry(included, event, blockHash, blockNumber);
|
||||
wasIncluded = true;
|
||||
} else if (paraEvents.CandidateTimedOut.is(event)) {
|
||||
includeEntry(timeout, event, blockHash, blockNumber);
|
||||
wasTimeout = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wasBacked || wasIncluded || wasTimeout
|
||||
? {
|
||||
lastBacked: wasBacked
|
||||
? { ...prev.lastBacked, ...backed }
|
||||
: prev.lastBacked,
|
||||
lastIncluded: wasIncluded
|
||||
? { ...prev.lastIncluded, ...included }
|
||||
: prev.lastIncluded,
|
||||
lastTimeout: wasTimeout
|
||||
? { ...prev.lastTimeout, ...timeout }
|
||||
: prev.lastTimeout
|
||||
}
|
||||
: prev;
|
||||
}
|
||||
|
||||
function useEventsImpl (): Result {
|
||||
const { api } = useApi();
|
||||
const lastBlock = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks);
|
||||
const [state, setState] = useState<Result>(EMPTY_EVENTS);
|
||||
|
||||
useEffect((): void => {
|
||||
lastBlock && setState((prev) =>
|
||||
extractEvents(api, lastBlock, prev)
|
||||
);
|
||||
}, [api, lastBlock]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useEvents', useEventsImpl);
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, Vec } from '@pezkuwi/types';
|
||||
import type { AccountId, BalanceOf, BlockNumber, CandidatePendingAvailability, HeadData, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeCommonParasRegistrarParaInfo, PezkuwiRuntimeTeyrchainsParasParaLifecycle } from '@pezkuwi/types/lookup';
|
||||
import type { Codec, ITuple } from '@pezkuwi/types/types';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
|
||||
type QueryResult = [Option<HeadData>, Option<BlockNumber>, Option<PezkuwiRuntimeTeyrchainsParasParaLifecycle>, Vec<Codec>, Vec<Codec>, Vec<Codec>, Vec<Codec>, Option<BlockNumber>, Option<CandidatePendingAvailability>, Option<PezkuwiRuntimeCommonParasRegistrarParaInfo>, Option<ITuple<[AccountId, BalanceOf]>>[]];
|
||||
|
||||
interface Result {
|
||||
headHex: string | null;
|
||||
leases: number[];
|
||||
lifecycle: PezkuwiRuntimeTeyrchainsParasParaLifecycle | null;
|
||||
paraInfo: PezkuwiRuntimeCommonParasRegistrarParaInfo | null;
|
||||
pendingAvail: CandidatePendingAvailability | null;
|
||||
updateAt: BlockNumber | null;
|
||||
qDmp: number;
|
||||
qUmp: number;
|
||||
qHrmpE: number;
|
||||
qHrmpI: number;
|
||||
watermark: BlockNumber | null;
|
||||
}
|
||||
|
||||
const MULTI_OPTS = {
|
||||
defaultValue: {
|
||||
headHex: null,
|
||||
leases: [],
|
||||
lifecycle: null,
|
||||
paraInfo: null,
|
||||
pendingAvail: null,
|
||||
qDmp: 0,
|
||||
qHrmpE: 0,
|
||||
qHrmpI: 0,
|
||||
qUmp: 0,
|
||||
updateAt: null,
|
||||
watermark: null
|
||||
},
|
||||
transform: ([headData, optUp, optLifecycle, dmp, ump, hrmpE, hrmpI, optWm, optPending, optInfo, leases]: QueryResult): Result => ({
|
||||
headHex: headData.isSome
|
||||
? headData.unwrap().toHex()
|
||||
: null,
|
||||
leases: leases
|
||||
.map((opt, index) => opt.isSome ? index : -1)
|
||||
.filter((period) => period !== -1),
|
||||
lifecycle: optLifecycle.unwrapOr(null),
|
||||
paraInfo: optInfo.unwrapOr(null),
|
||||
pendingAvail: optPending?.isSome ? optPending.unwrapOr(null) : null,
|
||||
qDmp: dmp.length,
|
||||
qHrmpE: hrmpE.length,
|
||||
qHrmpI: hrmpI.length,
|
||||
qUmp: ump?.length ?? 0,
|
||||
updateAt: optUp.unwrapOr(null),
|
||||
watermark: optWm.unwrapOr(null)
|
||||
})
|
||||
};
|
||||
|
||||
function useParaInfoImpl (id: ParaId): Result {
|
||||
const { api } = useApi();
|
||||
|
||||
return useCallMulti<Result>([
|
||||
[api.query.paras.heads, id],
|
||||
[api.query.paras.futureCodeUpgrades, id],
|
||||
[api.query.paras.paraLifecycles, id],
|
||||
[(api.query.parasDmp || api.query.paraDmp || api.query.dmp)?.downwardMessageQueues, id],
|
||||
[(api.query.parasUmp || api.query.ump)?.relayDispatchQueues, id],
|
||||
[(api.query.parasHrmp || api.query.paraHrmp || api.query.hrmp)?.hrmpEgressChannelsIndex, id],
|
||||
[(api.query.parasHrmp || api.query.paraHrmp || api.query.hrmp)?.hrmpIngressChannelsIndex, id],
|
||||
[(api.query.parasHrmp || api.query.paraHrmp || api.query.hrmp)?.hrmpWatermarks, id],
|
||||
[(api.query.parasInclusion || api.query.paraInclusion || api.query.inclusion)?.pendingAvailability, id],
|
||||
[api.query.registrar.paras, id],
|
||||
[api.query.slots.leases, id]
|
||||
], MULTI_OPTS);
|
||||
}
|
||||
|
||||
export default createNamedHook('useParaInfo', useParaInfoImpl);
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, CoreAssignment, GroupIndex, ParaId, ParaValidatorIndex } from '@pezkuwi/types/interfaces';
|
||||
import type { ValidatorInfo } from './types.js';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
|
||||
type MultiResult = [AccountId[] | null, CoreAssignment[] | null, ParaValidatorIndex[][] | null, ParaValidatorIndex[] | null];
|
||||
|
||||
type Result = [AccountId[] | null, Record<string, [GroupIndex, ValidatorInfo[]]>];
|
||||
|
||||
const optionsMulti = {
|
||||
defaultValue: [null, null, null, null] as MultiResult
|
||||
};
|
||||
|
||||
function mapValidators (startWith: Record<string, [GroupIndex, ValidatorInfo[]]>, ids: ParaId[], validators: AccountId[], groups: ParaValidatorIndex[][], indices: ParaValidatorIndex[], scheduled: CoreAssignment[]): Record<string, [GroupIndex, ValidatorInfo[]]> {
|
||||
return ids.reduce((all: Record<string, [GroupIndex, ValidatorInfo[]]>, id) => {
|
||||
// paraId should never be undefined, since it comes from the state, yet here we are...
|
||||
// See https://github.com/pezkuwichain/pezkuwi-apps/issues/6435
|
||||
const assignment = scheduled.find(({ paraId }) => paraId && paraId.eq(id));
|
||||
|
||||
if (!assignment) {
|
||||
return all;
|
||||
}
|
||||
|
||||
return {
|
||||
...all,
|
||||
[id.toString()]: [
|
||||
assignment.groupIdx,
|
||||
(groups[assignment.groupIdx.toNumber()] || [])
|
||||
.map((index) => [index, indices[index.toNumber()]])
|
||||
.filter(([, a]) => a)
|
||||
.map(([indexActive, indexValidator]) => ({
|
||||
indexActive,
|
||||
indexValidator,
|
||||
validatorId: validators[indexValidator.toNumber()]
|
||||
}))
|
||||
]
|
||||
};
|
||||
}, { ...startWith });
|
||||
}
|
||||
|
||||
function useValidatorsImpl (ids?: ParaId[]): Result {
|
||||
const { api } = useApi();
|
||||
const [validators, scheduled, groups, indices] = useCallMulti<MultiResult>([
|
||||
api.query.session.validators,
|
||||
(api.query.parasScheduler || api.query.paraScheduler || api.query.scheduler)?.scheduled,
|
||||
(api.query.parasScheduler || api.query.paraScheduler || api.query.scheduler)?.validatorGroups,
|
||||
(api.query.parasShared || api.query.paraShared || api.query.shared)?.activeValidatorIndices
|
||||
], optionsMulti);
|
||||
const [state, setState] = useState<Record<string, [GroupIndex, ValidatorInfo[]]>>({});
|
||||
|
||||
useEffect((): void => {
|
||||
groups && ids && indices && scheduled && validators && setState((prev) =>
|
||||
mapValidators(prev, ids, validators, groups, indices, scheduled)
|
||||
);
|
||||
}, [groups, ids, indices, scheduled, validators]);
|
||||
|
||||
return useMemo(
|
||||
(): Result => [validators, state],
|
||||
[state, validators]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useValidators', useValidatorsImpl);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { OwnedId } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { LOWEST_PUBLIC_ID } from './constants.js';
|
||||
import DeregisterId from './DeregisterId.js';
|
||||
import RegisterId from './RegisterId.js';
|
||||
import RegisterThread from './RegisterThread.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
const OPT_NEXT = {
|
||||
transform: (nextId: ParaId) =>
|
||||
nextId.isZero()
|
||||
? LOWEST_PUBLIC_ID
|
||||
: nextId
|
||||
};
|
||||
|
||||
function Actions ({ className, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isRegisterOpen, toggleRegisterOpen] = useToggle();
|
||||
const [isReserveOpen, toggleReserveOpen] = useToggle();
|
||||
const [isDeregisterOpen, toggleDeregisterOpen] = useToggle();
|
||||
const nextParaId = useCall<ParaId | BN>(api.query.registrar.nextFreeParaId, [], OPT_NEXT);
|
||||
|
||||
return (
|
||||
<Button.Group className={className}>
|
||||
<Button
|
||||
icon='minus'
|
||||
isDisabled={api.tx.registrar.deregister ? !ownedIds.length : false}
|
||||
label={t('Deregister')}
|
||||
onClick={toggleDeregisterOpen}
|
||||
/>
|
||||
{isDeregisterOpen && (
|
||||
<DeregisterId
|
||||
nextParaId={nextParaId}
|
||||
onClose={toggleDeregisterOpen}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!api.tx.registrar.reserve}
|
||||
label={t('ParaId')}
|
||||
onClick={toggleReserveOpen}
|
||||
/>
|
||||
{isReserveOpen && (
|
||||
<RegisterId
|
||||
nextParaId={nextParaId}
|
||||
onClose={toggleReserveOpen}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={api.tx.registrar.reserve ? !ownedIds.length : false}
|
||||
label={t('ParaThread')}
|
||||
onClick={toggleRegisterOpen}
|
||||
/>
|
||||
{isRegisterOpen && (
|
||||
<RegisterThread
|
||||
nextParaId={nextParaId}
|
||||
onClose={toggleRegisterOpen}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
)}
|
||||
</Button.Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Actions);
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { OwnedId, OwnerInfo } from '../types.js';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { InputAddress, InputNumber, MarkWarning, Modal, styled, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { CallExpander } from '@pezkuwi/react-params';
|
||||
|
||||
import InputOwner from '../InputOwner.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { LOWEST_INVALID_ID } from './constants.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
nextParaId?: BN;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
function DeregisterId ({ className, nextParaId, onClose, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [paraId, setParaId] = useState<BN | undefined>();
|
||||
|
||||
const _setOwner = useCallback(
|
||||
({ accountId, paraId }: OwnerInfo) => {
|
||||
setAccountId(accountId);
|
||||
setParaId(new BN(paraId));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const isIdError = !paraId || !paraId.gt(LOWEST_INVALID_ID);
|
||||
|
||||
const extrinsic = useMemo(() => api.tx.registrar.deregister(paraId), [api.tx.registrar, paraId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Deregister ParaId')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
{api.tx.registrar.reserve
|
||||
? (
|
||||
<InputOwner
|
||||
noCodeCheck
|
||||
onChange={_setOwner}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Modal.Columns hint={t('This account will be associated with the teyrchain and pay the deposit.')}>
|
||||
<InputAddress
|
||||
label={t('register from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The id of this teyrchain as known on the network')}>
|
||||
<InputNumber
|
||||
autoFocus
|
||||
defaultValue={nextParaId}
|
||||
isError={isIdError}
|
||||
isZeroable={false}
|
||||
label={t('teyrchain id')}
|
||||
onChange={setParaId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Modal.Columns>
|
||||
<CallExpander
|
||||
isExpanded
|
||||
isHeader
|
||||
value={extrinsic}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<MarkWarning withIcon={false}>
|
||||
<strong>{t('Deregistering a paraID will:')}</strong>
|
||||
<WarningList>
|
||||
<WarningItem>{t('Remove it from the active teyrchain/parathread set.')}</WarningItem>
|
||||
<WarningItem>{t('Exclude it from future auctions and onboarding.')}</WarningItem>
|
||||
<WarningItem>{t('Potentially release any reserved deposits linked to it.')}</WarningItem>
|
||||
<WarningItem>{t('Require re-registration if you wish to use it again.')}</WarningItem>
|
||||
</WarningList>
|
||||
<p><strong>{t('This action is permanent.')}</strong> {t('Please ensure this is your intended action before continuing.')}</p>
|
||||
</MarkWarning>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='minus'
|
||||
isDisabled={isIdError}
|
||||
label={t('Deregister')}
|
||||
onStart={onClose}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const WarningList = styled.ul`
|
||||
padding-left: 20px;
|
||||
margin: 8px 0;
|
||||
`;
|
||||
|
||||
const WarningItem = styled.li`
|
||||
margin-bottom: 2px;
|
||||
`;
|
||||
|
||||
export default React.memo(DeregisterId);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { LeaseInfo, LeasePeriod, QueuedAction } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, ParaLink, Table, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Lifecycle from '../Overview/Lifecycle.js';
|
||||
// import TeyrchainInfo from '../Overview/TeyrchainInfo.js';
|
||||
import Periods from '../Overview/Periods.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useThreadInfo from './useThreadInfo.js';
|
||||
|
||||
interface Props {
|
||||
id: ParaId;
|
||||
leasePeriod: LeasePeriod;
|
||||
leases: LeaseInfo[];
|
||||
nextAction?: QueuedAction;
|
||||
}
|
||||
|
||||
function Parathread ({ id, leasePeriod, leases, nextAction }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { isAccount } = useAccounts();
|
||||
const { headHex, lifecycle, manager } = useThreadInfo(id);
|
||||
|
||||
const periods = useMemo(
|
||||
() => leasePeriod?.currentPeriod && leases?.map(({ period }) => period),
|
||||
[leasePeriod?.currentPeriod, leases]
|
||||
);
|
||||
|
||||
const isManager = isAccount(manager?.toString());
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<Table.Column.Id value={id} />
|
||||
<td className='badge'><ParaLink id={id} /></td>
|
||||
<td className='address media--2000'>{manager && <AddressSmall value={manager} />}</td>
|
||||
<td className='start together hash media--1500'>
|
||||
<div className='shortHash'>{headHex}</div>
|
||||
</td>
|
||||
<td className='start'>
|
||||
<Lifecycle
|
||||
lifecycle={lifecycle}
|
||||
nextAction={nextAction}
|
||||
/>
|
||||
</td>
|
||||
<td className='all' />
|
||||
<td className='number no-pad-left'>
|
||||
{/* <!-- TeyrchainInfo id={id} /--> */}
|
||||
</td>
|
||||
<td className='number together'>
|
||||
{leasePeriod && leases && periods && (
|
||||
leases.length
|
||||
? (
|
||||
<Periods
|
||||
fromFirst
|
||||
leasePeriod={leasePeriod}
|
||||
periods={periods}
|
||||
/>
|
||||
)
|
||||
: t('None')
|
||||
)}
|
||||
</td>
|
||||
<td className='button media--900'>
|
||||
<TxButton
|
||||
accountId={manager}
|
||||
icon='times'
|
||||
isDisabled={!isManager}
|
||||
label={t('Deregister')}
|
||||
params={[id]}
|
||||
tx={api.tx.registrar.deregister}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Parathread);
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u128 } from '@pezkuwi/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
nextParaId?: BN;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function RegisterId ({ className, nextParaId, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Reserve ParaId')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will be used to the Id reservation and for the future parathread.')}>
|
||||
<InputAddress
|
||||
label={t('reserve from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The Id of this teyrchain as known on the network (selected from nextFreeId)')}>
|
||||
<InputNumber
|
||||
defaultValue={nextParaId}
|
||||
isDisabled
|
||||
label={t('teyrchain id')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The reservation fee for this Id')}>
|
||||
<InputBalance
|
||||
defaultValue={api.consts.registrar.paraDeposit as u128}
|
||||
isDisabled
|
||||
label={t('reserved deposit')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!nextParaId}
|
||||
onStart={onClose}
|
||||
params={[]}
|
||||
tx={api.tx.registrar.reserve}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RegisterId);
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BalanceOf } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration } from '@pezkuwi/types/lookup';
|
||||
import type { OwnedId, OwnerInfo } from '../types.js';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, InputFile, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN, compactAddLength } from '@pezkuwi/util';
|
||||
|
||||
import InputOwner from '../InputOwner.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { LOWEST_INVALID_ID } from './constants.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
nextParaId?: BN;
|
||||
onClose: () => void;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
function RegisterThread ({ className, nextParaId, onClose, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [paraId, setParaId] = useState<BN | undefined>();
|
||||
const [wasm, setWasm] = useState<Uint8Array | null>(null);
|
||||
const [genesisState, setGenesisState] = useState<Uint8Array | null>(null);
|
||||
const paraConfig = useCall<PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration>(api.query.configuration?.activeConfig);
|
||||
|
||||
const _setGenesisState = useCallback(
|
||||
(data: Uint8Array) => setGenesisState(compactAddLength(data)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setWasm = useCallback(
|
||||
(data: Uint8Array) => setWasm(compactAddLength(data)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setOwner = useCallback(
|
||||
({ accountId, paraId }: OwnerInfo) => {
|
||||
setAccountId(accountId);
|
||||
setParaId(new BN(paraId));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const reservedDeposit = useMemo(
|
||||
() => (api.consts.registrar.paraDeposit as BalanceOf)
|
||||
.add((api.consts.registrar.dataDepositPerByte as BalanceOf).muln(
|
||||
paraConfig?.maxCodeSize
|
||||
? paraConfig.maxCodeSize.toNumber()
|
||||
: wasm
|
||||
? wasm.length
|
||||
: 0
|
||||
))
|
||||
.iadd((api.consts.registrar.dataDepositPerByte as BalanceOf).muln(genesisState ? genesisState.length : 0)),
|
||||
[api, wasm, genesisState, paraConfig]
|
||||
);
|
||||
|
||||
const isIdError = !paraId || !paraId.gt(LOWEST_INVALID_ID);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Register parathread')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
{api.tx.registrar.reserve
|
||||
? (
|
||||
<InputOwner
|
||||
noCodeCheck
|
||||
onChange={_setOwner}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Modal.Columns hint={t('This account will be associated with the teyrchain and pay the deposit.')}>
|
||||
<InputAddress
|
||||
label={t('register from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The id of this teyrchain as known on the network')}>
|
||||
<InputNumber
|
||||
autoFocus
|
||||
defaultValue={nextParaId}
|
||||
isError={isIdError}
|
||||
isZeroable={false}
|
||||
label={t('teyrchain id')}
|
||||
onChange={setParaId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Modal.Columns hint={t('The WASM validation function for this teyrchain.')}>
|
||||
<InputFile
|
||||
isError={!wasm}
|
||||
label={t('code')}
|
||||
onChange={_setWasm}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The genesis state for this teyrchain.')}>
|
||||
<InputFile
|
||||
isError={!genesisState}
|
||||
label={t('initial state')}
|
||||
onChange={_setGenesisState}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The reservation fee for this teyrchain, including base fee and per-byte fees')}>
|
||||
<InputBalance
|
||||
defaultValue={reservedDeposit}
|
||||
isDisabled
|
||||
label={t('reserved deposit')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!wasm || !genesisState || isIdError}
|
||||
onStart={onClose}
|
||||
params={[paraId, genesisState, wasm]}
|
||||
tx={api.tx.registrar.register}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RegisterThread);
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BN, BN_ONE, BN_THOUSAND } from '@pezkuwi/util';
|
||||
|
||||
export const LOWEST_PUBLIC_ID = new BN(2_000);
|
||||
|
||||
export const LOWEST_INVALID_ID = LOWEST_PUBLIC_ID.sub(BN_ONE);
|
||||
|
||||
export const LOWEST_USER_ID = BN_THOUSAND;
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { LeasePeriod, OwnedId, QueuedAction } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Actions from './Actions.js';
|
||||
import Parathread from './Parathread.js';
|
||||
import useParaMap from './useParaMap.js';
|
||||
|
||||
interface Props {
|
||||
actionsQueue: QueuedAction[];
|
||||
className?: string;
|
||||
ids?: ParaId[];
|
||||
leasePeriod?: LeasePeriod;
|
||||
ownedIds: OwnedId[];
|
||||
}
|
||||
|
||||
function Parathreads ({ actionsQueue, className, ids, leasePeriod, ownedIds }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const leaseMap = useParaMap(ids);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('parathreads'), 'start', 2],
|
||||
['', 'media--2000'],
|
||||
[t('head'), 'start media--1500'],
|
||||
[t('lifecycle'), 'start'],
|
||||
[],
|
||||
[], // [t('chain'), 'no-pad-left'],
|
||||
[t('leases')],
|
||||
['', 'media--900']
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Actions ownedIds={ownedIds} />
|
||||
<Table
|
||||
empty={leasePeriod && ids && (ids.length === 0 || leaseMap) && t('There are no available parathreads')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{leasePeriod && leaseMap?.map(([id, leases]): React.ReactNode => (
|
||||
<Parathread
|
||||
id={id}
|
||||
key={id.toString()}
|
||||
leasePeriod={leasePeriod}
|
||||
leases={leases}
|
||||
nextAction={actionsQueue.find(({ paraIds }) =>
|
||||
paraIds.some((p) => p.eq(id))
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Parathreads);
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { AccountId, BalanceOf, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { ITuple } from '@pezkuwi/types/types';
|
||||
import type { LeaseInfo } from '../types.js';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall, useIsParasLinked } from '@pezkuwi/react-hooks';
|
||||
|
||||
type Result = [ParaId, LeaseInfo[]][];
|
||||
|
||||
function extractParaMap (hasLinksMap: Record<string, boolean>, paraIds: ParaId[], leases: Option<ITuple<[AccountId, BalanceOf]>>[][]): Result {
|
||||
return paraIds
|
||||
.reduce((all: Result, id, index): Result => {
|
||||
all.push([
|
||||
id,
|
||||
leases[index]
|
||||
.map((optLease, period): LeaseInfo | null => {
|
||||
if (optLease.isNone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [accountId, balance] = optLease.unwrap();
|
||||
|
||||
return {
|
||||
accountId,
|
||||
balance,
|
||||
period
|
||||
};
|
||||
})
|
||||
.filter((item): item is LeaseInfo => !!item)
|
||||
]);
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.sort(([aId, aLeases], [bId, bLeases]): number => {
|
||||
const aKnown = hasLinksMap[aId.toString()] || false;
|
||||
const bKnown = hasLinksMap[bId.toString()] || false;
|
||||
|
||||
return aLeases.length && bLeases.length
|
||||
? (aLeases[0].period - bLeases[0].period) || aId.cmp(bId)
|
||||
: aLeases.length
|
||||
? -1
|
||||
: bLeases.length
|
||||
? 1
|
||||
: aKnown === bKnown
|
||||
? aId.cmp(bId)
|
||||
: aKnown
|
||||
? -1
|
||||
: 1;
|
||||
});
|
||||
}
|
||||
|
||||
function useParaMapImpl (ids?: ParaId[]): Result | undefined {
|
||||
const { api } = useApi();
|
||||
const hasLinksMap = useIsParasLinked(ids);
|
||||
const transform = useCallback(
|
||||
([[paraIds], leases]: [[ParaId[]], Option<ITuple<[AccountId, BalanceOf]>>[][]]): Result =>
|
||||
extractParaMap(hasLinksMap, paraIds, leases),
|
||||
[hasLinksMap]
|
||||
);
|
||||
|
||||
return useCall<Result>(ids && api.query.slots.leases.multi, [ids], {
|
||||
transform,
|
||||
withParamsTransform: true
|
||||
});
|
||||
}
|
||||
|
||||
export default createNamedHook('useParaMap', useParaMapImpl);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { AccountId, HeadData, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeCommonParasRegistrarParaInfo, PezkuwiRuntimeTeyrchainsParasParaGenesisArgs, PezkuwiRuntimeTeyrchainsParasParaLifecycle } from '@pezkuwi/types/lookup';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Result {
|
||||
headHex: string | null;
|
||||
lifecycle: PezkuwiRuntimeTeyrchainsParasParaLifecycle | null;
|
||||
manager: AccountId | null;
|
||||
}
|
||||
|
||||
const OPT_MULTI = {
|
||||
defaultValue: {
|
||||
headHex: null,
|
||||
lifecycle: null,
|
||||
manager: null
|
||||
},
|
||||
transform: ([optHead, optGenesis, optLifecycle, optInfo]: [Option<HeadData>, Option<PezkuwiRuntimeTeyrchainsParasParaGenesisArgs>, Option<PezkuwiRuntimeTeyrchainsParasParaLifecycle>, Option<PezkuwiRuntimeCommonParasRegistrarParaInfo>]): Result => ({
|
||||
headHex: optHead.isSome
|
||||
? optHead.unwrap().toHex()
|
||||
: optGenesis.isSome
|
||||
? optGenesis.unwrap().genesisHead.toHex()
|
||||
: null,
|
||||
lifecycle: optLifecycle.unwrapOr(null),
|
||||
manager: optInfo.isSome
|
||||
? optInfo.unwrap().manager
|
||||
: null
|
||||
})
|
||||
};
|
||||
|
||||
function useThreadInfoImpl (id: ParaId): Result {
|
||||
const { api } = useApi();
|
||||
|
||||
return useCallMulti<Result>([
|
||||
[api.query.paras.heads, id],
|
||||
[api.query.paras.upcomingParasGenesis, id],
|
||||
[api.query.paras.paraLifecycles, id],
|
||||
[api.query.registrar.paras, id]
|
||||
], OPT_MULTI);
|
||||
}
|
||||
|
||||
export default createNamedHook('useThreadInfo', useThreadInfoImpl);
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Propose from './Propose.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Actions (): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const [showPropose, togglePropose] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!hasAccounts}
|
||||
label={t('Propose')}
|
||||
onClick={togglePropose}
|
||||
/>
|
||||
</Button.Group>
|
||||
{showPropose && (
|
||||
<Propose onClose={togglePropose} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Actions);
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { ScheduledProposals } from '../types.js';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddressMini, AddressSmall, Badge, Expander, ParaLink, Table, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useSudo } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { sliceHex } from '../util.js';
|
||||
import useProposal from './useProposal.js';
|
||||
|
||||
interface Props {
|
||||
approvedIds: ParaId[];
|
||||
id: ParaId;
|
||||
scheduled: ScheduledProposals[];
|
||||
}
|
||||
|
||||
function Proposal ({ approvedIds, id, scheduled }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const { hasSudoKey, sudoKey } = useSudo();
|
||||
const proposal = useProposal(id, approvedIds, scheduled);
|
||||
|
||||
const cancelTx = useMemo(
|
||||
() => api.tx.sudo && hasSudoKey
|
||||
? api.tx.sudo.sudo(api.tx.proposeTeyrchain.cancelProposal(id))
|
||||
: allAccounts.some((a) => proposal.proposal?.proposer.eq(a))
|
||||
? api.tx.proposeTeyrchain.cancelProposal(id)
|
||||
: null,
|
||||
[api, allAccounts, hasSudoKey, id, proposal]
|
||||
);
|
||||
|
||||
const approveTx = useMemo(
|
||||
() => api.tx.sudo?.sudo(api.tx.proposeTeyrchain.approveProposal(id)),
|
||||
[api, id]
|
||||
);
|
||||
|
||||
const initialHex = useMemo(
|
||||
() => proposal?.proposal && sliceHex(proposal.proposal.genesisHead),
|
||||
[proposal]
|
||||
);
|
||||
|
||||
const renderVals = useCallback(
|
||||
() => proposal.proposal?.validators.map((validatorId) => (
|
||||
<AddressMini
|
||||
key={validatorId.toString()}
|
||||
value={validatorId}
|
||||
/>
|
||||
)),
|
||||
[proposal.proposal]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<Table.Column.Id value={id} />
|
||||
<td className='badge together'>
|
||||
{(proposal.isApproved || proposal.isScheduled) && (
|
||||
<Badge
|
||||
color='green'
|
||||
icon={proposal.isScheduled ? 'clock' : 'check'}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='badge'><ParaLink id={id} /></td>
|
||||
<td className='start together'>{proposal.proposal?.name.toUtf8()}</td>
|
||||
<td className='address'>
|
||||
{proposal.proposal?.validators && (
|
||||
<Expander
|
||||
renderChildren={renderVals}
|
||||
summary={t('Validators ({{count}})', { replace: { count: formatNumber(proposal.proposal?.validators.length) } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='address'>{proposal.proposal && <AddressSmall value={proposal.proposal.proposer} />}</td>
|
||||
<td className='number media--1100'>{proposal.proposal && <FormatBalance value={proposal.proposal.balance} />}</td>
|
||||
<td className='start hash together all'>{initialHex}</td>
|
||||
<td className='button'>
|
||||
{!(proposal.isApproved || proposal.isScheduled) && (
|
||||
<>
|
||||
<TxButton
|
||||
accountId={sudoKey}
|
||||
className='media--800'
|
||||
extrinsic={approveTx}
|
||||
icon='check'
|
||||
isDisabled={!hasSudoKey}
|
||||
label={t('Approve')}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={hasSudoKey ? sudoKey : proposal.proposal?.proposer}
|
||||
className='media--1100'
|
||||
extrinsic={cancelTx}
|
||||
icon='ban'
|
||||
isDisabled={!hasSudoKey || !proposal.proposal}
|
||||
label={t('Cancel')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposal);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Proposals as UseProposals } from '../types.js';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Proposal from './Proposal.js';
|
||||
|
||||
interface Props {
|
||||
proposals?: UseProposals;
|
||||
}
|
||||
|
||||
function Proposals ({ proposals }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortedIds = useMemo(
|
||||
() => proposals?.proposalIds.sort((a, b) => a.cmp(b)),
|
||||
[proposals]
|
||||
);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('proposals'), 'start', 3],
|
||||
[],
|
||||
[],
|
||||
[t('proposer'), 'address'],
|
||||
[t('balance'), 'media--1100'],
|
||||
[t('initial state'), 'start media--1400'],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
empty={proposals && sortedIds && t('There are no pending proposals')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{proposals && sortedIds?.map((id): React.ReactNode => (
|
||||
<Proposal
|
||||
approvedIds={proposals.approvedIds}
|
||||
id={id}
|
||||
key={id.toString()}
|
||||
scheduled={proposals.scheduled}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposals);
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, InputBalance, InputFile, InputNumber, InputWasm, MarkWarning, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { BN, BN_TEN, BN_THOUSAND, BN_ZERO, compactAddLength } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface CodeState {
|
||||
isWasmValid: boolean;
|
||||
wasm: Uint8Array | null;
|
||||
}
|
||||
|
||||
interface ValidatorProps {
|
||||
address: string;
|
||||
index: number;
|
||||
setAddress: (index: number, value: string) => void;
|
||||
t: (key: string, options?: { replace: Record<string, unknown> }) => string;
|
||||
}
|
||||
|
||||
function Validator ({ address, index, setAddress, t }: ValidatorProps): React.ReactElement<ValidatorProps> {
|
||||
const _setAddress = useCallback(
|
||||
(value: string | null) => value && setAddress(index, value),
|
||||
[index, setAddress]
|
||||
);
|
||||
|
||||
return (
|
||||
<InputAddress
|
||||
defaultValue={address}
|
||||
label={t('validator {{index}}', { replace: { index: index + 1 } })}
|
||||
onChange={_setAddress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Propose ({ className, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [paraId, setParaId] = useState<BN | undefined>();
|
||||
const [balance, setBalance] = useState<BN | undefined>(() => 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 });
|
||||
const [genesisState, setGenesisState] = useState<Uint8Array | null>(null);
|
||||
|
||||
const _setGenesisState = useCallback(
|
||||
(data: Uint8Array) => setGenesisState(compactAddLength(data)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setWasm = useCallback(
|
||||
(wasm: Uint8Array, isWasmValid: boolean) => setWasm({ isWasmValid, wasm }),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setAddress = useCallback(
|
||||
(index: number, address: string) =>
|
||||
setValidators((v) => v.map((v, i) => i === index ? address : v)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _addValidator = useCallback(
|
||||
() => setValidators((v) => [...v, '']),
|
||||
[]
|
||||
);
|
||||
|
||||
const _delValidator = useCallback(
|
||||
() => setValidators((v) => [...v.slice(0, v.length - 1)]),
|
||||
[]
|
||||
);
|
||||
|
||||
const isNameValid = name.length >= 3;
|
||||
const isValDuplicate = validators.some((a, ai) => validators.some((b, bi) => ai !== bi && a === b));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Propose teyrchain')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('This account will be associated with the teyrchain and pay the deposit.')}>
|
||||
<InputAddress
|
||||
label={t('propose from')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The name for this teyrchain, the id and the allocated/requested balance.')}>
|
||||
<Input
|
||||
autoFocus
|
||||
isError={!isNameValid}
|
||||
label={t('teyrchain name')}
|
||||
onChange={setName}
|
||||
/>
|
||||
<InputNumber
|
||||
isZeroable={false}
|
||||
label={t('requested id')}
|
||||
onChange={setParaId}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={balance}
|
||||
label={t('initial balance')}
|
||||
onChange={setBalance}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The WASM validation function as well as the genesis state for this teyrchain.')}>
|
||||
<InputWasm
|
||||
isError={!isWasmValid}
|
||||
label={t('validation code')}
|
||||
onChange={_setWasm}
|
||||
placeholder={wasm && !isWasmValid && t('The code is not recognized as being in valid WASM format')}
|
||||
/>
|
||||
<InputFile
|
||||
isError={!genesisState}
|
||||
label={t('genesis state')}
|
||||
onChange={_setGenesisState}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The validators for this teyrchain. At least one is required and where multiple is supplied, they need to be unique.')}>
|
||||
{validators.map((address, index) => (
|
||||
<Validator
|
||||
address={address}
|
||||
index={index}
|
||||
key={index}
|
||||
setAddress={_setAddress}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
{!validators.length && (
|
||||
<MarkWarning content={t('You need to supply at last one running validator for your teyrchain alongside this request.')} />
|
||||
)}
|
||||
{isValDuplicate && (
|
||||
<MarkWarning content={t('You have duplicated validator entries, ensure each is unique.')} />
|
||||
)}
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Add validator')}
|
||||
onClick={_addValidator}
|
||||
/>
|
||||
<Button
|
||||
icon='minus'
|
||||
isDisabled={validators.length === 0}
|
||||
label={t('Remove validator')}
|
||||
onClick={_delValidator}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!isWasmValid || !genesisState || !isNameValid || !validators.length || !paraId?.gt(BN_ZERO)}
|
||||
onStart={onClose}
|
||||
params={[paraId, name, wasm, genesisState, validators, balance]}
|
||||
tx={api.tx.proposeTeyrchain?.proposeTeyrchain}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Propose);
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Proposals } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Actions from './Actions.js';
|
||||
import ProposalList from './Proposals.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
proposals?: Proposals;
|
||||
}
|
||||
|
||||
function ProposalsTab ({ className, proposals }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Actions />
|
||||
<ProposalList proposals={proposals} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ProposalsTab);
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { ParaId, TeyrchainProposal } from '@pezkuwi/types/interfaces';
|
||||
import type { ProposalExt, ScheduledProposals } from '../types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
function useProposalImpl (id: ParaId, approvedIds: ParaId[], scheduled: ScheduledProposals[]): ProposalExt {
|
||||
const { api } = useApi();
|
||||
const opt = useCall<Option<TeyrchainProposal>>(api.query.proposeTeyrchain.proposals, [id]);
|
||||
|
||||
return useMemo(
|
||||
(): ProposalExt => ({
|
||||
id,
|
||||
isApproved: approvedIds.some((a) => a.eq(id)),
|
||||
isScheduled: scheduled.some(({ scheduledIds }) => scheduledIds.some((s) => s.eq(id))),
|
||||
proposal: opt && opt.isSome
|
||||
? opt.unwrap()
|
||||
: undefined
|
||||
}),
|
||||
[approvedIds, id, opt, scheduled]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useProposal', useProposalImpl);
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/types';
|
||||
import type { DeriveBalancesAll } from '@pezkuwi/api-derive/types';
|
||||
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
|
||||
import type { Option } from '@pezkuwi/apps-config/settings/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ChainImg, Dropdown, InputAddress, InputBalance, MarkWarning, Modal, styled, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useApiUrl, useCall, useTeleport } from '@pezkuwi/react-hooks';
|
||||
import { Available, FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_HUNDRED, BN_ZERO, isFunction, nextTick } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const INVALID_PARAID = Number.MAX_SAFE_INTEGER;
|
||||
const XCM_LOC = ['xcm', 'xcmPallet', 'pezkuwiXcm'];
|
||||
|
||||
function getDestMultilocation (isParaTeleport: boolean | undefined, recipientParaId: number) {
|
||||
if (isParaTeleport) {
|
||||
if (recipientParaId === -1) { // para -> relay
|
||||
return {
|
||||
interior: 'Here',
|
||||
parents: 1
|
||||
};
|
||||
} else { // para -> para
|
||||
return {
|
||||
interior: {
|
||||
X1: [{
|
||||
ParaChain: recipientParaId
|
||||
}]
|
||||
},
|
||||
parents: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// relay -> para
|
||||
return {
|
||||
interior: {
|
||||
X1: [{
|
||||
ParaChain: recipientParaId
|
||||
}]
|
||||
},
|
||||
parents: 0
|
||||
};
|
||||
}
|
||||
|
||||
function createOption ({ paraId, text, ui }: LinkOption): Option {
|
||||
return {
|
||||
text: (
|
||||
<div
|
||||
className='ui--Dropdown-item'
|
||||
key={paraId}
|
||||
>
|
||||
<ChainImg
|
||||
className='ui--Dropdown-icon'
|
||||
logo={ui.logo}
|
||||
/>
|
||||
<div className='ui--Dropdown-name'>{text}</div>
|
||||
</div>
|
||||
),
|
||||
value: paraId || -1
|
||||
};
|
||||
}
|
||||
|
||||
function Teleport ({ onClose }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amount, setAmount] = useState<BN | undefined>(BN_ZERO);
|
||||
const [recipientId, setRecipientId] = useState<string | null>(null);
|
||||
const [senderId, setSenderId] = useState<string | null>(null);
|
||||
const [recipientParaId, setParaId] = useState(INVALID_PARAID);
|
||||
const { allowTeleport, destinations, isParaTeleport, oneWay } = useTeleport();
|
||||
const balances = useCall<DeriveBalancesAll>(api.derive.balances?.all, [senderId]);
|
||||
const [maxTransfer, setMaxTransfer] = useState<BN | null>(null);
|
||||
|
||||
const call = useMemo(
|
||||
(): SubmittableExtrinsicFunction<'promise'> => {
|
||||
const m = XCM_LOC.filter((x) => api.tx[x] && isFunction(api.tx[x].limitedTeleportAssets))[0];
|
||||
|
||||
return api.tx[m].limitedTeleportAssets;
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const chainOpts = useMemo(
|
||||
() => destinations.map(createOption),
|
||||
[destinations]
|
||||
);
|
||||
|
||||
const urls = useMemo(
|
||||
() => destinations.find(({ paraId }, index) =>
|
||||
recipientParaId === -1
|
||||
? index === 0
|
||||
: recipientParaId === paraId
|
||||
)?.providers,
|
||||
[destinations, recipientParaId]
|
||||
);
|
||||
|
||||
const destApi = useApiUrl(urls);
|
||||
|
||||
const params = useMemo(
|
||||
() => [
|
||||
{ V4: getDestMultilocation(isParaTeleport, recipientParaId) },
|
||||
{
|
||||
V4: {
|
||||
interior: {
|
||||
X1: [{
|
||||
AccountId32: {
|
||||
id: api.createType('AccountId32', recipientId).toHex(),
|
||||
network: null
|
||||
}
|
||||
}]
|
||||
},
|
||||
parents: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
V4: [{
|
||||
fun: {
|
||||
Fungible: amount
|
||||
},
|
||||
id: {
|
||||
interior: 'Here',
|
||||
parents: isParaTeleport
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
}]
|
||||
},
|
||||
0,
|
||||
{ Unlimited: null }
|
||||
],
|
||||
[api, amount, isParaTeleport, recipientId, recipientParaId]
|
||||
);
|
||||
|
||||
const hasAvailable = !!amount && (maxTransfer ? amount.lte(maxTransfer) : true);
|
||||
|
||||
useEffect(() => {
|
||||
if (balances && balances.accountId.eq(senderId) && senderId && recipientId && api.call.transactionPaymentApi && api.tx.balances) {
|
||||
nextTick(async (): Promise<void> => {
|
||||
try {
|
||||
const extrinsic = call(...params);
|
||||
const { partialFee } = await extrinsic.paymentInfo(senderId);
|
||||
|
||||
const adjFee = partialFee.muln(110).div(BN_HUNDRED);
|
||||
const maxTransfer = (balances.transferable || balances.availableBalance).sub(adjFee);
|
||||
|
||||
setMaxTransfer(
|
||||
api.consts.balances && maxTransfer.gt(api.consts.balances.existentialDeposit)
|
||||
? maxTransfer.sub(api.consts.balances.existentialDeposit)
|
||||
: null
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setMaxTransfer(null);
|
||||
}
|
||||
}, [api.call.transactionPaymentApi, api.consts.balances, api.tx.balances, balances, call, params, recipientId, senderId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t('Teleport assets')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The transferred balance will be subtracted (along with fees) from the sender account.')}>
|
||||
<InputAddress
|
||||
label={t('send from account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t('transferable')}
|
||||
params={senderId}
|
||||
/>
|
||||
}
|
||||
onChange={setSenderId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
{chainOpts.length !== 0 && (
|
||||
<Modal.Columns hint={t('The destination chain for this asset teleport. The transferred value will appear on this chain.')}>
|
||||
<Dropdown
|
||||
defaultValue={chainOpts[0].value}
|
||||
label={t('destination chain')}
|
||||
onChange={setParaId}
|
||||
options={chainOpts}
|
||||
/>
|
||||
{!isParaTeleport && oneWay.includes(recipientParaId) && (
|
||||
<MarkWarning content={t('Currently this is a one-way transfer since the on-chain runtime functionality to send the funds from the destination chain back to this account not yet available.')} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
)}
|
||||
<Modal.Columns hint={t('The beneficiary will have access to the transferred amount when the transaction is included in a block.')}>
|
||||
<InputAddress
|
||||
label={t('send to address')}
|
||||
onChange={setRecipientId}
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
hint={
|
||||
<>
|
||||
<p>{t('This is the amount to be teleported to the destination chain and does not account for the source or the destination transfer fee')}</p>
|
||||
<p>{t('The amount deposited to the recipient will be net the calculated cross-chain fee. If the recipient address is new, the amount deposited should be greater than the Existential Deposit')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
isError={!hasAvailable}
|
||||
isZeroable
|
||||
label={t('amount')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
{maxTransfer &&
|
||||
<StyledInfo>
|
||||
<span>Max -</span>
|
||||
<FormatBalance value={maxTransfer} />
|
||||
</StyledInfo>}
|
||||
<InputBalance
|
||||
defaultValue={destApi?.consts.balances?.existentialDeposit}
|
||||
isDisabled
|
||||
isLoading={!destApi}
|
||||
label={t('destination existential deposit')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<MarkWarning
|
||||
className='warning'
|
||||
withIcon={false}
|
||||
>
|
||||
<p>{t('To ensure a successful XCM transaction, please make sure the following conditions are met:')}</p>
|
||||
<ol>
|
||||
<li>
|
||||
{t('The source account must retain a balance greater than the existential deposit after covering the fee.')}
|
||||
</li>
|
||||
<li>
|
||||
{t('The destination account must hold at least the minimum existential deposit after receiving the transfer and paying any applicable destination fees.')}
|
||||
</li>
|
||||
</ol>
|
||||
</MarkWarning>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={senderId}
|
||||
icon='share-square'
|
||||
isDisabled={!allowTeleport || !hasAvailable || !recipientId || !amount || !destApi || (!isParaTeleport && recipientParaId === INVALID_PARAID)}
|
||||
label={t('Teleport')}
|
||||
onStart={onClose}
|
||||
params={params}
|
||||
tx={call}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Teleport);
|
||||
|
||||
const StyledInfo = styled.p`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
gap: 3px;
|
||||
justify-content: flex-end;
|
||||
span:first-of-type {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { stringToU8a } from '@pezkuwi/util';
|
||||
|
||||
export const CROWD_PREFIX = stringToU8a('modlpy/cfund');
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '@pezkuwi/api-augment/bizinikiwi';
|
||||
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Auctions from './Auctions/index.js';
|
||||
import Crowdloan from './Crowdloan/index.js';
|
||||
import Overview from './Overview/index.js';
|
||||
import Parathreads from './Parathreads/index.js';
|
||||
import Proposals from './Proposals/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
import useActionsQueue from './useActionsQueue.js';
|
||||
import useAuctionInfo from './useAuctionInfo.js';
|
||||
import useFunds from './useFunds.js';
|
||||
import useLeasePeriod from './useLeasePeriod.js';
|
||||
import useOwnedIds from './useOwnedIds.js';
|
||||
import useProposals from './useProposals.js';
|
||||
import useUpcomingIds from './useUpcomingIds.js';
|
||||
import useWinningData from './useWinningData.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function TeyrchainsApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { pathname } = useLocation();
|
||||
const auctionInfo = useAuctionInfo();
|
||||
const campaigns = useFunds();
|
||||
const leasePeriod = useLeasePeriod();
|
||||
const ownedIds = useOwnedIds();
|
||||
const winningData = useWinningData(auctionInfo);
|
||||
const proposals = useProposals();
|
||||
const actionsQueue = useActionsQueue();
|
||||
const upcomingIds = useUpcomingIds();
|
||||
const paraIds = useCall<ParaId[]>(api.query.paras.teyrchains);
|
||||
|
||||
const items = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'parathreads',
|
||||
text: t('Parathreads')
|
||||
},
|
||||
api.query.proposeTeyrchain && {
|
||||
name: 'proposals',
|
||||
text: t('Proposals')
|
||||
},
|
||||
api.query.auctions && {
|
||||
name: 'auctions',
|
||||
text: t('Auctions')
|
||||
},
|
||||
api.query.crowdloan && {
|
||||
name: 'crowdloan',
|
||||
text: t('Crowdloan')
|
||||
}
|
||||
].filter((q) => !!q));
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={items.current}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path={basePath}>
|
||||
<Route
|
||||
element={
|
||||
<Auctions
|
||||
auctionInfo={auctionInfo}
|
||||
campaigns={campaigns}
|
||||
ownedIds={ownedIds}
|
||||
winningData={winningData}
|
||||
/>
|
||||
}
|
||||
path='auctions'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Crowdloan
|
||||
auctionInfo={auctionInfo}
|
||||
campaigns={campaigns}
|
||||
leasePeriod={leasePeriod}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
}
|
||||
path='crowdloan'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Proposals proposals={proposals} />
|
||||
}
|
||||
path='proposals'
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
<Overview
|
||||
actionsQueue={actionsQueue}
|
||||
className={pathname === basePath ? '' : '--hidden'}
|
||||
leasePeriod={leasePeriod}
|
||||
paraIds={paraIds}
|
||||
proposals={proposals}
|
||||
threadIds={upcomingIds}
|
||||
/>
|
||||
<Parathreads
|
||||
actionsQueue={actionsQueue}
|
||||
className={pathname === `${basePath}/parathreads` ? '' : '--hidden'}
|
||||
ids={upcomingIds}
|
||||
leasePeriod={leasePeriod}
|
||||
ownedIds={ownedIds}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TeyrchainsApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-teyrchains');
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, AuctionIndex, BalanceOf, BlockNumber, LeasePeriodOf, ParaId, SessionIndex, TeyrchainProposal } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeCommonCrowdloanFundInfo, PezkuwiRuntimeCommonParasRegistrarParaInfo, PezkuwiRuntimeTeyrchainsHrmpHrmpChannel, PezkuwiTeyrchainPrimitivesPrimitivesHrmpChannelId } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export type ChannelMap = Record<string, [PezkuwiTeyrchainPrimitivesPrimitivesHrmpChannelId, PezkuwiRuntimeTeyrchainsHrmpHrmpChannel][]>;
|
||||
|
||||
export interface AllChannels {
|
||||
dst: ChannelMap;
|
||||
src: ChannelMap;
|
||||
}
|
||||
|
||||
export interface LeaseInfo {
|
||||
accountId: AccountId;
|
||||
balance: BalanceOf;
|
||||
period: number;
|
||||
}
|
||||
|
||||
export interface QueuedAction {
|
||||
paraIds: ParaId[];
|
||||
sessionIndex: BN;
|
||||
}
|
||||
|
||||
export interface AuctionInfo {
|
||||
endBlock: BlockNumber | null;
|
||||
leasePeriod: LeasePeriodOf | null;
|
||||
numAuctions: AuctionIndex;
|
||||
}
|
||||
|
||||
export interface ProposalExt {
|
||||
id: ParaId;
|
||||
isApproved: boolean;
|
||||
isScheduled: boolean;
|
||||
proposal?: TeyrchainProposal;
|
||||
}
|
||||
|
||||
export interface ScheduledProposals {
|
||||
scheduledIds: ParaId[];
|
||||
sessionIndex: SessionIndex;
|
||||
}
|
||||
|
||||
export interface Campaigns {
|
||||
activeCap: BN;
|
||||
activeRaised: BN;
|
||||
funds: Campaign[] | null;
|
||||
isLoading?: boolean;
|
||||
totalCap: BN;
|
||||
totalRaised: BN;
|
||||
}
|
||||
|
||||
export interface Campaign extends WinnerData {
|
||||
info: PezkuwiRuntimeCommonCrowdloanFundInfo;
|
||||
isCapped?: boolean;
|
||||
isEnded?: boolean;
|
||||
isWinner?: boolean;
|
||||
}
|
||||
|
||||
export interface LeasePeriod {
|
||||
currentPeriod: BN;
|
||||
length: BN;
|
||||
progress: BN;
|
||||
remainder: BN;
|
||||
}
|
||||
|
||||
export interface Proposals {
|
||||
approvedIds: ParaId[];
|
||||
proposalIds: ParaId[];
|
||||
scheduled: ScheduledProposals[];
|
||||
}
|
||||
|
||||
export interface OwnedIdPartial {
|
||||
manager: string;
|
||||
paraId: ParaId;
|
||||
paraInfo: PezkuwiRuntimeCommonParasRegistrarParaInfo;
|
||||
}
|
||||
|
||||
export interface OwnedId extends OwnedIdPartial {
|
||||
hasCode: boolean;
|
||||
}
|
||||
|
||||
export interface OwnerInfo {
|
||||
accountId: string | null;
|
||||
paraId: number;
|
||||
}
|
||||
|
||||
export interface WinnerData {
|
||||
accountId: string;
|
||||
firstSlot: BN;
|
||||
isCrowdloan: boolean;
|
||||
key: string;
|
||||
lastSlot: BN;
|
||||
paraId: ParaId;
|
||||
value: BN;
|
||||
}
|
||||
|
||||
export interface Winning {
|
||||
blockNumber: BN;
|
||||
blockOffset: BN;
|
||||
total: BN;
|
||||
winners: WinnerData[];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ParaId, SessionIndex } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { QueuedAction } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN_EIGHT, BN_FIVE, BN_FOUR, BN_NINE, BN_ONE, BN_SEVEN, BN_SIX, BN_TEN, BN_THREE, BN_TWO } from '@pezkuwi/util';
|
||||
|
||||
const INC = [BN_ONE, BN_TWO, BN_THREE, BN_FOUR, BN_FIVE, BN_SIX, BN_SEVEN, BN_EIGHT, BN_NINE, BN_TEN];
|
||||
|
||||
const OPT_NEXT = {
|
||||
withParams: true
|
||||
};
|
||||
|
||||
function useActionsQueueImpl (): QueuedAction[] {
|
||||
const { api } = useApi();
|
||||
const currentIndex = useCall<SessionIndex>(api.query.session.currentIndex);
|
||||
const queryIndexes = useMemo(() => currentIndex && INC.map((i) => currentIndex.add(i)), [currentIndex]);
|
||||
const nextActions = useCall<[[BN[]], ParaId[][]]>(queryIndexes && api.query.paras.actionsQueue.multi, [queryIndexes], OPT_NEXT);
|
||||
|
||||
return useMemo(
|
||||
(): QueuedAction[] =>
|
||||
nextActions
|
||||
? nextActions[0][0]
|
||||
.map((sessionIndex, index) => ({
|
||||
paraIds: nextActions[1][index],
|
||||
sessionIndex
|
||||
}))
|
||||
.filter(({ paraIds }) => paraIds.length)
|
||||
: [],
|
||||
[nextActions]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useActionsQueue', useActionsQueueImpl);
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { AuctionIndex, BlockNumber, LeasePeriodOf } from '@pezkuwi/types/interfaces';
|
||||
import type { ITuple } from '@pezkuwi/types/types';
|
||||
import type { AuctionInfo } from './types.js';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
|
||||
const OPT_MULTI = {
|
||||
transform: ([numAuctions, optInfo]: [AuctionIndex, Option<ITuple<[LeasePeriodOf, BlockNumber]>>]): AuctionInfo => {
|
||||
const [leasePeriod, endBlock] = optInfo.unwrapOr([null, null]);
|
||||
|
||||
return {
|
||||
endBlock,
|
||||
leasePeriod,
|
||||
numAuctions
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function useAuctionInfoImpl (): AuctionInfo | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
return useCallMulti<AuctionInfo>([
|
||||
api.query.auctions?.auctionCounter,
|
||||
api.query.auctions?.auctionInfo
|
||||
], OPT_MULTI);
|
||||
}
|
||||
|
||||
export default createNamedHook('useAuctionInfo', useAuctionInfoImpl);
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, StorageKey } from '@pezkuwi/types';
|
||||
import type { AccountId, BalanceOf, BlockNumber, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeCommonCrowdloanFundInfo } from '@pezkuwi/types/lookup';
|
||||
import type { INumber, ITuple } from '@pezkuwi/types/types';
|
||||
import type { Campaign, Campaigns } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useBestNumber, useCall, useEventTrigger, useIsMountedRef, useMapKeys } from '@pezkuwi/react-hooks';
|
||||
import { BN, BN_ZERO, u8aConcat, u8aEq } from '@pezkuwi/util';
|
||||
import { encodeAddress } from '@pezkuwi/util-crypto';
|
||||
|
||||
import { CROWD_PREFIX } from './constants.js';
|
||||
|
||||
const EMPTY: Campaigns = {
|
||||
activeCap: BN_ZERO,
|
||||
activeRaised: BN_ZERO,
|
||||
funds: null,
|
||||
isLoading: true,
|
||||
totalCap: BN_ZERO,
|
||||
totalRaised: BN_ZERO
|
||||
};
|
||||
|
||||
const EMPTY_U8A = new Uint8Array(32);
|
||||
|
||||
function createAddress (id: INumber): Uint8Array {
|
||||
return u8aConcat(CROWD_PREFIX, id.toU8a(), EMPTY_U8A).subarray(0, 32);
|
||||
}
|
||||
|
||||
function hasCrowdloadPrefix (accountId: AccountId): boolean {
|
||||
return u8aEq(accountId.slice(0, CROWD_PREFIX.length), CROWD_PREFIX);
|
||||
}
|
||||
|
||||
function hasLease (paraId: ParaId, leased: ParaId[]): boolean {
|
||||
return leased.some((l) => l.eq(paraId));
|
||||
}
|
||||
|
||||
// map into a campaign
|
||||
function updateFund (bestNumber: BN, minContribution: BN, data: Campaign, leased: ParaId[]): Campaign {
|
||||
data.isCapped = data.info.cap.sub(data.info.raised).lt(minContribution);
|
||||
data.isEnded = bestNumber.gt(data.info.end);
|
||||
data.isWinner = hasLease(data.paraId, leased);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function isFundUpdated (bestNumber: BlockNumber, minContribution: BN, { info: { cap, end, raised }, paraId }: Campaign, leased: ParaId[], allPrev: Campaigns): boolean {
|
||||
const prev = allPrev.funds?.find((p) => p.paraId.eq(paraId));
|
||||
|
||||
return !prev ||
|
||||
(!prev.isEnded && bestNumber.gt(end)) ||
|
||||
(!prev.isCapped && cap.sub(raised).lt(minContribution)) ||
|
||||
(!prev.isWinner && hasLease(paraId, leased));
|
||||
}
|
||||
|
||||
function sortCampaigns (a: Campaign, b: Campaign): number {
|
||||
return a.isWinner !== b.isWinner
|
||||
? a.isWinner
|
||||
? -1
|
||||
: 1
|
||||
: a.isCapped !== b.isCapped
|
||||
? a.isCapped
|
||||
? -1
|
||||
: 1
|
||||
: a.isEnded !== b.isEnded
|
||||
? a.isEnded
|
||||
? 1
|
||||
: -1
|
||||
: 0;
|
||||
}
|
||||
|
||||
// compare the current campaigns against the previous, manually adding ending and calculating the new totals
|
||||
function createResult (bestNumber: BlockNumber, minContribution: BN, funds: Campaign[], leased: ParaId[], prev: Campaigns): Campaigns {
|
||||
const [activeRaised, activeCap, totalRaised, totalCap] = funds.reduce(([ar, ac, tr, tc], { info: { cap, end, raised }, isWinner }) => [
|
||||
(bestNumber.gt(end) || isWinner) ? ar : ar.iadd(raised),
|
||||
(bestNumber.gt(end) || isWinner) ? ac : ac.iadd(cap),
|
||||
tr.iadd(raised),
|
||||
tc.iadd(cap)
|
||||
], [new BN(0), new BN(0), new BN(0), new BN(0)]);
|
||||
const hasNewActiveCap = !prev.activeCap.eq(activeCap);
|
||||
const hasNewActiveRaised = !prev.activeRaised.eq(activeRaised);
|
||||
const hasNewTotalCap = !prev.totalCap.eq(totalCap);
|
||||
const hasNewTotalRaised = !prev.totalRaised.eq(totalRaised);
|
||||
const hasChanged =
|
||||
!prev.funds || prev.funds.length !== funds.length ||
|
||||
hasNewActiveCap || hasNewActiveRaised || hasNewTotalCap || hasNewTotalRaised ||
|
||||
funds.some((c) => isFundUpdated(bestNumber, minContribution, c, leased, prev));
|
||||
|
||||
if (!hasChanged) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
activeCap: hasNewActiveCap
|
||||
? activeCap
|
||||
: prev.activeCap,
|
||||
activeRaised: hasNewActiveRaised
|
||||
? activeRaised
|
||||
: prev.activeRaised,
|
||||
funds: funds
|
||||
.map((c) => updateFund(bestNumber, minContribution, c, leased))
|
||||
.sort(sortCampaigns),
|
||||
totalCap: hasNewTotalCap
|
||||
? totalCap
|
||||
: prev.totalCap,
|
||||
totalRaised: hasNewTotalRaised
|
||||
? totalRaised
|
||||
: prev.totalRaised
|
||||
};
|
||||
}
|
||||
|
||||
const OPT_FUNDS_MULTI = {
|
||||
transform: ([[paraIds], optFunds]: [[ParaId[]], Option<PezkuwiRuntimeCommonCrowdloanFundInfo>[]]): Campaign[] =>
|
||||
paraIds
|
||||
.map((paraId, i): [ParaId, PezkuwiRuntimeCommonCrowdloanFundInfo | null] => [paraId, optFunds[i].unwrapOr(null)])
|
||||
.filter((v): v is [ParaId, PezkuwiRuntimeCommonCrowdloanFundInfo] => !!v[1])
|
||||
.map(([paraId, info]): Campaign => ({
|
||||
accountId: encodeAddress(createAddress(paraId)),
|
||||
firstSlot: info.firstPeriod,
|
||||
info,
|
||||
isCrowdloan: true,
|
||||
key: paraId.toString(),
|
||||
lastSlot: info.lastPeriod,
|
||||
paraId,
|
||||
value: info.raised
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.info.end.cmp(b.info.end) ||
|
||||
a.info.firstPeriod.cmp(b.info.firstPeriod) ||
|
||||
a.info.lastPeriod.cmp(b.info.lastPeriod) ||
|
||||
a.paraId.cmp(b.paraId)
|
||||
),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
const OPT_LEASE = {
|
||||
transform: ([[paraIds], leases]: [[ParaId[]], Option<ITuple<[AccountId, BalanceOf]>>[][]]): ParaId[] =>
|
||||
paraIds.filter((_, i) =>
|
||||
leases[i]
|
||||
.map((o) => o.unwrapOr(null))
|
||||
.filter((v): v is ITuple<[AccountId, BalanceOf]> => !!v)
|
||||
.filter(([accountId]) => hasCrowdloadPrefix(accountId))
|
||||
.length !== 0
|
||||
),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
const OPT_FUNDS = {
|
||||
transform: (keys: StorageKey<[ParaId]>[]): ParaId[] =>
|
||||
keys.map(({ args: [paraId] }) => paraId)
|
||||
};
|
||||
|
||||
function useFundsImpl (): Campaigns {
|
||||
const { api } = useApi();
|
||||
const bestNumber = useBestNumber();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const trigger = useEventTrigger([api.events.crowdloan?.Created]);
|
||||
const paraIds = useMapKeys(api.query.crowdloan?.funds, [], OPT_FUNDS, trigger.blockHash);
|
||||
const campaigns = useCall<Campaign[]>(api.query.crowdloan?.funds.multi, [paraIds], OPT_FUNDS_MULTI);
|
||||
const leases = useCall<ParaId[]>(api.query.slots.leases.multi, [paraIds], OPT_LEASE);
|
||||
const [result, setResult] = useState<Campaigns>(EMPTY);
|
||||
|
||||
// here we manually add the actual ending status and calculate the totals
|
||||
useEffect((): void => {
|
||||
mountedRef.current && bestNumber && campaigns && leases && setResult((prev) =>
|
||||
createResult(bestNumber, api.consts.crowdloan.minContribution as BlockNumber, campaigns, leases, prev)
|
||||
);
|
||||
}, [api, bestNumber, campaigns, leases, mountedRef]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default createNamedHook('useFunds', useFundsImpl);
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BlockNumber } from '@pezkuwi/types/interfaces';
|
||||
import type { LeasePeriod } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useBestNumber } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
function useLeasePeriodImpl (): LeasePeriod | undefined {
|
||||
const { api } = useApi();
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
return useMemo((): LeasePeriod | undefined => {
|
||||
if (!api.consts.slots.leasePeriod || !bestNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const length = api.consts.slots.leasePeriod as BlockNumber;
|
||||
const startNumber = bestNumber.sub((api.consts.slots.leaseOffset as BlockNumber) || BN_ZERO);
|
||||
const progress = startNumber.mod(length);
|
||||
|
||||
return {
|
||||
currentPeriod: startNumber.div(length),
|
||||
length,
|
||||
progress,
|
||||
remainder: length.sub(progress)
|
||||
};
|
||||
}, [api, bestNumber]);
|
||||
}
|
||||
|
||||
export default createNamedHook('useLeasePeriod', useLeasePeriodImpl);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi } from '@pezkuwi/react-hooks';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
const RANGES_DEFAULT: [number, number][] = [
|
||||
[0, 0], [0, 1], [0, 2], [0, 3],
|
||||
[1, 1], [1, 2], [1, 3],
|
||||
[2, 2], [2, 3],
|
||||
[3, 3]
|
||||
];
|
||||
|
||||
function isU32 (leasePeriodsPerSlot: unknown): leasePeriodsPerSlot is u32 {
|
||||
return !!leasePeriodsPerSlot;
|
||||
}
|
||||
|
||||
function useLeaseRangesImpl (): [number, number][] {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMemo(
|
||||
(): [number, number][] => {
|
||||
if (isU32(api.consts.auctions?.leasePeriodsPerSlot)) {
|
||||
const ranges: [number, number][] = [];
|
||||
|
||||
for (let i = 0; api.consts.auctions.leasePeriodsPerSlot.gtn(i); i++) {
|
||||
for (let j = i; api.consts.auctions.leasePeriodsPerSlot.gtn(j); j++) {
|
||||
ranges.push([i, j]);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
return RANGES_DEFAULT;
|
||||
},
|
||||
[api]
|
||||
);
|
||||
}
|
||||
|
||||
export const useLeaseRanges = createNamedHook('useLeaseRanges', useLeaseRangesImpl);
|
||||
|
||||
function useLeaseRangeMaxImpl (): BN {
|
||||
const ranges = useLeaseRanges();
|
||||
|
||||
return useMemo(
|
||||
() => new BN(ranges[ranges.length - 1][1]),
|
||||
[ranges]
|
||||
);
|
||||
}
|
||||
|
||||
export const useLeaseRangeMax = createNamedHook('useLeaseRangeMax', useLeaseRangeMaxImpl);
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, StorageKey } from '@pezkuwi/types';
|
||||
import type { Hash, ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeCommonParasRegistrarParaInfo } from '@pezkuwi/types/lookup';
|
||||
import type { OwnedId, OwnedIdPartial } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall, useEventTrigger, useMapEntries } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface CodeHash {
|
||||
hash: Hash | null;
|
||||
paraId: ParaId;
|
||||
}
|
||||
|
||||
interface Owned {
|
||||
ids: ParaId[];
|
||||
owned: OwnedIdPartial[];
|
||||
}
|
||||
|
||||
const OPT_ENTRIES = {
|
||||
transform: (entries: [StorageKey<[ParaId]>, Option<PezkuwiRuntimeCommonParasRegistrarParaInfo>][]): Owned => {
|
||||
const owned = entries
|
||||
.map(([{ args: [paraId] }, optInfo]): OwnedIdPartial | null => {
|
||||
if (optInfo.isNone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const paraInfo = optInfo.unwrap();
|
||||
|
||||
return {
|
||||
manager: paraInfo.manager.toString(),
|
||||
paraId,
|
||||
paraInfo
|
||||
};
|
||||
})
|
||||
.filter((id): id is OwnedIdPartial => !!id);
|
||||
|
||||
return {
|
||||
ids: owned.map(({ paraId }) => paraId),
|
||||
owned
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const OPT_HASHES = {
|
||||
transform: ([[paraIds], optHashes]: [[ParaId[]], Option<Hash>[]]) =>
|
||||
paraIds.map((paraId, index): CodeHash => ({
|
||||
hash: optHashes[index].unwrapOr(null),
|
||||
paraId
|
||||
})),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function useOwnedIdsImpl (): OwnedId[] {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const trigger = useEventTrigger([
|
||||
api.events.registrar.Registered,
|
||||
api.events.registrar.Reserved
|
||||
]);
|
||||
const unfiltered = useMapEntries<Owned>(api.query.registrar.paras, [], OPT_ENTRIES, trigger.blockHash);
|
||||
const hashes = useCall(api.query.paras.currentCodeHash.multi, [unfiltered ? unfiltered.ids : []], OPT_HASHES);
|
||||
|
||||
return useMemo(
|
||||
() => unfiltered && hashes
|
||||
? unfiltered.owned
|
||||
.filter((id) => allAccounts.some((a) => a === id.manager))
|
||||
.map((data): OwnedId => ({
|
||||
...data,
|
||||
hasCode: hashes.some((h) => !!h.hash && h.paraId.eq(data.paraId))
|
||||
}))
|
||||
: [],
|
||||
[allAccounts, hashes, unfiltered]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useOwnedIds', useOwnedIdsImpl);
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey } from '@pezkuwi/types';
|
||||
import type { ParaId, SessionIndex } from '@pezkuwi/types/interfaces';
|
||||
import type { Proposals } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti, useEventTrigger, useIsMountedRef, useMapEntries, useMapKeys } from '@pezkuwi/react-hooks';
|
||||
|
||||
type MultiQuery = [SessionIndex | undefined, ParaId[] | undefined];
|
||||
|
||||
interface Scheduled {
|
||||
scheduledIds: ParaId[];
|
||||
sessionIndex: SessionIndex;
|
||||
}
|
||||
|
||||
const OPT_MULTI = {
|
||||
defaultValue: [undefined, undefined] as MultiQuery
|
||||
};
|
||||
|
||||
const OPT_IDS = {
|
||||
transform: (keys: StorageKey<[ParaId]>[]): ParaId[] =>
|
||||
keys.map(({ args: [id] }) => id)
|
||||
};
|
||||
|
||||
const OPT_SCHED = {
|
||||
transform: (entries: [StorageKey<[SessionIndex]>, ParaId[]][]): Scheduled[] =>
|
||||
entries.map(([{ args: [sessionIndex] }, scheduledIds]) => ({
|
||||
scheduledIds,
|
||||
sessionIndex
|
||||
}))
|
||||
};
|
||||
|
||||
function useProposalsImpl (): Proposals | undefined {
|
||||
const { api } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const trigger = useEventTrigger([api.events.proposeTeyrchain?.ProposeTeyrchain]);
|
||||
const proposalIds = useMapKeys(api.query.proposeTeyrchain?.proposals, [], OPT_IDS, trigger.blockHash);
|
||||
const scheduled = useMapEntries(api.query.proposeTeyrchain?.scheduledProposals, [], OPT_SCHED, trigger.blockHash);
|
||||
const [sessionIndex, approvedIds] = useCallMulti<MultiQuery>([
|
||||
api.query.session.currentIndex,
|
||||
api.query.proposeTeyrchain?.approvedProposals
|
||||
], OPT_MULTI);
|
||||
|
||||
return useMemo(
|
||||
() => approvedIds && sessionIndex && proposalIds && scheduled && mountedRef.current
|
||||
? {
|
||||
approvedIds,
|
||||
proposalIds,
|
||||
scheduled: scheduled.filter((s) => s.sessionIndex.gt(sessionIndex))
|
||||
}
|
||||
: undefined,
|
||||
[approvedIds, mountedRef, proposalIds, sessionIndex, scheduled]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useProposals', useProposalsImpl);
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, StorageKey } from '@pezkuwi/types';
|
||||
import type { ParaId } from '@pezkuwi/types/interfaces';
|
||||
import type { PezkuwiRuntimeTeyrchainsParasParaLifecycle } from '@pezkuwi/types/lookup';
|
||||
|
||||
import { createNamedHook, useApi, useEventTrigger, useMapEntries } from '@pezkuwi/react-hooks';
|
||||
|
||||
const OPT_ENTRIES = {
|
||||
transform: (entries: [StorageKey<[ParaId]>, Option<PezkuwiRuntimeTeyrchainsParasParaLifecycle>][]): ParaId[] =>
|
||||
entries
|
||||
.map(([{ args: [paraId] }, optValue]): ParaId | null => {
|
||||
const value = optValue.unwrapOr(null);
|
||||
|
||||
return value && (
|
||||
value.isParathread ||
|
||||
value.isUpgradingParathread ||
|
||||
value.isOffboardingParathread ||
|
||||
value.isOnboarding
|
||||
)
|
||||
? paraId
|
||||
: null;
|
||||
})
|
||||
.filter((paraId): paraId is ParaId => !!paraId)
|
||||
.sort((a, b) => a.cmp(b))
|
||||
};
|
||||
|
||||
function useUpomingIdsImpl (): ParaId[] | undefined {
|
||||
const { api } = useApi();
|
||||
const trigger = useEventTrigger([
|
||||
api.events.session.NewSession,
|
||||
api.events.registrar.Registered
|
||||
]);
|
||||
|
||||
return useMapEntries(api.query.paras.paraLifecycles, [], OPT_ENTRIES, trigger.blockHash);
|
||||
}
|
||||
|
||||
export default createNamedHook('useUpomingIds', useUpomingIdsImpl);
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, StorageKey } from '@pezkuwi/types';
|
||||
import type { BlockNumber, WinningData } from '@pezkuwi/types/interfaces';
|
||||
import type { AuctionInfo, WinnerData, Winning } from './types.js';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useBestNumber, useCall, useEventTrigger, useIsMountedRef } from '@pezkuwi/react-hooks';
|
||||
import { BN, BN_ONE, BN_ZERO, u8aEq } from '@pezkuwi/util';
|
||||
|
||||
import { CROWD_PREFIX } from './constants.js';
|
||||
import { useLeaseRanges } from './useLeaseRanges.js';
|
||||
|
||||
const FIRST_PARAM = [0];
|
||||
|
||||
function isNewWinners (a: WinnerData[], b: WinnerData[]): boolean {
|
||||
return JSON.stringify({ w: a }) !== JSON.stringify({ w: b });
|
||||
}
|
||||
|
||||
function isNewOrdering (a: WinnerData[], b: WinnerData[]): boolean {
|
||||
return a.length !== b.length ||
|
||||
a.some(({ firstSlot, lastSlot, paraId }, index) =>
|
||||
!paraId.eq(b[index].paraId) ||
|
||||
!firstSlot.eq(b[index].firstSlot) ||
|
||||
!lastSlot.eq(b[index].lastSlot)
|
||||
);
|
||||
}
|
||||
|
||||
function extractWinners (ranges: [number, number][], auctionInfo: AuctionInfo, optData: Option<WinningData>): WinnerData[] {
|
||||
return optData.isNone
|
||||
? []
|
||||
: optData.unwrap().reduce<WinnerData[]>((winners, optEntry, index): WinnerData[] => {
|
||||
if (optEntry.isSome) {
|
||||
const [accountId, paraId, value] = optEntry.unwrap();
|
||||
const period = auctionInfo.leasePeriod || BN_ZERO;
|
||||
const [first, last] = ranges[index];
|
||||
|
||||
winners.push({
|
||||
accountId: accountId.toString(),
|
||||
firstSlot: period.addn(first),
|
||||
isCrowdloan: u8aEq(CROWD_PREFIX, accountId.subarray(0, CROWD_PREFIX.length)),
|
||||
key: paraId.toString(),
|
||||
lastSlot: period.addn(last),
|
||||
paraId,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
return winners;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createWinning ({ endBlock }: AuctionInfo, blockOffset: BN | null | undefined, winners: WinnerData[]): Winning {
|
||||
return {
|
||||
blockNumber: endBlock && blockOffset
|
||||
? blockOffset.add(endBlock)
|
||||
: blockOffset || BN_ZERO,
|
||||
blockOffset: blockOffset || BN_ZERO,
|
||||
total: winners.reduce((total, { value }) => total.iadd(value), new BN(0)),
|
||||
winners
|
||||
};
|
||||
}
|
||||
|
||||
function extractData (ranges: [number, number][], auctionInfo: AuctionInfo, values: [StorageKey<[BlockNumber]>, Option<WinningData>][]): Winning[] {
|
||||
return values
|
||||
.sort(([{ args: [a] }], [{ args: [b] }]) => a.cmp(b))
|
||||
.reduce((all: Winning[], [{ args: [blockOffset] }, optData]): Winning[] => {
|
||||
const winners = extractWinners(ranges, auctionInfo, optData);
|
||||
|
||||
winners.length && (
|
||||
all.length === 0 ||
|
||||
isNewWinners(winners, all[all.length - 1].winners)
|
||||
) && all.push(createWinning(auctionInfo, blockOffset, winners));
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
}
|
||||
|
||||
function mergeCurrent (ranges: [number, number][], auctionInfo: AuctionInfo, prev: Winning[] | undefined, optCurrent: Option<WinningData>, blockOffset: BN): Winning[] | undefined {
|
||||
const current = createWinning(auctionInfo, blockOffset, extractWinners(ranges, auctionInfo, optCurrent));
|
||||
|
||||
if (current.winners.length) {
|
||||
if (!prev?.length) {
|
||||
return [current];
|
||||
}
|
||||
|
||||
if (isNewWinners(current.winners, prev[0].winners)) {
|
||||
if (isNewOrdering(current.winners, prev[0].winners)) {
|
||||
return [current, ...prev];
|
||||
}
|
||||
|
||||
prev[0] = current;
|
||||
|
||||
return [...prev];
|
||||
}
|
||||
}
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
function mergeFirst (ranges: [number, number][], auctionInfo: AuctionInfo, prev: Winning[] | undefined, optFirstData: Option<WinningData>): Winning[] | undefined {
|
||||
if (prev && prev.length <= 1) {
|
||||
const updated: Winning[] = prev || [];
|
||||
const firstEntry = createWinning(auctionInfo, null, extractWinners(ranges, auctionInfo, optFirstData));
|
||||
|
||||
if (!firstEntry.winners.length) {
|
||||
return updated;
|
||||
} else if (!updated.length) {
|
||||
return [firstEntry];
|
||||
}
|
||||
|
||||
updated[updated.length - 1] = firstEntry;
|
||||
|
||||
return updated.slice();
|
||||
}
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
function useWinningDataImpl (auctionInfo?: AuctionInfo): Winning[] | undefined {
|
||||
const { api } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const ranges = useLeaseRanges();
|
||||
const [result, setResult] = useState<Winning[] | undefined>();
|
||||
const bestNumber = useBestNumber();
|
||||
const trigger = useEventTrigger([api.events.auctions?.BidAccepted]);
|
||||
const triggerRef = useRef(trigger);
|
||||
const initialEntries = useCall<[StorageKey<[BlockNumber]>, Option<WinningData>][]>(api.query.auctions?.winning.entries);
|
||||
const optFirstData = useCall<Option<WinningData>>(api.query.auctions?.winning, FIRST_PARAM);
|
||||
|
||||
// should be fired once, all entries as an initial round
|
||||
useEffect((): void => {
|
||||
mountedRef.current && auctionInfo && initialEntries && setResult(
|
||||
extractData(ranges, auctionInfo, initialEntries)
|
||||
);
|
||||
}, [auctionInfo, initialEntries, mountedRef, ranges]);
|
||||
|
||||
// when block 0 changes, update (typically in non-ending-period, static otherwise)
|
||||
useEffect((): void => {
|
||||
mountedRef.current && auctionInfo && optFirstData && setResult((prev) =>
|
||||
mergeFirst(ranges, auctionInfo, prev, optFirstData)
|
||||
);
|
||||
}, [auctionInfo, optFirstData, mountedRef, ranges]);
|
||||
|
||||
// on a bid event, get the new entry (assuming the event really triggered, i.e. not just a block)
|
||||
// and add it to the list when not duplicated. Additionally we cleanup after ourselves when endBlock
|
||||
// gets cleared
|
||||
useEffect((): void => {
|
||||
if (auctionInfo?.endBlock && bestNumber && bestNumber.gt(auctionInfo.endBlock) && triggerRef.current !== trigger) {
|
||||
const blockOffset = bestNumber.sub(auctionInfo.endBlock).iadd(BN_ONE);
|
||||
|
||||
triggerRef.current = trigger;
|
||||
|
||||
api.query.auctions
|
||||
?.winning<Option<WinningData>>(blockOffset)
|
||||
.then((optCurrent) =>
|
||||
mountedRef.current && setResult((prev) =>
|
||||
mergeCurrent(ranges, auctionInfo, prev, optCurrent, blockOffset)
|
||||
)
|
||||
)
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [api, bestNumber, auctionInfo, mountedRef, ranges, trigger, triggerRef]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default createNamedHook('useWinningData', useWinningDataImpl);
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-teyrchains authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Codec } from '@pezkuwi/types/types';
|
||||
|
||||
export function sliceHex (value: Codec, max = 6): string {
|
||||
const hex = value.toHex();
|
||||
|
||||
return hex.length > ((2 * max) + 2)
|
||||
? `${hex.slice(0, max + 2)}…${hex.slice(-max)}`
|
||||
: hex === '0x'
|
||||
? ''
|
||||
: hex;
|
||||
}
|
||||
Reference in New Issue
Block a user