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:
2026-01-08 16:24:19 +03:00
parent e64f846b0d
commit 7a4bbeac25
570 changed files with 3281 additions and 3030 deletions
@@ -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}
/>&nbsp;/&nbsp;<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>&nbsp;&nbsp;&nbsp;
</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
/>
&nbsp;/&nbsp;
<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
/>
&nbsp;/&nbsp;
<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 && <>&nbsp;{`(${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);
+281
View File
@@ -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');
+130
View File
@@ -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');
}
+103
View File
@@ -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);
+176
View File
@@ -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);
+14
View File
@@ -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;
}