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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
+124
View File
@@ -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);
+88
View File
@@ -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);
+188
View File
@@ -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 && (
<>&nbsp;/&nbsp;{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);
+51
View File
@@ -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);