mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-14 07:31:09 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { Params } from './types.js';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, InputBalance, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { bnMax } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAmountError from './useAmountError.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
ownAccounts?: string[];
|
||||
params: Params;
|
||||
}
|
||||
|
||||
const MAX_META_LEN = 32;
|
||||
const MIN_META_LEN = 3;
|
||||
|
||||
function Create ({ className, isDisabled, ownAccounts, params: { minCreateBond, minNominatorBond, nextPoolId } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccount] = useState<string | null>(null);
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const [metadata, setMetadata] = useState('');
|
||||
|
||||
const minValue = useMemo(
|
||||
() => minCreateBond && minNominatorBond &&
|
||||
bnMax(minCreateBond, minNominatorBond, api.consts.balances.existentialDeposit),
|
||||
[api, minCreateBond, minNominatorBond]
|
||||
);
|
||||
|
||||
const isAmountError = useAmountError(accountId, amount, minValue);
|
||||
|
||||
const isMetaError = useMemo(
|
||||
() => !metadata || (metadata.length < MIN_META_LEN) || (metadata.length > MAX_META_LEN),
|
||||
[metadata]
|
||||
);
|
||||
|
||||
const extrinsic = useMemo(
|
||||
() => amount && accountId && !isAmountError && nextPoolId
|
||||
? api.tx.utility.batch([
|
||||
api.tx.nominationPools.create(amount, accountId, accountId, accountId),
|
||||
api.tx.nominationPools.setMetadata(nextPoolId, metadata)
|
||||
])
|
||||
: null,
|
||||
[api, accountId, amount, isAmountError, metadata, nextPoolId]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={isDisabled || !minValue}
|
||||
label={t('Add pool')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Create nomination pool')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The origin account will also be set as the pool admin, nominator and state toggler.')}>
|
||||
<InputAddress
|
||||
filter={ownAccounts}
|
||||
label={t('create pool from')}
|
||||
onChange={setAccount}
|
||||
type='account'
|
||||
value={accountId}
|
||||
withExclude
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The initial value to assign to the pool. It is set to the maximum of the minimum bond and the minimum nomination value.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={minValue}
|
||||
isError={isAmountError}
|
||||
label={t('initial value')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The metadata description to set for this pool')}>
|
||||
<Input
|
||||
isError={isMetaError}
|
||||
label={t('description')}
|
||||
maxLength={MAX_META_LEN}
|
||||
onChange={setMetadata}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The id that will be assigned to this nomination pool.')}>
|
||||
<InputNumber
|
||||
defaultValue={nextPoolId}
|
||||
isDisabled
|
||||
label={t('pool id')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='plus'
|
||||
isDisabled={!accountId || isAmountError || isMetaError}
|
||||
label={t('Create')}
|
||||
onStart={toggleOpen}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Create);
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { Params } from './types.js';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAmountError from './useAmountError.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
ownAccounts?: string[];
|
||||
params: Params;
|
||||
poolId: BN;
|
||||
}
|
||||
|
||||
function Join ({ className, isDisabled, ownAccounts, params: { minMemberBond }, poolId }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccount] = useState<string | null>(null);
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const isAmountError = useAmountError(accountId, amount, minMemberBond);
|
||||
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!minMemberBond}
|
||||
label={t('Join')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Join nomination pool')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The account that will join the pool.')}>
|
||||
<InputAddress
|
||||
filter={ownAccounts}
|
||||
label={t('join pool from')}
|
||||
onChange={setAccount}
|
||||
type='account'
|
||||
value={accountId}
|
||||
withExclude
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The initial value to assign to the pool. It is set to the minimum value required to join a pool.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={minMemberBond}
|
||||
isError={isAmountError}
|
||||
label={t('initial value')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!accountId || isAmountError}
|
||||
label={t('Join')}
|
||||
onStart={toggleOpen}
|
||||
params={[amount, poolId]}
|
||||
tx={api.tx.nominationPools.join}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Join);
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { MembersMapEntry, Params } from './types.js';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AddressMini, ExpandButton, ExpanderScroll, Spinner, styled, Table } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Join from './Join.js';
|
||||
import usePoolInfo from './usePoolInfo.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
members: MembersMapEntry[];
|
||||
ownAccounts?: string[];
|
||||
params: Params;
|
||||
poolId: BN;
|
||||
}
|
||||
|
||||
function Pool ({ className = '', members, ownAccounts, params, poolId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const info = usePoolInfo(poolId);
|
||||
const [isExpanded, toggleExpanded] = useToggle(false);
|
||||
|
||||
const renderMembers = useCallback(
|
||||
() => members.map(({ accountId, member }, count): React.ReactNode => (
|
||||
<AddressMini
|
||||
balance={member.points}
|
||||
key={`${count}:${accountId}`}
|
||||
value={accountId}
|
||||
withBalance
|
||||
withShrink
|
||||
/>
|
||||
)),
|
||||
[members]
|
||||
);
|
||||
|
||||
const renderNominees = useCallback(
|
||||
() => info?.nominating.map((stashId, count): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={`${count}:${stashId}`}
|
||||
value={stashId}
|
||||
withShrink
|
||||
/>
|
||||
)),
|
||||
[info]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTr className={`${className} isFirst isExpanded ${isExpanded ? '' : 'isLast'}`}>
|
||||
<Table.Column.Id value={poolId} />
|
||||
<td className='start'>
|
||||
<div className={`${isExpanded ? '' : 'clamp'}`}>
|
||||
{info
|
||||
? info.metadata
|
||||
: <span className='--tmp'>This is a pool placeholder</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className='number media--1100'>
|
||||
{info
|
||||
? info.bonded.state.type
|
||||
: <span className='--tmp'>Destroying</span>}
|
||||
</td>
|
||||
<Table.Column.Balance
|
||||
value={info?.bonded.points}
|
||||
withLoading
|
||||
/>
|
||||
<td className='number media--1400'>{info && !info.rewardClaimable.isZero() && <FormatBalance value={info.rewardClaimable} />}</td>
|
||||
<td className='number'>{info && info.bonded.commission.current.value && <div>{info.bonded.commission.current.value[0]?.toHuman()}</div>}</td>
|
||||
<td className='number'>
|
||||
{info && info.nominating.length !== 0 && (
|
||||
<ExpanderScroll
|
||||
className='media--1300'
|
||||
empty={t('No nominees')}
|
||||
renderChildren={renderNominees}
|
||||
summary={t('Nominees ({{count}})', { replace: { count: info.nominating.length } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{members && members.length !== 0 && (
|
||||
<ExpanderScroll
|
||||
className='media--1200'
|
||||
empty={t('No members')}
|
||||
renderChildren={renderMembers}
|
||||
summary={t('Members ({{count}})', { replace: { count: members.length } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='button'>
|
||||
{info
|
||||
? (
|
||||
<>
|
||||
<Join
|
||||
isDisabled={!info.bonded.state.isOpen || (!!params.maxMembersPerPool && !info.bonded.memberCounter.ltn(params.maxMembersPerPool))}
|
||||
ownAccounts={ownAccounts}
|
||||
params={params}
|
||||
poolId={poolId}
|
||||
/>
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleExpanded}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: <Spinner noLabel />
|
||||
}
|
||||
</td>
|
||||
</StyledTr>
|
||||
{info && isExpanded && (
|
||||
<StyledTr className={`${className} isExpanded isLast`}>
|
||||
<td colSpan={4}>
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('creator')}</div>
|
||||
<div className='inline-balance'><AddressMini value={info.bonded.roles.depositor} /></div>
|
||||
</div>
|
||||
{info.bonded.roles.root.isSome && (
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('root')}</div>
|
||||
<div className='inline-balance'><AddressMini value={info.bonded.roles.root.unwrap()} /></div>
|
||||
</div>
|
||||
)}
|
||||
{info.bonded.roles.nominator.isSome && (
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('nominator')}</div>
|
||||
<div className='inline-balance'><AddressMini value={info.bonded.roles.nominator.unwrap()} /></div>
|
||||
</div>
|
||||
)}
|
||||
{(info.bonded.roles as { stateToggler?: { isSome: boolean } }).stateToggler?.isSome && (
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('toggler')}</div>
|
||||
<div className='inline-balance'><AddressMini value={(info.bonded.roles as unknown as { stateToggler: { unwrap: () => string } }).stateToggler.unwrap()} /></div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td colSpan={4}>
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('stash')}</div>
|
||||
<div className='inline-balance'><AddressMini value={info.stashId} /></div>
|
||||
</div>
|
||||
<div className='label-column-right'>
|
||||
<div className='label'>{t('rewards')}</div>
|
||||
<div className='inline-balance'><AddressMini value={info.rewardId} /></div>
|
||||
</div>
|
||||
</td>
|
||||
</StyledTr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
.label-column-right,
|
||||
.label-column-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: right;
|
||||
padding: 0 1.7rem 0 0;
|
||||
line-height: normal;
|
||||
color: var(--color-label);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.clamp {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
box-orient: vertical;
|
||||
display: -webkit-box;
|
||||
line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Pool);
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { OwnPool, Params } from './types.js';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Table, ToggleGroup } from '@pezkuwi/react-components';
|
||||
import { arrayFlatten } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Create from './Create.js';
|
||||
import Pool from './Pool.js';
|
||||
import useMembers from './useMembers.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ids?: BN[];
|
||||
ownPools?: OwnPool[];
|
||||
params: Params;
|
||||
}
|
||||
|
||||
function Pools ({ className, ids, ownPools, params }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const membersMap = useMembers();
|
||||
const [typeIndex, setTypeIndex] = useState(() => ownPools?.length ? 0 : 1);
|
||||
|
||||
const ownAccounts = useMemo(
|
||||
() => ownPools && arrayFlatten(ownPools.map(({ members }) => Object.keys(members))),
|
||||
[ownPools]
|
||||
);
|
||||
|
||||
const noCreate = useMemo(
|
||||
() => !ids || (!!params.maxPools && (ids.length > params.maxPools)),
|
||||
[ids, params]
|
||||
);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => ownPools && ids
|
||||
? typeIndex
|
||||
? ids
|
||||
: ids.filter((id) => ownPools.some(({ poolId }) => id.eq(poolId)))
|
||||
: undefined,
|
||||
[ids, ownPools, typeIndex]
|
||||
);
|
||||
|
||||
const header = useMemo<[React.ReactNode?, string?, number?][]>(
|
||||
() => [
|
||||
[t('pools'), 'start', 2],
|
||||
[t('state'), 'media--1100'],
|
||||
[t('points')],
|
||||
[t('claimable'), 'media--1400'],
|
||||
[t('commission')],
|
||||
[undefined, undefined, 3]
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const poolTypes = useRef([
|
||||
{ text: t('Own pools'), value: 'mine' },
|
||||
{ text: t('All pools'), value: 'all' }
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button.Group>
|
||||
<ToggleGroup
|
||||
onChange={setTypeIndex}
|
||||
options={poolTypes.current}
|
||||
value={typeIndex}
|
||||
/>
|
||||
<Create
|
||||
isDisabled={noCreate}
|
||||
ownAccounts={ownAccounts}
|
||||
params={params}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Table
|
||||
className={className}
|
||||
empty={membersMap && filtered && t('No available nomination pools')}
|
||||
emptySpinner={t('Retrieving nomination pools')}
|
||||
header={header}
|
||||
>
|
||||
{membersMap && filtered?.map((poolId) => (
|
||||
<Pool
|
||||
key={poolId.toString()}
|
||||
members={membersMap[poolId.toString()]}
|
||||
ownAccounts={ownAccounts}
|
||||
params={params}
|
||||
poolId={poolId}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Pools);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Params } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { formatNumber, isNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
params: Params;
|
||||
poolCount?: number;
|
||||
}
|
||||
|
||||
function Summary ({ className, params: { maxMembers, maxMembersPerPool, maxPools }, poolCount }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('pools')}>
|
||||
{isNumber(poolCount) && (
|
||||
<>
|
||||
{formatNumber(poolCount)}
|
||||
{maxPools > 0 && (
|
||||
<> / {formatNumber(maxPools)}</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardSummary>
|
||||
<section>
|
||||
{maxMembers > 0 && (
|
||||
<CardSummary label={t('max. members')}>
|
||||
{formatNumber(maxMembers)}
|
||||
</CardSummary>
|
||||
)}
|
||||
{maxMembersPerPool > 0 && (
|
||||
<CardSummary label={t('max. members / pool')}>
|
||||
{formatNumber(maxMembersPerPool)}
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { OwnPool } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Pools from './Pools.js';
|
||||
import Summary from './Summary.js';
|
||||
import useOwnPools from './useOwnPools.js';
|
||||
import useParams from './useParams.js';
|
||||
import usePoolIds from './usePoolIds.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ownPools?: OwnPool[];
|
||||
}
|
||||
|
||||
function NominationPools ({ className, ownPools: ownPoolProp }: Props): React.ReactElement<Props> {
|
||||
const ids = usePoolIds();
|
||||
const ownPools = useOwnPools();
|
||||
const params = useParams();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary
|
||||
params={params}
|
||||
poolCount={ids?.length}
|
||||
/>
|
||||
<Pools
|
||||
ids={ids}
|
||||
ownPools={ownPools || ownPoolProp}
|
||||
params={params}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(NominationPools);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
import type { PalletNominationPoolsBondedPoolInner, PalletNominationPoolsPoolMember, PalletNominationPoolsRewardPool } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export interface PoolAccounts {
|
||||
rewardId: string;
|
||||
stashId: string;
|
||||
}
|
||||
|
||||
export interface OwnPoolBase {
|
||||
members: Record<string, PalletNominationPoolsPoolMember>;
|
||||
poolId: u32;
|
||||
}
|
||||
|
||||
export interface OwnPool extends OwnPoolBase, PoolAccounts {
|
||||
// nothing additional, only combined
|
||||
}
|
||||
|
||||
export interface Params {
|
||||
lastPoolId: BN;
|
||||
maxMembers: number;
|
||||
maxMembersPerPool: number;
|
||||
maxPools: number;
|
||||
minCreateBond?: BN;
|
||||
minJoinBond?: BN;
|
||||
minMemberBond?: BN;
|
||||
minNominatorBond?: BN;
|
||||
nextPoolId: BN;
|
||||
}
|
||||
|
||||
export interface PoolInfoBase {
|
||||
bonded: PalletNominationPoolsBondedPoolInner;
|
||||
reward: PalletNominationPoolsRewardPool;
|
||||
metadata: string | null;
|
||||
nominating: string[];
|
||||
rewardClaimable: BN;
|
||||
}
|
||||
|
||||
export interface PoolInfo extends PoolInfoBase, PoolAccounts {
|
||||
// nothing extra
|
||||
}
|
||||
|
||||
export interface MembersMapEntry {
|
||||
accountId: string;
|
||||
member: PalletNominationPoolsPoolMember;
|
||||
}
|
||||
|
||||
export type MembersMap = Record<string, MembersMapEntry[]>;
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveBalancesAll } from '@pezkuwi/api-derive/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO, hexToString } from '@pezkuwi/util';
|
||||
|
||||
// Consider only OpenGov-related locks
|
||||
const openGovLockIds = ['referenda', 'convictionVoting', 'pyconvot'];
|
||||
|
||||
function useAmountErrorImpl (accountId?: string | null, amount?: BN | null, minAmount?: BN | null): boolean {
|
||||
const { api } = useApi();
|
||||
const balances = useCall<DeriveBalancesAll>(!!accountId && api.derive.balances.all, [accountId]);
|
||||
|
||||
return useMemo(
|
||||
() => {
|
||||
if (!amount || amount.isZero() || !minAmount || minAmount.gt(amount) || !balances) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Filter out OpenGov-related locks (those that don't prevent staking)
|
||||
const openGovLocks = balances.lockedBreakdown.filter((lock) => {
|
||||
return openGovLockIds.includes(hexToString(lock.id.toHex()));
|
||||
});
|
||||
|
||||
// Total locked amount that affects staking
|
||||
const openGovLockedBalance = openGovLocks.reduce((acc, lock) => acc.add(lock.amount), BN_ZERO);
|
||||
|
||||
const usableBalance = (balances.transferable || balances.availableBalance)
|
||||
.add(openGovLockedBalance)
|
||||
.sub(api.consts.balances.existentialDeposit);
|
||||
|
||||
return amount.gt(usableBalance);
|
||||
},
|
||||
[api, amount, balances, minAmount]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useAmountError', useAmountErrorImpl);
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Changes } from '@pezkuwi/react-hooks/useEventChanges';
|
||||
import type { bool, Option, StorageKey, u32, u128 } from '@pezkuwi/types';
|
||||
import type { AccountId32, EventRecord } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletNominationPoolsPoolMember } from '@pezkuwi/types/lookup';
|
||||
import type { MembersMap, MembersMapEntry } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall, useEventChanges, useMapEntries } from '@pezkuwi/react-hooks';
|
||||
|
||||
const EMPTY_START: AccountId32[] = [];
|
||||
|
||||
const OPT_ENTRIES = {
|
||||
transform: (entries: [StorageKey<[AccountId32]>, Option<PalletNominationPoolsPoolMember>][]): MembersMap =>
|
||||
entries.reduce((all: MembersMap, [{ args: [accountId] }, optMember]) => {
|
||||
if (optMember.isSome) {
|
||||
const member = optMember.unwrap();
|
||||
const poolId = member.poolId.toString();
|
||||
|
||||
if (!all[poolId]) {
|
||||
all[poolId] = [];
|
||||
}
|
||||
|
||||
all[poolId].push({
|
||||
accountId: accountId.toString(),
|
||||
member
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
}, {})
|
||||
};
|
||||
|
||||
const OPT_MULTI = {
|
||||
transform: ([[ids], values]: [[AccountId32[]], Option<PalletNominationPoolsPoolMember>[]]): MembersMapEntry[] =>
|
||||
ids
|
||||
.filter((_, i) => values[i].isSome)
|
||||
.map((accountId, i) => ({
|
||||
accountId: accountId.toString(),
|
||||
member: values[i].unwrap()
|
||||
})),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function filterEvents (records: EventRecord[]): Changes<AccountId32> {
|
||||
const added: AccountId32[] = [];
|
||||
const removed: AccountId32[] = [];
|
||||
|
||||
records.forEach(({ event: { data, method } }): void => {
|
||||
if (method === 'Bonded') {
|
||||
const [accountId,,, joined] = data as unknown as [AccountId32, u32, u128, bool];
|
||||
|
||||
if (joined.isTrue) {
|
||||
added.push(accountId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
function interleave (prev: MembersMap, additions: MembersMapEntry[]): MembersMap {
|
||||
return additions.reduce<MembersMap>((all, entry) => {
|
||||
const poolId = entry.member.poolId.toString();
|
||||
const arr: MembersMapEntry[] = [];
|
||||
|
||||
if (all[poolId]) {
|
||||
all[poolId].forEach((prev): void => {
|
||||
if (prev.accountId !== entry.accountId) {
|
||||
arr.push(prev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
arr.push(entry);
|
||||
|
||||
all[poolId] = arr;
|
||||
|
||||
return all;
|
||||
}, { ...prev });
|
||||
}
|
||||
|
||||
function useMembersImpl (): MembersMap | undefined {
|
||||
const { api } = useApi();
|
||||
const [membersMap, setMembersMap] = useState<MembersMap | undefined>();
|
||||
const queryMap = useMapEntries(api.query.nominationPools.poolMembers, [], OPT_ENTRIES);
|
||||
const ids = useEventChanges([
|
||||
api.events.nominationPools.Bonded
|
||||
], filterEvents, EMPTY_START);
|
||||
const additions = useCall(ids && ids.length !== 0 && api.query.nominationPools.poolMembers.multi, [ids], OPT_MULTI);
|
||||
|
||||
// initial entries
|
||||
useEffect((): void => {
|
||||
queryMap && setMembersMap(queryMap);
|
||||
}, [queryMap]);
|
||||
|
||||
// additions via events
|
||||
useEffect((): void => {
|
||||
additions && setMembersMap((prev) => prev && interleave(prev, additions));
|
||||
}, [additions]);
|
||||
|
||||
return membersMap;
|
||||
}
|
||||
|
||||
export default createNamedHook('useMembers', useMembersImpl);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { PalletNominationPoolsPoolMember } from '@pezkuwi/types/lookup';
|
||||
import type { OwnPool, OwnPoolBase } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { createAccounts } from './usePoolAccounts.js';
|
||||
|
||||
const OPT_MULTI = {
|
||||
transform: ([[ids], opts]: [[string[]], Option<PalletNominationPoolsPoolMember>[]]): OwnPoolBase[] => {
|
||||
const pools: OwnPoolBase[] = [];
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
if (opts[i].isSome) {
|
||||
const member = opts[i].unwrap();
|
||||
let entry = pools.find(({ poolId }) => poolId.eq(member.poolId));
|
||||
|
||||
if (!entry) {
|
||||
entry = { members: {}, poolId: member.poolId };
|
||||
|
||||
pools.push(entry);
|
||||
}
|
||||
|
||||
entry.members[ids[i]] = member;
|
||||
}
|
||||
}
|
||||
|
||||
return pools;
|
||||
},
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function useOwnPoolsImpl (): OwnPool[] | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const base = useCall(api.query.nominationPools?.poolMembers.multi, [allAccounts], OPT_MULTI);
|
||||
|
||||
return useMemo(
|
||||
() => base?.map((base) => ({
|
||||
...base,
|
||||
...createAccounts(api, base.poolId)
|
||||
})),
|
||||
[api, base]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useOwnPools', useOwnPoolsImpl);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, u32 } from '@pezkuwi/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { Params } from './types.js';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
const OPT_MULTI = {
|
||||
defaultValue: {
|
||||
lastPoolId: BN_ZERO,
|
||||
maxMembers: 0,
|
||||
maxMembersPerPool: 0,
|
||||
maxPools: 0,
|
||||
nextPoolId: BN_ONE
|
||||
},
|
||||
transform: ([lastPoolId, maxPoolMembers, maxPoolMembersPerPool, maxPools, minCreateBond, minJoinBond, minNominatorBond]: [BN, Option<u32>, Option<u32>, Option<u32>, BN, BN, BN]): Params => ({
|
||||
lastPoolId,
|
||||
maxMembers: maxPoolMembers.unwrapOr(BN_ZERO).toNumber(),
|
||||
maxMembersPerPool: maxPoolMembersPerPool.unwrapOr(BN_ZERO).toNumber(),
|
||||
maxPools: maxPools.unwrapOr(BN_ZERO).toNumber(),
|
||||
minCreateBond,
|
||||
minJoinBond,
|
||||
minMemberBond: minJoinBond,
|
||||
minNominatorBond,
|
||||
nextPoolId: lastPoolId.add(BN_ONE)
|
||||
})
|
||||
};
|
||||
|
||||
function useParamsImpl (): Params {
|
||||
const { api } = useApi();
|
||||
|
||||
return useCallMulti<Params>([
|
||||
api.query.nominationPools.lastPoolId,
|
||||
api.query.nominationPools.maxPoolMembers,
|
||||
api.query.nominationPools.maxPoolMembersPerPool,
|
||||
api.query.nominationPools.maxPools,
|
||||
api.query.nominationPools.minCreateBond,
|
||||
api.query.nominationPools.minJoinBond,
|
||||
api.query.staking.minNominatorBond
|
||||
], OPT_MULTI);
|
||||
}
|
||||
|
||||
export default createNamedHook('useParams', useParamsImpl);
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { PoolAccounts } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi } from '@pezkuwi/react-hooks';
|
||||
import { bnToU8a, stringToU8a, u8aConcat } from '@pezkuwi/util';
|
||||
|
||||
const EMPTY_H256 = new Uint8Array(32);
|
||||
const MOD_PREFIX = stringToU8a('modl');
|
||||
const U32_OPTS = { bitLength: 32, isLe: true };
|
||||
|
||||
function createAccount (api: ApiPromise, palletId: Uint8Array, poolId: BN, index: number): string {
|
||||
return api.registry.createType('AccountId32', u8aConcat(
|
||||
MOD_PREFIX,
|
||||
palletId,
|
||||
new Uint8Array([index]),
|
||||
bnToU8a(poolId, U32_OPTS),
|
||||
EMPTY_H256
|
||||
)).toString();
|
||||
}
|
||||
|
||||
export function createAccounts (api: ApiPromise, poolId: BN): PoolAccounts {
|
||||
const palletId = api.consts.nominationPools.palletId.toU8a();
|
||||
|
||||
return {
|
||||
rewardId: createAccount(api, palletId, poolId, 1),
|
||||
stashId: createAccount(api, palletId, poolId, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function usePoolAccountsImpl (poolId: BN): PoolAccounts {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMemo(
|
||||
() => createAccounts(api, poolId),
|
||||
[api, poolId]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('usePoolAccounts', usePoolAccountsImpl);
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Changes } from '@pezkuwi/react-hooks/useEventChanges';
|
||||
import type { StorageKey, u32 } from '@pezkuwi/types';
|
||||
import type { EventRecord } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@pezkuwi/react-hooks';
|
||||
|
||||
const OPT_KEYS = {
|
||||
transform: (keys: StorageKey<[u32]>[]): u32[] =>
|
||||
keys
|
||||
.map(({ args: [poolId] }) => poolId)
|
||||
.sort((a, b) => a.cmp(b))
|
||||
};
|
||||
|
||||
function filterEvents (records: EventRecord[]): Changes<u32> {
|
||||
const added: u32[] = [];
|
||||
const removed: u32[] = [];
|
||||
|
||||
records.forEach(({ event: { data, method } }): void => {
|
||||
if (method === 'Created') {
|
||||
added.push(data[1] as u32);
|
||||
} else {
|
||||
removed.push(data[0] as u32);
|
||||
}
|
||||
});
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
function usePoolIdsImpl (): u32[] | undefined {
|
||||
const { api } = useApi();
|
||||
const startValue = useMapKeys(api.query.nominationPools.bondedPools, [], OPT_KEYS);
|
||||
|
||||
return useEventChanges([
|
||||
api.events.nominationPools.Created,
|
||||
api.events.nominationPools.Destroyed
|
||||
], filterEvents, startValue);
|
||||
}
|
||||
|
||||
export default createNamedHook('usePoolIds', usePoolIdsImpl);
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Bytes, Option } from '@pezkuwi/types';
|
||||
import type { FrameSystemAccountInfo, PalletNominationPoolsBondedPoolInner, PalletNominationPoolsRewardPool, PalletStakingNominations } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { PoolInfo, PoolInfoBase } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO, bnMax } from '@pezkuwi/util';
|
||||
|
||||
import usePoolAccounts from './usePoolAccounts.js';
|
||||
|
||||
const OPT_MULTI = {
|
||||
defaultValue: null,
|
||||
transform: ([optBonded, metadata, optReward, optNominating, accountInfo]: [Option<PalletNominationPoolsBondedPoolInner>, Bytes, Option<PalletNominationPoolsRewardPool>, Option<PalletStakingNominations>, FrameSystemAccountInfo]): PoolInfoBase | null =>
|
||||
optBonded.isSome && optReward.isSome
|
||||
? {
|
||||
bonded: optBonded.unwrap(),
|
||||
metadata: metadata.length
|
||||
? transformName(
|
||||
metadata.isUtf8
|
||||
? metadata.toUtf8()
|
||||
: metadata.toString()
|
||||
)
|
||||
: null,
|
||||
nominating: optNominating
|
||||
.unwrapOr({ targets: [] })
|
||||
.targets.map((n) => n.toString()),
|
||||
reward: optReward.unwrap(),
|
||||
rewardClaimable: accountInfo.data.free
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
function transformName (input: string): string {
|
||||
return input.replace(/[^\x20-\x7E]/g, '');
|
||||
}
|
||||
|
||||
function usePoolInfoImpl (poolId: BN): PoolInfo | null | undefined {
|
||||
const { api } = useApi();
|
||||
const accounts = usePoolAccounts(poolId);
|
||||
const baseInfo = useCallMulti([
|
||||
[api.query.nominationPools.bondedPools, poolId],
|
||||
[api.query.nominationPools.metadata, poolId],
|
||||
[api.query.nominationPools.rewardPools, poolId],
|
||||
[api.query.staking.nominators, accounts.stashId],
|
||||
[api.query.system.account, accounts.rewardId]
|
||||
], OPT_MULTI);
|
||||
|
||||
return useMemo(
|
||||
() => baseInfo && {
|
||||
...accounts,
|
||||
...baseInfo,
|
||||
rewardClaimable: bnMax(BN_ZERO, baseInfo.rewardClaimable.sub(api.consts.balances.existentialDeposit))
|
||||
},
|
||||
[api, baseInfo, accounts]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('usePoolInfo', usePoolInfoImpl);
|
||||
Reference in New Issue
Block a user