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
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
export const AddressIdentityOtherDiscordKey = 'Discord';
export enum CoreTimeTypes {
'Reservation',
'Lease',
'Bulk Coretime',
'On Demand'
}
export const ChainRenewalStatus = {
Eligible: 'eligible',
None: '-',
Renewed: 'renewed'
};
// block time on coretime chain is 2 x slower than on relay chain
export const BlockTimeCoretimeToRelayConstant = 2;
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
export function createNamedHook <F extends (...args: any[]) => any> (name: string, fn: F): (...args: Parameters<F>) => ReturnType<F> {
return (...args: Parameters<F>): ReturnType<F> => {
try {
// eslint-disable-next-line
return fn(...args);
} catch (error) {
throw new Error(`${name}:: ${(error as Error).message}:: ${(error as Error).stack || '<unknown>'}`);
}
};
}
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
type SetStateContext = undefined | (([address, onUpdateName]: [string | null, (() => void) | null]) => void);
export const AccountSidebarCtx = React.createContext<SetStateContext>(undefined);
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiProps } from './types.js';
import React from 'react';
export const ApiCtx = React.createContext<ApiProps>({} as unknown as ApiProps);
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { ProviderStats } from '@pezkuwi/rpc-provider/types';
import type { ApiStats } from './types.js';
import React, { useCallback } from 'react';
import { useApi } from '../useApi.js';
import { useTimer } from '../useTimer.js';
interface Props {
children?: React.ReactNode;
}
const MAX_NUM = 60; // 5 minutes
const INTERVAL = 5_000;
const EMPTY_STATE: ApiStats[] = [];
function getStats (...apis: ApiPromise[]): { stats: ProviderStats, when: number } {
const stats: ProviderStats = {
active: {
requests: 0,
subscriptions: 0
},
total: {
bytesRecv: 0,
bytesSent: 0,
cached: 0,
errors: 0,
requests: 0,
subscriptions: 0,
timeout: 0
}
};
for (let i = 0, count = apis.length; i < count; i++) {
const s = apis[i].stats;
if (s) {
stats.active.requests += s.active.requests;
stats.active.subscriptions += s.active.subscriptions;
stats.total.bytesRecv += s.total.bytesRecv;
stats.total.bytesSent += s.total.bytesSent;
stats.total.cached += s.total.cached;
stats.total.errors += s.total.errors;
stats.total.requests += s.total.requests;
stats.total.subscriptions += s.total.subscriptions;
stats.total.timeout += s.total.timeout;
}
}
return {
stats,
when: Date.now()
};
}
function mergeStats (curr: ApiStats, prev: ApiStats[]): ApiStats[] {
return prev.length === 0
? [curr]
: prev.length === MAX_NUM
? prev.concat(curr).slice(-MAX_NUM)
: prev.concat(curr);
}
export const ApiStatsCtx = React.createContext<ApiStats[]>(EMPTY_STATE);
export function ApiStatsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const stateFn = useCallback(
(prev: ApiStats[]): ApiStats[] =>
mergeStats(getStats(api), prev),
[api]
);
const stats = useTimer(stateFn, EMPTY_STATE, INTERVAL);
return (
<ApiStatsCtx.Provider value={stats}>
{children}
</ApiStatsCtx.Provider>
);
}
@@ -0,0 +1,104 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { EraRewardPoints } from '@pezkuwi/types/interfaces';
import type { AugmentedBlockHeader, BlockAuthors } from './types.js';
import React, { useEffect, useState } from 'react';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import { formatNumber } from '@pezkuwi/util';
interface Props {
children: React.ReactNode;
}
const MAX_HEADERS = 75;
const byAuthor: Record<string, string> = {};
const eraPoints: Record<string, string> = {};
const EMPTY_STATE: BlockAuthors = { byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] };
export const BlockAuthorsCtx = React.createContext<BlockAuthors>(EMPTY_STATE);
export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const queryPoints = useCall<EraRewardPoints>(isApiReady && api.derive.staking?.currentPoints);
const [state, setState] = useState<BlockAuthors>(EMPTY_STATE);
// No unsub, global context - destroyed on app close
useEffect((): void => {
api.isReady.then((): void => {
let lastHeaders: AugmentedBlockHeader[] = [];
let lastBlockAuthors: string[] = [];
let lastBlockNumber = '';
// subscribe to new headers
api.derive.chain.subscribeNewHeads(async (header: HeaderExtended): Promise<void> => {
if (header?.number) {
const timestamp = await ((await api.at(header.hash)).query.timestamp.now());
const lastHeader = Object.assign(header, { timestamp }) as AugmentedBlockHeader;
const blockNumber = lastHeader.number.unwrap();
let thisBlockAuthor = '';
if (lastHeader.author) {
thisBlockAuthor = lastHeader.author.toString();
}
const thisBlockNumber = formatNumber(blockNumber);
if (thisBlockAuthor) {
byAuthor[thisBlockAuthor] = thisBlockNumber;
if (thisBlockNumber !== lastBlockNumber) {
lastBlockNumber = thisBlockNumber;
lastBlockAuthors = [thisBlockAuthor];
} else {
lastBlockAuthors.push(thisBlockAuthor);
}
}
lastHeaders = lastHeaders
.filter((old, index) => index < MAX_HEADERS && old.number.unwrap().lt(blockNumber))
.reduce((next, header): AugmentedBlockHeader[] => {
next.push(header);
return next;
}, [lastHeader])
.sort((a, b) => b.number.unwrap().cmp(a.number.unwrap()));
setState({ byAuthor, eraPoints, lastBlockAuthors: lastBlockAuthors.slice(), lastBlockNumber, lastHeader, lastHeaders });
}
}).catch(console.error);
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect((): void => {
if (queryPoints) {
const entries = [...queryPoints.individual.entries()]
.map(([accountId, points]) => [accountId.toString(), formatNumber(points)]);
const current = Object.keys(eraPoints);
// we have an update, clear all previous
if (current.length !== entries.length) {
current.forEach((accountId): void => {
delete eraPoints[accountId];
});
}
entries.forEach(([accountId, points]): void => {
eraPoints[accountId] = points;
});
}
}, [queryPoints]);
return (
<BlockAuthorsCtx.Provider value={state}>
{children}
</BlockAuthorsCtx.Provider>
);
}
@@ -0,0 +1,119 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Vec } from '@pezkuwi/types';
import type { EventRecord } from '@pezkuwi/types/interfaces';
import type { BlockEvents, IndexedEvent, KeyedEvent } from './types.js';
import React, { useEffect, useRef, useState } from 'react';
import { stringify, stringToU8a } from '@pezkuwi/util';
import { xxhashAsHex } from '@pezkuwi/util-crypto';
import { useApi } from '../useApi.js';
import { useCall } from '../useCall.js';
interface Props {
children: React.ReactNode;
}
interface PrevHashes {
block: string | null;
event: string | null;
}
const DEFAULT_EVENTS: BlockEvents = { eventCount: 0, events: [] };
const MAX_EVENTS = 75;
export const BlockEventsCtx = React.createContext<BlockEvents>(DEFAULT_EVENTS);
async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec<EventRecord>, setState: React.Dispatch<React.SetStateAction<BlockEvents>>): Promise<void> {
const newEvents: IndexedEvent[] = records
.map((record, index) => ({ indexes: [index], record }))
.filter(({ record: { event: { method, section } } }) =>
section !== 'system' &&
(
!['balances', 'treasury'].includes(section) ||
!['Deposit', 'UpdatedInactive', 'Withdraw'].includes(method)
) &&
(
!['transactionPayment'].includes(section) ||
!['TransactionFeePaid'].includes(method)
) &&
(
!['paraInclusion', 'parasInclusion', 'inclusion'].includes(section) ||
!['CandidateBacked', 'CandidateIncluded'].includes(method)
) &&
(
!['relayChainInfo'].includes(section) ||
!['CurrentBlockNumbers'].includes(method)
)
)
.reduce((combined: IndexedEvent[], e): IndexedEvent[] => {
const prev = combined.find(({ record: { event: { method, section } } }) =>
e.record.event.section === section &&
e.record.event.method === method
);
if (prev) {
prev.indexes.push(...e.indexes);
} else {
combined.push(e);
}
return combined;
}, [])
.reverse();
const newEventHash = xxhashAsHex(stringToU8a(stringify(newEvents)));
if (newEventHash !== prev.event && newEvents.length) {
prev.event = newEventHash;
// retrieve the last header, this will map to the current state
const header = await api.rpc.chain.getHeader(records.createdAtHash);
const blockNumber = header.number.unwrap();
const blockHash = header.hash.toHex();
if (blockHash !== prev.block) {
prev.block = blockHash;
setState(({ events }) => ({
eventCount: records.length,
events: [
...newEvents.map(({ indexes, record }): KeyedEvent => ({
blockHash,
blockNumber,
indexes,
key: `${blockNumber.toNumber()}-${blockHash}-${indexes.join('.')}`,
record
})),
// remove all events for the previous same-height blockNumber
...events.filter((p) => !p.blockNumber?.eq(blockNumber))
].slice(0, MAX_EVENTS)
}));
}
} else {
setState(({ events }) => ({
eventCount: records.length,
events
}));
}
}
export function BlockEventsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const [state, setState] = useState<BlockEvents>(DEFAULT_EVENTS);
const records = useCall<Vec<EventRecord>>(isApiReady && api.query.system.events);
const prevHashes = useRef({ block: null, event: null });
useEffect((): void => {
records && manageEvents(api, prevHashes.current, records, setState).catch(console.error);
}, [api, prevHashes, records, setState]);
return (
<BlockEventsCtx.Provider value={state}>
{children}
</BlockEventsCtx.Provider>
);
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubjectInfo } from '@pezkuwi/ui-keyring/observable/types';
import type { Accounts, Addresses } from './types.js';
import React, { useEffect, useState } from 'react';
import { combineLatest, map } from 'rxjs';
import { keyring } from '@pezkuwi/ui-keyring';
import { u8aToHex } from '@pezkuwi/util';
import { decodeAddress } from '@pezkuwi/util-crypto';
import { useApi } from '../useApi.js';
interface Props {
children?: React.ReactNode;
}
interface State {
accounts: Accounts;
addresses: Addresses;
}
const EMPTY_IS = () => false;
const EMPTY: State = {
accounts: { allAccounts: [], allAccountsHex: [], areAccountsLoaded: false, hasAccounts: false, isAccount: EMPTY_IS },
addresses: { allAddresses: [], allAddressesHex: [], areAddressesLoaded: false, hasAddresses: false, isAddress: EMPTY_IS }
};
export const KeyringCtx = React.createContext<State>(EMPTY);
/**
* @internal Helper function to dedupe a list of items, only adding it if
*
* 1. It is not already present in our list of results
* 2. It does not exist against a secondary list to check
*
* The first check ensures that we never have dupes in the original. The second
* ensures that e.g. an address is not also available as an account
**/
function filter (isEthereum: boolean, items: string[], others: string[] = []): string[] {
const allowedLength = isEthereum
? 20
: 32;
return items.reduce<string[]>((result, a) => {
if (!result.includes(a) && !others.includes(a)) {
try {
if (decodeAddress(a).length >= allowedLength) {
result.push(a);
} else {
console.warn(`Not adding address ${a}, not in correct format for chain (requires publickey from address)`);
}
} catch {
console.error(a, allowedLength);
}
}
return result;
}, []);
}
/**
* @internal Helper function to convert a list of ss58 addresses into hex
**/
function toHex (items: string[]): string[] {
return items
.map((a): string | null => {
try {
return u8aToHex(decodeAddress(a));
} catch (error) {
// This is actually just a failsafe - the keyring really should
// not be passing through invalid ss58 values, but never say never
console.error(`Unable to convert address ${a} to hex`, (error as Error).message);
return null;
}
})
.filter((a): a is string => !!a);
}
/**
* @internal Helper to create an is{Account, Address} check
**/
function createCheck (items: string[]): Accounts['isAccount'] {
return (a?: string | null | { toString: () => string }): boolean =>
!!a && items.includes(a.toString());
}
function extractAccounts (isEthereum: boolean, accounts: SubjectInfo = {}): Accounts {
const allAccounts = filter(isEthereum, Object.keys(accounts));
return {
allAccounts,
allAccountsHex: toHex(allAccounts),
areAccountsLoaded: true,
hasAccounts: allAccounts.length !== 0,
isAccount: createCheck(allAccounts)
};
}
function extractAddresses (isEthereum: boolean, addresses: SubjectInfo = {}, accounts: string[]): Addresses {
const allAddresses = filter(isEthereum, Object.keys(addresses), accounts);
return {
allAddresses,
allAddressesHex: toHex(allAddresses),
areAddressesLoaded: true,
hasAddresses: allAddresses.length !== 0,
isAddress: createCheck(allAddresses)
};
}
export function KeyringCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { isApiReady, isEthereum } = useApi();
const [state, setState] = useState(EMPTY);
useEffect((): () => void => {
let sub: null | { unsubscribe: () => void } = null;
// Defer keyring injection until the API is ready - we need to have the chain
// info to determine which type of addresses we can use (before subscribing)
if (isApiReady) {
sub = combineLatest([
keyring.accounts.subject.pipe(
map((accInfo) => extractAccounts(isEthereum, accInfo))
),
keyring.addresses.subject
])
.pipe(
map(([accounts, addrInfo]): State => ({
accounts,
addresses: extractAddresses(isEthereum, addrInfo, accounts.allAccounts)
}))
)
.subscribe((state) => setState(state));
}
return (): void => {
sub && sub.unsubscribe();
};
}, [isApiReady, isEthereum]);
return (
<KeyringCtx.Provider value={state}>
{children}
</KeyringCtx.Provider>
);
}
@@ -0,0 +1,92 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { AssetInfoComplete } from '../types.js';
import type { PayWithAsset } from './types.js';
import React, { useCallback, useMemo, useState } from 'react';
import { useApi, useAssetIds, useAssetInfos } from '@pezkuwi/react-hooks';
interface Props {
children?: React.ReactNode;
}
const EMPTY_STATE: PayWithAsset = {
assetOptions: [],
isDisabled: true,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange: () => {},
selectedFeeAsset: null
};
export const PayWithAssetCtx = React.createContext<PayWithAsset>(EMPTY_STATE);
export function PayWithAssetCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { isApiReady } = useApi();
return isApiReady ? <PayWithAssetProvider>{children}</PayWithAssetProvider> : <>{children}</>;
}
function PayWithAssetProvider ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const ids = useAssetIds();
const assetInfos = useAssetInfos(ids);
const [selectedFeeAsset, setSelectedFeeAsset] = useState<AssetInfoComplete | null>(null);
const nativeAsset = useMemo(
() => api.registry.chainTokens[0],
[api]
);
const completeAssetInfos = useMemo(
() => (assetInfos
?.filter((i): i is AssetInfoComplete =>
!!(i.details && i.metadata) && !i.details.supply.isZero() && !!i.details?.toJSON().isSufficient)
) || [],
[assetInfos]
);
const assetOptions = useMemo(
() => [
{ text: `${nativeAsset} (Native)`, value: nativeAsset },
...completeAssetInfos.map(({ id, metadata }) => ({
text: `${metadata.name.toUtf8()} (${id.toString()})`,
value: id.toString()
}))],
[completeAssetInfos, nativeAsset]
);
const onChange = useCallback((assetId: BN, cb?: () => void) => {
const selectedFeeAsset = completeAssetInfos.find((a) => a.id.toString() === assetId.toString());
setSelectedFeeAsset(selectedFeeAsset ?? null);
cb?.();
}, [completeAssetInfos]);
const isEnabled = useMemo(() =>
api.registry.signedExtensions.some(
(a) => a === 'ChargeAssetTxPayment'
) &&
!!api.tx.assetConversion &&
!!api.call.assetConversionApi &&
completeAssetInfos.length > 0,
[api.call.assetConversionApi, api.registry.signedExtensions, api.tx.assetConversion, completeAssetInfos.length]
);
const values: PayWithAsset = useMemo(() => {
return {
assetOptions,
isDisabled: !isEnabled,
onChange,
selectedFeeAsset
};
}, [assetOptions, isEnabled, onChange, selectedFeeAsset]);
return (
<PayWithAssetCtx.Provider value={values}>
{children}
</PayWithAssetCtx.Provider>
);
}
+307
View File
@@ -0,0 +1,307 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableResult } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/promise/types';
import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueProps, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from '@pezkuwi/react-components/Status/types';
import type { DispatchError, EventRecord } from '@pezkuwi/types/interfaces';
import type { ITuple, Registry, SignerPayloadJSON } from '@pezkuwi/types/types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { getDispatchError, getIncompleteMessage } from '@pezkuwi/react-components/Status/checks';
import { STATUS_COMPLETE } from '@pezkuwi/react-components/Status/constants';
import { getContractAbi } from '@pezkuwi/react-components/util';
import jsonrpc from '@pezkuwi/types/interfaces/jsonrpc';
export interface Props {
children: React.ReactNode;
}
interface StatusCount {
count: number;
status: ActionStatusPartial;
}
const EMPTY_STATE: Partial<QueueProps> = {
stqueue: [] as QueueStatus[],
txqueue: [] as QueueTx[]
};
let nextId = 0;
const EVENT_MESSAGE = 'extrinsic event';
const REMOVE_TIMEOUT = 7500;
const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic;
export const QueueCtx = React.createContext<QueueProps>(EMPTY_STATE as QueueProps);
function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] {
let others: ActionStatus | null = null;
const initial = status
.reduce((result: StatusCount[], status): StatusCount[] => {
const prev = result.find(({ status: prev }) =>
prev.action === status.action &&
prev.status === status.status
);
if (prev) {
prev.count++;
} else {
result.push({ count: 1, status });
}
return result;
}, [])
.map(({ count, status }): ActionStatusPartial =>
count === 1
? status
: { ...status, action: `${status.action} (x${count})` }
)
.filter((status): boolean => {
if (status.message !== EVENT_MESSAGE) {
return true;
}
if (others) {
if (status.action.startsWith('system.ExtrinsicSuccess')) {
(others.action as string[]).unshift(status.action);
} else {
(others.action as string[]).push(status.action);
}
} else {
others = {
...status,
action: [status.action]
};
}
return false;
});
return others
? initial.concat(others)
: initial;
}
function extractEvents (result?: SubmittableResult): ActionStatus[] {
return mergeStatus(
(result?.events || [])
// filter events handled globally, or those we are not interested in, these are
// handled by the global overview, so don't add them here
.filter((record) =>
!!record.event &&
record.event.section !== 'democracy'
)
.map((record): ActionStatusPartial => {
const { event: { data, method, section } } = record;
if (section === 'system' && method === 'ExtrinsicFailed') {
const [dispatchError] = data as unknown as ITuple<[DispatchError]>;
return {
action: `${section}.${method}`,
message: getDispatchError(dispatchError),
status: 'error'
};
}
const incomplete = getIncompleteMessage(record);
if (incomplete) {
return {
action: `${section}.${method}`,
message: incomplete,
status: 'eventWarn'
};
} else if (section === 'contracts') {
if (method === 'ContractExecution' && data.length === 2) {
// see if we have info for this contract
const [accountId, encoded] = data;
try {
const abi = getContractAbi(accountId.toString());
if (abi) {
const decoded = abi.decodeEvent(encoded as EventRecord);
return {
action: decoded.event.identifier,
message: 'contract event',
status: 'event'
};
}
} catch (error) {
// ABI mismatch?
console.error(error);
}
} else if (method === 'Evicted') {
return {
action: `${section}.${method}`,
message: 'contract evicted',
status: 'error'
};
}
}
return {
action: `${section}.${method}`,
message: EVENT_MESSAGE,
status: 'event'
};
})
);
}
const EMPTY_QUEUE_ST: QueueStatus[] = [];
const EMPTY_QUEUE_TX: QueueTx[] = [];
export function QueueCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [stqueue, _setStQueue] = useState<QueueStatus[]>(EMPTY_QUEUE_ST);
const [txqueue, _setTxQueue] = useState<QueueTx[]>(EMPTY_QUEUE_TX);
const stRef = useRef(stqueue);
const txRef = useRef(txqueue);
const setStQueue = useCallback(
(st: QueueStatus[]): void => {
stRef.current = st;
_setStQueue(st);
},
[]
);
const setTxQueue = useCallback(
(tx: QueueTx[]): void => {
txRef.current = tx;
_setTxQueue(tx);
},
[]
);
const addToTxQueue = useCallback(
(value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => {
const id = ++nextId;
const removeItem = () => setTxQueue([
...txRef.current.map((item): QueueTx =>
item.id === id
? { ...item, status: 'completed' }
: item
)
]);
setTxQueue([...txRef.current, {
...value,
id,
removeItem,
rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC,
status: 'queued'
}]);
},
[setTxQueue]
);
const queueAction = useCallback(
(_status: ActionStatus | ActionStatus[]): void => {
const status = Array.isArray(_status) ? _status : [_status];
status.length && setStQueue([
...stRef.current,
...(status.map((item): QueueStatus => {
const id = ++nextId;
const removeItem = (): void =>
setStQueue([...stRef.current.filter((item) => item.id !== id)]);
setTimeout(removeItem, REMOVE_TIMEOUT);
return {
...item,
id,
isCompleted: false,
removeItem
};
}))
]);
},
[setStQueue]
);
const queueExtrinsic = useCallback(
(value: PartialQueueTxExtrinsic) => addToTxQueue({ ...value }),
[addToTxQueue]
);
const queuePayload = useCallback(
(registry: Registry, payload: SignerPayloadJSON, signerCb: SignerCallback): void => {
addToTxQueue({
accountId: payload.address,
// this is not great, but the Extrinsic doesn't need a submittable
extrinsic: registry.createType('Extrinsic',
{ method: registry.createType('Call', payload.method) },
{ version: payload.version }
) as unknown as SubmittableExtrinsic,
payload,
signerCb
});
},
[addToTxQueue]
);
const queueRpc = useCallback(
(value: PartialQueueTxRpc) => addToTxQueue({ ...value }),
[addToTxQueue]
);
const queueSetTxStatus = useCallback(
(id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => {
setTxQueue([
...txRef.current.map((item): QueueTx =>
item.id === id
? {
...item,
error: error === undefined
? item.error
: error,
result: result === undefined
? item.result as SubmittableResult
: result,
status: item.status === 'completed'
? item.status
: status
}
: item
)
]);
queueAction(extractEvents(result));
if (STATUS_COMPLETE.includes(status)) {
setTimeout((): void => {
const item = txRef.current.find((item) => item.id === id);
item && item.removeItem();
}, REMOVE_TIMEOUT);
}
},
[queueAction, setTxQueue]
);
const value = useMemo(
() => ({
queueAction,
queueExtrinsic,
queuePayload,
queueRpc,
queueSetTxStatus,
stqueue,
txqueue
}),
[queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue]
);
return (
<QueueCtx.Provider value={value}>
{children}
</QueueCtx.Provider>
);
}
@@ -0,0 +1,100 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PropsWithChildren } from 'react';
import type { StakingAsyncApis } from './types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { useApi } from '@pezkuwi/react-hooks';
const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k);
export const getApi = async (url: string[]|string) => {
const api = await ApiPromise.create({
provider: new WsProvider(url)
});
await api.isReadyOrError;
return api;
};
const EMPTY_STATE: StakingAsyncApis = {
ahEndPoints: [],
isRelayChain: false,
isStakingAsync: false,
rcEndPoints: []
};
export const StakingAsyncApisCtx = React.createContext<StakingAsyncApis>(EMPTY_STATE);
export const StakingAsyncApisCtxRoot = ({ children }: PropsWithChildren) => {
const { api, isApiReady } = useApi();
return isApiReady &&
!!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient))
? <StakingAsyncProvider>{children}</StakingAsyncProvider>
: <>{children}</>;
};
export const StakingAsyncProvider = ({ children }: PropsWithChildren) => {
const { api, apiEndpoint } = useApi();
const [ahApi, setAhApi] = useState<ApiPromise>();
const [rcApi, setRcApi] = useState<ApiPromise>();
const isRelayChain = useMemo(() => {
return !!api.tx.stakingAhClient;
}, [api]);
const isStakingAsync = useMemo(() => {
return !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient));
}, [api]);
const rcEndPoints = useMemo(() => {
return (isRelayChain
? apiEndpoint?.providers
: apiEndpoint?.valueRelay)?.filter((e) => e.startsWith('wss://')) || [];
}, [apiEndpoint, isRelayChain]);
const ahEndPoints: string[] = useMemo(() => {
if (isRelayChain) {
return allEndPoints.find(({ genesisHashRelay, paraId }) =>
paraId === 1000 && genesisHashRelay === api.genesisHash.toHex()
)?.providers || [];
}
return apiEndpoint?.providers?.filter((e) => e.startsWith('wss://')) || [];
}, [api, apiEndpoint, isRelayChain]);
useEffect(() => {
if (isRelayChain) {
const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length));
setRcApi(api);
!!ahUrl && getApi(ahUrl).then(setAhApi).catch(console.error);
} else {
const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length));
setAhApi(api);
!!rcUrl && getApi(rcUrl).then(setRcApi).catch(console.error);
}
}, [ahEndPoints, api, isRelayChain, rcEndPoints]);
const state = useMemo(() => ({
ahApi,
ahEndPoints,
isRelayChain,
isStakingAsync,
rcApi,
rcEndPoints
}), [ahApi, ahEndPoints, isRelayChain, isStakingAsync, rcApi, rcEndPoints]);
return <StakingAsyncApisCtx.Provider value={state}>
{children}
</StakingAsyncApisCtx.Provider>;
};
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import React from 'react';
interface SectionType {
icon?: IconName;
text?: string;
}
export const TabsCtx = React.createContext<SectionType>({});
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Adapted from https://hackernoon.com/simplifying-responsive-layouts-with-react-hooks-19db73893a7a
import type { WindowSize } from './types.js';
import React, { useEffect, useState } from 'react';
interface Props {
children: React.ReactNode;
}
function getDimensions (): WindowSize {
return {
height: window.innerHeight,
width: window.innerWidth
};
}
export const WindowSizeCtx = React.createContext<WindowSize>(getDimensions());
export function WindowSizeCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [dimensions, setDimensions] = useState(() => getDimensions());
useEffect((): () => void => {
function handleResize (): void {
setDimensions(getDimensions());
}
window.addEventListener('resize', handleResize);
return (): void => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<WindowSizeCtx.Provider value={dimensions}>
{children}
</WindowSizeCtx.Provider>
);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { ApiStatsCtxRoot } from './ApiStats.js';
export { BlockAuthorsCtxRoot } from './BlockAuthors.js';
export { BlockEventsCtxRoot } from './BlockEvents.js';
export { KeyringCtxRoot } from './Keyring.js';
export { PayWithAssetCtxRoot } from './PayWithAsset.js';
export { QueueCtxRoot } from './Queue.js';
export { StakingAsyncApisCtxRoot } from './StakingAsync.js';
export { WindowSizeCtxRoot } from './WindowSize.js';
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/promise/types';
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import type { InjectedExtension } from '@pezkuwi/extension-inject/types';
import type { ProviderStats } from '@pezkuwi/rpc-provider/types';
import type { BlockNumber, EventRecord, Moment } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
import type { AssetInfoComplete } from '../types.js';
export interface ApiState {
apiDefaultTx: SubmittableExtrinsicFunction;
apiDefaultTxSudo: SubmittableExtrinsicFunction;
chainSS58: number;
fork: Blockchain | null;
hasInjectedAccounts: boolean;
isApiReady: boolean;
isDevelopment: boolean;
isEthereum: boolean;
specName: string;
specVersion: string;
systemChain: string;
systemName: string;
systemVersion: string;
}
export interface ApiProps extends ApiState {
api: ApiPromise;
apiEndpoint: LinkOption | null;
apiError: string | null;
apiIdentity: ApiPromise;
apiCoretime: ApiPromise;
enableIdentity: boolean;
apiRelay: ApiPromise | null;
apiSystemPeople: ApiPromise | null;
apiUrl?: string;
createLink: (path: string, apiUrl?: string) => string;
extensions?: InjectedExtension[];
isApiConnected: boolean;
isApiInitialized: boolean;
isElectron: boolean;
isWaitingInjected: boolean;
isLocalFork?: boolean;
}
export interface Accounts {
allAccounts: string[];
allAccountsHex: string[];
areAccountsLoaded: boolean;
hasAccounts: boolean;
isAccount: (address?: string | null | { toString: () => string }) => boolean;
}
export interface Addresses {
allAddresses: string[];
allAddressesHex: string[];
areAddressesLoaded: boolean;
hasAddresses: boolean;
isAddress: (address?: string | null | { toString: () => string }) => boolean;
}
export interface ApiStats {
stats: ProviderStats;
when: number;
}
export interface PayWithAsset {
isDisabled: boolean;
assetOptions: {text: string, value: string}[];
onChange: (assetId: BN, cb?: () => void) => void;
selectedFeeAsset: AssetInfoComplete | null;
}
interface BlockTime {
readonly timestamp: Moment;
}
export type AugmentedBlockHeader = HeaderExtended & BlockTime;
export interface BlockAuthors {
byAuthor: Record<string, string>;
eraPoints: Record<string, string>;
lastBlockAuthors: string[];
lastBlockNumber?: string;
lastHeader?: AugmentedBlockHeader;
lastHeaders: AugmentedBlockHeader[];
}
export interface BlockEvents {
eventCount: number;
events: KeyedEvent[];
}
export interface IndexedEvent {
indexes: number[];
record: EventRecord;
}
export interface KeyedEvent extends IndexedEvent {
blockHash?: string;
blockNumber?: BlockNumber;
key: string;
}
export type SidebarState = [string | null, (() => void) | null];
export type Sidebar = undefined | (([address, onUpdateName]: SidebarState) => void);
export interface WindowSize {
height: number;
width: number;
}
export interface StakingAsyncApis {
rcApi?: ApiPromise,
ahApi?: ApiPromise,
ahEndPoints: string[],
isRelayChain: boolean,
isStakingAsync: boolean,
rcEndPoints: string[]
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// we use augmented types in this tsconfig
import '@pezkuwi/api-augment/bizinikiwi';
export { createNamedHook } from './createNamedHook.js';
export * from './ctx/index.js';
export { useAccountId } from './useAccountId.js';
export { useAccountInfo } from './useAccountInfo.js';
export { useAccounts } from './useAccounts.js';
export { useAddresses } from './useAddresses.js';
export { useApi } from './useApi.js';
export { useApiStats } from './useApiStats.js';
export { useApiUrl } from './useApiUrl.js';
export { useAssetIds } from './useAssetIds.js';
export { useAssetInfos } from './useAssetInfos.js';
export { useAvailableSlashes } from './useAvailableSlashes.js';
export { useBalancesAll } from './useBalancesAll.js';
export { useBestHash } from './useBestHash.js';
export { useBestNumber, useBestNumberRelay } from './useBestNumber.js';
export { useBlockAuthors } from './useBlockAuthors.js';
export { useBlockEvents } from './useBlockEvents.js';
export { useBlockInterval } from './useBlockInterval.js';
export { useBlocksPerDays } from './useBlocksPerDays.js';
export { useBlockTime } from './useBlockTime.js';
export { useBrokerConfig } from './useBrokerConfig.js';
export { useBrokerLeases } from './useBrokerLeases.js';
export { useBrokerReservations } from './useBrokerReservations.js';
export { useBrokerSalesInfo } from './useBrokerSalesInfo.js';
export { useBrokerStatus } from './useBrokerStatus.js';
export { useCacheKey } from './useCacheKey.js';
export { useCall } from './useCall.js';
export { useCallMulti } from './useCallMulti.js';
export { useCollectiveInstance } from './useCollectiveInstance.js';
export { useCollectiveMembers } from './useCollectiveMembers.js';
export { useCoreDescriptor } from './useCoreDescriptor.js';
export { useCoretimeConsts } from './useCoretimeConsts.js';
export { useCoretimeEndpoint } from './useCoretimeEndpoint.js';
export { useCoretimeInformation } from './useCoretimeInformation.js';
export { useDebounce } from './useDebounce.js';
export { useDelegations } from './useDelegations.js';
export { useDeriveAccountFlags } from './useDeriveAccountFlags.js';
export { useDeriveAccountInfo } from './useDeriveAccountInfo.js';
export { useElementPosition } from './useElementPosition.js';
export { useEndpoint } from './useEndpoint.js';
export { useEventChanges } from './useEventChanges.js';
export { useEventTrigger } from './useEventTrigger.js';
export { useExtrinsicTrigger } from './useExtrinsicTrigger.js';
export { useFavorites } from './useFavorites.js';
export { useFormField } from './useFormField.js';
export { useIncrement } from './useIncrement.js';
export { useInflation } from './useInflation.js';
export { useIpfs } from './useIpfs.js';
export { useIpfsLink } from './useIpfsLink.js';
export { useIsMountedRef } from './useIsMountedRef.js';
export { useJudgements } from './useJudgements.js';
export { useKeyring } from './useKeyring.js';
export { useLedger } from './useLedger.js';
export { useMapEntries } from './useMapEntries.js';
export { useMapKeys } from './useMapKeys.js';
export { useMemoValue } from './useMemoValue.js';
export { useMetadataFetch } from './useMetadataFetch.js';
export { useModal } from './useModal.js';
export { useNextTick } from './useNextTick.js';
export { useNonEmptyString } from './useNonEmptyString.js';
export { useNonZeroBn } from './useNonZeroBn.js';
export { useOutsideClick } from './useOutsideClick.js';
export { useOwnEraRewards } from './useOwnEraRewards.js';
export { useOwnStashes, useOwnStashIds } from './useOwnStashes.js';
export { useOwnStashInfos } from './useOwnStashInfos.js';
export { useParaApi } from './useParaApi.js';
export { useIsParasLinked, useParaEndpoints } from './useParaEndpoints.js';
export { usePassword } from './usePassword.js';
export { usePayWithAsset } from './usePayWithAsset.js';
export { usePeopleEndpoint } from './usePeopleEndpoint.js';
export { usePopupWindow } from './usePopupWindow.js';
export { usePreimage } from './usePreimage.js';
export { useProxies } from './useProxies.js';
export { useQueue } from './useQueue.js';
export { useQueueStatus } from './useQueueStatus.js';
export { useRegions } from './useRegions.js';
export { useRegistrars } from './useRegistrars.js';
export { useSavedFlags } from './useSavedFlags.js';
export { useScroll } from './useScroll.js';
export { useStakingAsyncApis } from './useStakingAsyncApis.js';
export { useStakingInfo } from './useStakingInfo.js';
export { useStepper } from './useStepper.js';
export { useSubidentities } from './useSubidentities.js';
export { useSudo } from './useSudo.js';
export { useSystemApi } from './useSystemApi.js';
export { useTeleport } from './useTeleport.js';
export { useTheme } from './useTheme.js';
export { useTimer } from './useTimer.js';
export { useToggle } from './useToggle.js';
export { useTreasury } from './useTreasury.js';
export { useTxBatch } from './useTxBatch.js';
export type { VestingInfo } from './useVesting.js';
export { useVesting } from './useVesting.js';
export { useVotingStatus } from './useVotingStatus.js';
export { useWeight } from './useWeight.js';
export { useWindowColumns } from './useWindowColumns.js';
export { useWindowSize } from './useWindowSize.js';
export { useWorkloadInfos } from './useWorkloadInfos.js';
export { useWorkplanInfos } from './useWorkplanInfos.js';
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/react-hooks 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('react-hooks');
}
+386
View File
@@ -0,0 +1,386 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/api-augment';
import type React from 'react';
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { DeriveAccountFlags, DeriveAccountRegistration } from '@pezkuwi/api-derive/types';
import type { Option, u32, u128, Vec } from '@pezkuwi/types';
import type { AccountId, BlockNumber, Call, Hash, SessionIndex, ValidatorPrefs } from '@pezkuwi/types/interfaces';
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata, PalletPreimageRequestStatus, PalletStakingRewardDestination, PalletStakingStakingLedger, PezkuwiRuntimeTeyrchainsAssignerCoretimeCoreDescriptor, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@pezkuwi/types/lookup';
import type { ICompact, IExtrinsic, INumber } from '@pezkuwi/types/types';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
import type { CoreTimeTypes } from './constants.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CallParam = any;
export type CallParams = [] | CallParam[];
export interface CallOptions<T> {
defaultValue?: T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
paramMap?: (params: any) => CallParams;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transform?: (value: any, api: ApiPromise) => T;
withParams?: boolean;
withParamsTransform?: boolean;
}
export type TxDef = [string, unknown[] | ((...params: unknown[]) => SubmittableExtrinsic<'promise'>)];
export type TxDefs = SubmittableExtrinsic<'promise'> | IExtrinsic | Call | TxDef | null;
export type TxSource<T extends TxDefs> = [T, boolean];
export type CollectiveType = 'alliance' | 'council' | 'membership' | 'technicalCommittee';
export interface ModalState {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
export interface Inflation {
idealStake: number;
idealInterest: number;
inflation: number;
stakedFraction: number;
stakedReturn: number;
}
export interface AssetInfo {
details: PalletAssetsAssetDetails | null;
id: BN;
isAdminMe: boolean;
isIssuerMe: boolean;
isFreezerMe: boolean;
isOwnerMe: boolean;
key: string;
metadata: PalletAssetsAssetMetadata | null;
}
export interface AssetInfoComplete extends AssetInfo {
details: PalletAssetsAssetDetails;
metadata: PalletAssetsAssetMetadata;
}
export interface Slash {
accountId: AccountId;
amount: u128;
}
export interface SessionRewards {
blockHash: Hash;
blockNumber: BlockNumber;
isEventsEmpty: boolean;
reward: u128;
sessionIndex: SessionIndex;
slashes: Slash[];
}
export interface ExtrinsicAndSenders {
extrinsic: SubmittableExtrinsic<'promise'> | null;
isSubmittable: boolean;
sendTx: () => void;
sendUnsigned: () => void;
}
export interface TxProps {
accountId?: string | null;
onChangeAccountId?: (_: string | null) => void;
onSuccess?: () => void;
onFailed?: () => void;
onStart?: () => void;
onUpdate?: () => void;
}
export interface TxState extends ExtrinsicAndSenders {
isSending: boolean;
accountId?: string | null;
onChangeAccountId: (_: string | null) => void;
}
export interface UseSudo {
allAccounts: string[];
hasSudoKey: boolean;
sudoKey?: string;
}
export interface AddressFlags extends DeriveAccountFlags {
isDevelopment: boolean;
isEditable: boolean;
isEthereum: boolean;
isExternal: boolean;
isFavorite: boolean;
isHardware: boolean;
isInContacts: boolean;
isInjected: boolean;
isMultisig: boolean;
isProxied: boolean;
isOwned: boolean;
isValidator: boolean;
isNominator: boolean;
}
export interface AddressIdentity extends DeriveAccountRegistration {
isExistent: boolean;
isKnownGood: boolean;
waitCount: number;
}
export interface UseAccountInfo {
accountIndex?: string;
flags: AddressFlags;
name: string;
setName: React.Dispatch<string>;
tags: string[];
setTags: React.Dispatch<string[]>;
genesisHash: string | null;
identity?: AddressIdentity;
isEditingName: boolean;
meta?: KeyringJson$Meta;
toggleIsEditingName: () => void;
isEditingTags: boolean;
isEditing: () => boolean;
isNull: boolean;
toggleIsEditingTags: () => void;
onSaveName: () => void;
onSaveTags: () => void;
onSetGenesisHash: (genesisHash: HexString | null) => void;
onForgetAddress: () => void;
setIsEditingName: (isEditing: boolean) => void;
setIsEditingTags: (isEditing: boolean) => void;
}
export interface StakerState {
controllerId: string | null;
destination?: PalletStakingRewardDestination | null;
exposurePaged?: Option<SpStakingExposurePage>;
exposureMeta?: Option<SpStakingPagedExposureMetadata>
claimedRewardsEras?: Vec<u32>
hexSessionIdNext: string | null;
hexSessionIdQueue: string | null;
isLoading: boolean;
isOwnController: boolean;
isOwnStash: boolean;
isStashNominating: boolean;
isStashValidating: boolean;
nominating?: string[];
sessionIds: string[];
stakingLedger?: PalletStakingStakingLedger;
stashId: string;
validatorPrefs?: ValidatorPrefs;
}
export interface Registrar {
address: string;
index: number;
}
export type BatchType = 'all' | 'default' | 'force';
export interface BatchOptions {
max?: number;
type?: BatchType;
}
export interface PreimageDeposit {
amount: BN;
who: string;
}
export interface PreimageStatus {
count: number;
deposit?: PreimageDeposit;
isCompleted: boolean;
isHashParam: boolean;
proposalHash: HexString;
proposalLength?: BN;
status: PalletPreimageRequestStatus | null;
}
export interface PreimageBytes {
proposal?: Call | null;
proposalError?: string | null;
proposalWarning?: string | null;
}
export interface Preimage extends PreimageBytes, PreimageStatus {
// just the interfaces above
}
export interface V2WeightConstruct {
refTime: BN | ICompact<INumber>;
proofSize?: BN | ICompact<INumber>;
}
export interface WeightResult {
v1Weight: BN;
v2Weight: V2WeightConstruct;
}
export interface CoreDescription {
core: number;
info: PezkuwiRuntimeTeyrchainsAssignerCoretimeCoreDescriptor[];
}
export interface CoreDescriptorAssignment {
task: string,
ratio: number,
remaining: number,
isTask: boolean,
isPool: boolean
}
export interface CoreDescriptor {
core: number,
info: {
currentWork: {
assignments: CoreDescriptorAssignment[],
endHint: BN | null,
pos: number,
step: number
},
queue: {
first: BN,
last: BN
}
}
}
export interface OnDemandQueueStatus {
traffic: u128;
nextIndex: u32;
smallestIndex: u32;
freedIndices: [string, u32][];
}
export interface CoreWorkload {
core: number,
info: CoreWorkloadInfo
}
export interface CoreWorkloadInfo {
task: number | string,
isTask: boolean
isPool: boolean
mask: string[]
maskBits: number
}
export interface CoreWorkplan {
core: number;
info: CoreWorkplanInfo
timeslice: number;
}
export interface CoreWorkplanInfo {
task: number | string,
isTask: boolean
isPool: boolean
mask: string[]
maskBits: number
}
export interface RegionInfo {
core: number,
start: number,
end: number,
owner: string,
paid: string,
mask: `0x${string}`
}
export interface Reservation {
task: string
mask: string[],
maskBits: number
}
export interface LegacyLease {
core: number,
until: number,
task: string
}
export interface PalletBrokerSaleInfoRecord {
saleStart: number;
leadinLength: number;
endPrice: BN;
regionBegin: number;
regionEnd: number;
idealCoresSold: number;
coresOffered: number;
firstCore: number;
selloutPrice: BN;
coresSold: number;
}
export interface PalletBrokerConfigRecord {
advanceNotice: number;
interludeLength: number;
leadinLength: number;
regionLength: number;
idealBulkProportion: BN;
limitCoresOffered: number;
renewalBump: BN;
contributionTimeout: number;
}
export interface ChainWorkTaskInformation {
lastBlock: number
renewal: PotentialRenewal | undefined
renewalStatus: string
renewalStatusMessage: string
type: CoreTimeTypes
workload: CoreWorkload | undefined
workplan: CoreWorkplan[] | undefined
}
export interface ChainInformation {
id: number,
lease: LegacyLease | undefined,
reservation: Reservation| undefined
workTaskInfo: ChainWorkTaskInformation[]
}
export interface ChainBlockConstants {
blocksPerTimeslice: number,
blocktimeMs: number
}
export interface ChainConstants {
coretime: ChainBlockConstants,
relay: ChainBlockConstants
}
export interface CoretimeInformation {
constants: ChainConstants,
chainInfo: Record<number, ChainInformation>,
salesInfo: PalletBrokerSaleInfoRecord,
status: BrokerStatus,
region: RegionInfo[],
config: PalletBrokerConfigRecord
taskIds: number[]
}
export interface BrokerStatus {
coreCount: number;
privatePoolSize: number;
systemPoolSize: number;
lastCommittedTimeslice: number;
lastTimeslice: number;
}
export interface PotentialRenewal {
core: number,
when: number,
price: BN,
completion: 'Complete' | 'Partial',
mask: string[]
maskBits: number,
task: string
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
function useAccountIdImpl (initialValue: string | null = null, onChangeAccountId?: (_: string | null) => void): [string | null, (_: string | null) => void] {
const [accountId, setAccountId] = useState<string | null>(initialValue);
const _setAccountId = useCallback(
(accountId: string | null = null): void => {
setAccountId(accountId);
onChangeAccountId && onChangeAccountId(accountId);
},
[onChangeAccountId]
);
return [accountId, _setAccountId];
}
export const useAccountId = createNamedHook('useAccountId', useAccountIdImpl);
+277
View File
@@ -0,0 +1,277 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Nominations, ValidatorPrefs } from '@pezkuwi/types/interfaces';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import type { HexString } from '@pezkuwi/util/types';
import type { AddressFlags, AddressIdentity, UseAccountInfo } from './types.js';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { keyring } from '@pezkuwi/ui-keyring';
import { isFunction, isHex } from '@pezkuwi/util';
import { isEmpty } from './utils/isEmpty.js';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useDeriveAccountFlags } from './useDeriveAccountFlags.js';
import { useDeriveAccountInfo } from './useDeriveAccountInfo.js';
import { useKeyring } from './useKeyring.js';
import { useToggle } from './useToggle.js';
const IS_NONE = {
isCouncil: false,
isDevelopment: false,
isEditable: false,
isEthereum: false,
isExternal: false,
isFavorite: false,
isHardware: false,
isInContacts: false,
isInjected: false,
isMultisig: false,
isNominator: false,
isOwned: false,
isProxied: false,
isSociety: false,
isSudo: false,
isTechCommittee: false,
isValidator: false
};
function useAccountInfoImpl (value: string | null, isContract = false): UseAccountInfo {
const { api, apiIdentity, apiSystemPeople } = useApi();
const { accounts: { isAccount }, addresses: { isAddress } } = useKeyring();
const accountInfo = useDeriveAccountInfo(value);
const accountFlags = useDeriveAccountFlags(value);
const nominator = useCall<Nominations>(api.query.staking?.nominators, [value]);
const validator = useCall<ValidatorPrefs>(api.query.staking?.validators, [value]);
const [accountIndex, setAccountIndex] = useState<string | undefined>(undefined);
const [tags, setSortedTags] = useState<string[]>([]);
const [name, setName] = useState('');
const [genesisHash, setGenesisHash] = useState<HexString | null>(null);
const [identity, setIdentity] = useState<AddressIdentity | undefined>();
const [flags, setFlags] = useState<AddressFlags>(IS_NONE);
const [meta, setMeta] = useState<KeyringJson$Meta | undefined>();
const [isEditingName, toggleIsEditingName, setIsEditingName] = useToggle();
const [isEditingTags, toggleIsEditingTags, setIsEditingTags] = useToggle();
useEffect((): void => {
validator && setFlags((flags) => ({
...flags,
isValidator: !isEmpty(validator)
}));
}, [validator]);
useEffect((): void => {
nominator && setFlags((flags) => ({
...flags,
isNominator: !isEmpty(nominator)
}));
}, [nominator]);
useEffect((): void => {
accountFlags && setFlags((flags) => ({
...flags,
...accountFlags
}));
}, [accountFlags]);
useEffect((): void => {
const { accountIndex, identity, nickname } = accountInfo || {};
const newIndex = accountIndex?.toString();
setAccountIndex((oldIndex) =>
oldIndex !== newIndex
? newIndex
: oldIndex
);
let name;
if (isFunction(apiIdentity.query.identity?.identityOf)) {
if (identity?.display) {
name = identity.display;
}
} else if (nickname) {
name = nickname;
}
setName(name || '');
if (identity) {
const judgements = identity.judgements.filter(([, judgement]) => !judgement.isFeePaid);
const isKnownGood = judgements.some(([, judgement]) => judgement.isKnownGood);
setIdentity({
...identity,
isExistent: !!identity.display,
isKnownGood,
judgements,
waitCount: identity.judgements.length - judgements.length
});
} else {
setIdentity(undefined);
}
}, [accountInfo, api, apiSystemPeople, apiIdentity]);
useEffect((): void => {
if (value) {
try {
const accountOrAddress = keyring.getAccount(value) || keyring.getAddress(value);
const isOwned = isAccount(value);
const isInContacts = isAddress(value);
setGenesisHash(accountOrAddress?.meta.genesisHash || null);
setFlags((flags): AddressFlags => ({
...flags,
isDevelopment: accountOrAddress?.meta.isTesting || false,
isEditable: !!(!identity?.display && (isInContacts || accountOrAddress?.meta.isMultisig || (accountOrAddress && !(accountOrAddress.meta.isInjected)))) || false,
isEthereum: isHex(value, 160),
isExternal: !!accountOrAddress?.meta.isExternal || false,
isHardware: !!accountOrAddress?.meta.isHardware || false,
isInContacts,
isInjected: !!accountOrAddress?.meta.isInjected || false,
isMultisig: !!accountOrAddress?.meta.isMultisig || false,
isOwned,
isProxied: !!accountOrAddress?.meta.isProxied || false
}));
setMeta(accountOrAddress?.meta);
setName(accountOrAddress?.meta.name || '');
setSortedTags(accountOrAddress?.meta.tags?.sort() || []);
} catch {
// ignore
}
}
}, [identity, isAccount, isAddress, value]);
const onSaveName = useCallback(
(): void => {
if (isEditingName) {
toggleIsEditingName();
}
const meta = { name, whenEdited: Date.now() };
if (isContract) {
try {
if (value) {
const originalMeta = keyring.getAddress(value)?.meta;
keyring.saveContract(value, { ...originalMeta, ...meta });
}
} catch (error) {
console.error(error);
}
} else if (value) {
try {
const pair = keyring.getPair(value);
pair && keyring.saveAccountMeta(pair, meta);
} catch {
const pair = keyring.getAddress(value);
if (pair) {
keyring.saveAddress(value, meta);
} else {
keyring.saveAddress(value, { genesisHash: api.genesisHash.toHex(), ...meta });
}
}
}
},
[api, isContract, isEditingName, name, toggleIsEditingName, value]
);
const onSaveTags = useCallback(
(): void => {
const meta = { tags, whenEdited: Date.now() };
if (isContract) {
try {
if (value) {
const originalMeta = keyring.getAddress(value)?.meta;
value && keyring.saveContract(value, { ...originalMeta, ...meta });
}
} catch (error) {
console.error(error);
}
} else if (value) {
try {
const currentKeyring = keyring.getPair(value);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch {
keyring.saveAddress(value, meta);
}
}
},
[isContract, tags, value]
);
const onForgetAddress = useCallback(
(): void => {
if (isEditingName) {
toggleIsEditingName();
}
if (isEditingTags) {
toggleIsEditingTags();
}
try {
value && keyring.forgetAddress(value);
} catch (e) {
console.error(e);
}
},
[isEditingName, isEditingTags, toggleIsEditingName, toggleIsEditingTags, value]
);
const onSetGenesisHash = useCallback(
(genesisHash: HexString | null): void => {
if (value) {
const account = keyring.getPair(value);
account && keyring.saveAccountMeta(account, { ...account.meta, genesisHash });
setGenesisHash(genesisHash);
}
},
[value]
);
const setTags = useCallback(
(tags: string[]) => setSortedTags(tags.sort()),
[]
);
const isEditing = useCallback(() => isEditingName || isEditingTags, [isEditingName, isEditingTags]);
return useMemo(() => ({
accountIndex,
flags,
genesisHash,
identity,
isEditing,
isEditingName,
isEditingTags,
isNull: !value,
meta,
name,
onForgetAddress,
onSaveName,
onSaveTags,
onSetGenesisHash,
setIsEditingName,
setIsEditingTags,
setName,
setTags,
tags,
toggleIsEditingName,
toggleIsEditingTags
}), [accountIndex, flags, genesisHash, identity, isEditing, isEditingName, isEditingTags, meta, name, onForgetAddress, onSaveName, onSaveTags, onSetGenesisHash, setIsEditingName, setIsEditingTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags, value]);
}
export const useAccountInfo = createNamedHook('useAccountInfo', useAccountInfoImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Accounts } from './ctx/types.js';
import { useContext } from 'react';
import { KeyringCtx } from './ctx/Keyring.js';
import { createNamedHook } from './createNamedHook.js';
function useAccountsImpl (): Accounts {
return useContext(KeyringCtx).accounts;
}
export const useAccounts = createNamedHook('useAccounts', useAccountsImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Addresses } from './ctx/types.js';
import { useContext } from 'react';
import { KeyringCtx } from './ctx/Keyring.js';
import { createNamedHook } from './createNamedHook.js';
function useAddressesImpl (): Addresses {
return useContext(KeyringCtx).addresses;
}
export const useAddresses = createNamedHook('useAddresses', useAddressesImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiProps } from '@pezkuwi/react-api/types';
import { useContext } from 'react';
import { ApiCtx } from './ctx/Api.js';
import { createNamedHook } from './createNamedHook.js';
function useApiImpl (): ApiProps {
return useContext(ApiCtx);
}
export const useApi = createNamedHook('useApi', useApiImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiStats } from './ctx/types.js';
import { useContext } from 'react';
import { ApiStatsCtx } from './ctx/ApiStats.js';
import { createNamedHook } from './createNamedHook.js';
function useApiStatsImpl (): ApiStats[] {
return useContext(ApiStatsCtx);
}
export const useApiStats = createNamedHook('useApiStats', useApiStatsImpl);
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ProviderInterface } from '@pezkuwi/rpc-provider/types';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { createWsEndpoints, typesBundle } from '@pezkuwi/apps-config';
import { settings } from '@pezkuwi/ui-settings';
import { arrayShuffle, isString } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useIsMountedRef } from './useIsMountedRef.js';
const endpoints = createWsEndpoints();
function disconnect (provider: ProviderInterface | null): null {
provider?.disconnect().catch(console.error);
return null;
}
function useApiUrlImpl (url?: null | string | string[]): ApiPromise | null {
const providerRef = useRef<ProviderInterface | null>(null);
const mountedRef = useIsMountedRef();
const [state, setState] = useState<ApiPromise | null>(null);
const groupedEndpoints = useMemo(() => {
const map = new Map<string, string>();
for (const endpoint of endpoints) {
const rpcProvider = endpoint.textBy;
if (typeof rpcProvider === 'string' && rpcProvider.length > 0 && rpcProvider !== 'Placeholder') {
map.set(endpoint.value, rpcProvider);
}
}
return map;
}, []);
const urls = useMemo(
() => {
let validUrls = url
? isString(url)
? [url]
: arrayShuffle(url.filter((u) => !u.startsWith('light://')))
: [];
const apiUrl = settings.apiUrl; // Selected chain URL
const apiUrlProvider = groupedEndpoints.get(apiUrl);
if (groupedEndpoints.has(apiUrl)) {
const matchIndex = validUrls.findIndex(
(u) => groupedEndpoints.get(u) === apiUrlProvider
);
// Move the RPC URL to the first position if it's provider matches the selected chain's provider
if (matchIndex > -1) {
const [match] = validUrls.splice(matchIndex, 1);
validUrls = [match, ...validUrls];
}
}
return validUrls;
},
[groupedEndpoints, url]
);
useEffect((): () => void => {
return (): void => {
providerRef.current = disconnect(providerRef.current);
};
}, []);
useEffect((): void => {
setState(null);
providerRef.current = disconnect(providerRef.current);
urls.length &&
ApiPromise
.create({
provider: (providerRef.current = new WsProvider(urls)),
typesBundle
})
.then((api) => mountedRef.current && setState(api))
.catch(console.error);
}, [mountedRef, providerRef, urls]);
return state;
}
export const useApiUrl = createNamedHook('useApiUrl', useApiUrlImpl);
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { StorageKey, u32 } from '@pezkuwi/types';
import type { EventRecord } from '@pezkuwi/types/interfaces';
import type { Changes } from './useEventChanges.js';
import { createNamedHook, useApi, useEventChanges, useMapKeys } from './index.js';
const EMPTY_PARAMS: unknown[] = [];
const OPT_KEY = {
transform: (keys: StorageKey<[u32]>[]): u32[] =>
keys.map(({ args: [id] }) => id).filter((id) => id !== undefined)
};
function filter (records: EventRecord[]): Changes<u32> {
const added: u32[] = [];
const removed: u32[] = [];
records.forEach(({ event: { data: [id], method } }): void => {
if (method === 'Created' || method === 'ForceCreated') {
added.push(id as u32);
} else {
removed.push(id as u32);
}
});
return { added, removed };
}
function useAssetIdsImpl (): u32[] | undefined {
const { api } = useApi();
const startValue = useMapKeys(api.query.assets?.asset, EMPTY_PARAMS, OPT_KEY) || [];
return useEventChanges([
api.events.assets?.Created,
api.events.assets?.Destroyed,
api.events.assets?.ForceCreated
], filter, startValue);
}
export const useAssetIds = createNamedHook('useAssetIds', useAssetIdsImpl);
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@pezkuwi/types';
import type { AccountId } from '@pezkuwi/types/interfaces';
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata } from '@pezkuwi/types/lookup';
import type { BN } from '@pezkuwi/util';
import type { AssetInfo } from './types.js';
import { useEffect, useMemo, useState } from 'react';
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
const EMPTY_FLAGS = {
isAdminMe: false,
isFreezerMe: false,
isIssuerMe: false,
isOwnerMe: false
};
const QUERY_OPTS = { withParams: true };
function isAccount (allAccounts: string[], accountId: AccountId): boolean {
const address = accountId.toString();
return allAccounts.some((a) => a === address);
}
function extractInfo (allAccounts: string[], id: BN, optDetails: Option<PalletAssetsAssetDetails>, metadata: PalletAssetsAssetMetadata): AssetInfo {
const details = optDetails.unwrapOr(null);
return {
...(details
? {
isAdminMe: isAccount(allAccounts, details.admin),
isFreezerMe: isAccount(allAccounts, details.freezer),
isIssuerMe: isAccount(allAccounts, details.issuer),
isOwnerMe: isAccount(allAccounts, details.owner)
}
: EMPTY_FLAGS
),
details,
id,
key: id.toString(),
metadata: metadata.isEmpty
? null
: metadata
};
}
function useAssetInfosImpl (ids?: BN[]): AssetInfo[] | undefined {
const { api } = useApi();
const { allAccounts } = useAccounts();
const isReady = useMemo(() => !!ids?.length && !!api.tx.assets?.setMetadata && !!api.tx.assets?.transferKeepAlive, [api.tx.assets?.setMetadata, api.tx.assets?.transferKeepAlive, ids?.length]);
const metadata = useCall<[[BN[]], PalletAssetsAssetMetadata[]]>(isReady && api.query.assets.metadata.multi, [ids], QUERY_OPTS);
const details = useCall<[[BN[]], Option<PalletAssetsAssetDetails>[]]>(isReady && api.query.assets.asset.multi, [ids], QUERY_OPTS);
const [state, setState] = useState<AssetInfo[] | undefined>();
useEffect((): void => {
if (details && metadata) {
(details[0][0].length === metadata[0][0].length)
? setState(
details[0][0].map((id, index) =>
extractInfo(allAccounts, id, details[1][index], metadata[1][index])
)
)
: setState((prev) => prev || []);
}
}, [allAccounts, details, ids, metadata]);
return state;
}
export const useAssetInfos = createNamedHook('useAssetInfos', useAssetInfosImpl);
@@ -0,0 +1,67 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { DeriveSessionIndexes } from '@pezkuwi/api-derive/types';
import type { Option } from '@pezkuwi/types';
import type { EraIndex } from '@pezkuwi/types/interfaces';
import type { PalletStakingUnappliedSlash } from '@pezkuwi/types/lookup';
import { useEffect, useMemo, useState } from 'react';
import { BN, BN_HUNDRED, BN_ONE, BN_ZERO } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useIsMountedRef } from './useIsMountedRef.js';
type Unsub = () => void;
function useAvailableSlashesImpl (apiOverride?: ApiPromise): [BN, PalletStakingUnappliedSlash[]][] {
const { api: connectedApi } = useApi();
const api = useMemo(() => apiOverride ?? connectedApi, [apiOverride, connectedApi]);
const indexes = useCall<DeriveSessionIndexes>(api.derive.session?.indexes);
const earliestSlash = useCall<Option<EraIndex>>(api.query.staking?.earliestUnappliedSlash);
const mountedRef = useIsMountedRef();
const [slashes, setSlashes] = useState<[BN, PalletStakingUnappliedSlash[]][]>([]);
useEffect((): Unsub => {
let unsub: Unsub | undefined;
const [from, offset] = api.query.staking?.earliestUnappliedSlash
? [earliestSlash?.unwrapOr(null), BN_ZERO]
// future depth (one more than activeEra for delay)
: [indexes?.activeEra, BN_ONE.add(api.consts.staking?.slashDeferDuration || BN_HUNDRED)];
if (mountedRef.current && indexes && from) {
const range: BN[] = [];
const end = indexes.activeEra.add(offset);
let start = new BN(from);
while (start.lte(end)) {
range.push(start);
start = start.add(BN_ONE);
}
if (range.length) {
(async (): Promise<void> => {
unsub = await api.query.staking.unappliedSlashes.multi(range, (values): void => {
mountedRef.current && setSlashes(
values
.map((value, index): [BN, PalletStakingUnappliedSlash[]] => [from.addn(index), value])
.filter(([, slashes]) => slashes.length)
);
});
})().catch(console.error);
}
}
return (): void => {
unsub && unsub();
};
}, [api, earliestSlash, indexes, mountedRef]);
return slashes;
}
export const useAvailableSlashes = createNamedHook('useAvailableSlashes', useAvailableSlashesImpl);
@@ -0,0 +1,22 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll } from '@pezkuwi/api-derive/types';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
/**
* Gets the account full balance information
*
* @param accountAddress The account address of which balance is to be returned
* @returns full information about account's balances
*/
function useBalancesAllImpl (accountAddress: string): DeriveBalancesAll | undefined {
const { api } = useApi();
return useCall<DeriveBalancesAll>(api.derive.balances?.all, [accountAddress]);
}
export const useBalancesAll = createNamedHook('useBalancesAll', useBalancesAllImpl);
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Header } from '@pezkuwi/types/interfaces';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
const OPT = {
transform: (header: Header) => header.hash.toHex()
};
function useBestHashImpl (): string | undefined {
const { api } = useApi();
return useCall<string>(api.rpc.chain.subscribeNewHeads, undefined, OPT);
}
export const useBestHash = createNamedHook('useBestHash', useBestHashImpl);
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber } from '@pezkuwi/types/interfaces';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useStakingAsyncApis } from './useStakingAsyncApis.js';
function useBestNumberImpl (): BlockNumber | undefined {
const { api } = useApi();
return useCall<BlockNumber>(api.derive.chain.bestNumber);
}
function useBestNumberRelayImpl (): BlockNumber | undefined {
const { api } = useApi();
const { isStakingAsync, rcApi } = useStakingAsyncApis();
return useCall<BlockNumber>((isStakingAsync ? rcApi : api)?.derive.chain.bestNumber);
}
export const useBestNumber = createNamedHook('useBestNumber', useBestNumberImpl);
export const useBestNumberRelay = createNamedHook('useBestNumberRelay', useBestNumberRelayImpl);
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockAuthors } from './ctx/types.js';
import { useContext } from 'react';
import { BlockAuthorsCtx } from './ctx/BlockAuthors.js';
import { createNamedHook } from './createNamedHook.js';
function useBlockAuthorsImpl (): BlockAuthors {
return useContext(BlockAuthorsCtx);
}
export const useBlockAuthors = createNamedHook('useBlockAuthors', useBlockAuthorsImpl);
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockEvents } from './ctx/types.js';
import { useContext } from 'react';
import { BlockEventsCtx } from './ctx/BlockEvents.js';
import { createNamedHook } from './createNamedHook.js';
function useBlockEventsImpl (): BlockEvents {
return useContext(BlockEventsCtx);
}
export const useBlockEvents = createNamedHook('useBlockEvents', useBlockEventsImpl);
@@ -0,0 +1,57 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { BabeGenesisConfiguration } from '@pezkuwi/types/interfaces/babe';
import { useMemo } from 'react';
import { BN, BN_THOUSAND, BN_TWO, bnMin } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { A_DAY } from './useBlocksPerDays.js';
import { useCall } from './useCall.js';
// Some chains incorrectly use these, i.e. it is set to values such as 0 or even 2
// Use a low minimum validity threshold to check these against
const THRESHOLD = BN_THOUSAND.div(BN_TWO);
const DEFAULT_TIME = new BN(6_000);
function calcInterval (api: ApiPromise): BN {
return bnMin(A_DAY, (
// Babe, e.g. Relay chains (Bizinikiwi defaults)
api.consts.babe?.expectedBlockTime ||
// POW, eg. Kulupu
api.consts.difficulty?.targetBlockTime ||
// Subspace
api.consts.subspace?.expectedBlockTime || (
// Check against threshold to determine value validity
api.consts.timestamp?.minimumPeriod.gte(THRESHOLD)
// Default minimum period config
? api.consts.timestamp.minimumPeriod.mul(BN_TWO)
: api.query.teyrchainSystem
// default guess for a teyrchain
? api.consts.aura?.slotDuration ?? DEFAULT_TIME.mul(BN_TWO)
// default guess for others
: DEFAULT_TIME
)
));
}
function useBlockIntervalImpl (apiOverride?: ApiPromise | null): BN {
const { api } = useApi();
const currApi = apiOverride || api;
const blockTimeAura = useCall<BN>(currApi.call.auraApi?.slotDuration && currApi.call.auraApi.slotDuration, []);
const blockTimeBabe = useCall(currApi.call.babeApi?.configuration && currApi.call.babeApi.configuration, [], {
transform: (data: BabeGenesisConfiguration | undefined) => data?.slotDuration
});
return useMemo(
() => (blockTimeAura || blockTimeBabe) ?? calcInterval(currApi),
[blockTimeAura, blockTimeBabe, currApi]
);
}
export const useBlockInterval = createNamedHook('useBlockInterval', useBlockIntervalImpl);
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { BN } from '@pezkuwi/util';
import type { Time } from '@pezkuwi/util/types';
import { useMemo } from 'react';
import { BN_MAX_INTEGER, BN_ONE, bnMin, bnToBn, extractTime } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useTranslation } from './translate.js';
import { useBlockInterval } from './useBlockInterval.js';
type Result = [blockInterval: number, timeStr: string, time: Time];
export function calcBlockTime (blockTime: BN, blocks: BN, t: (key: string, options?: { replace: Record<string, unknown> }) => string): Result {
// in the case of excessively large locks, limit to the max JS integer value
const value = bnMin(BN_MAX_INTEGER, blockTime.mul(blocks)).toNumber();
// time calculations are using the absolute value (< 0 detection only on strings)
const time = extractTime(Math.abs(value));
const { days, hours, minutes, seconds } = time;
return [
blockTime.toNumber(),
`${value < 0 ? '+' : ''}${[
days
? (days > 1)
? t('{{days}} days', { replace: { days } })
: t('1 day')
: null,
hours
? (hours > 1)
? t('{{hours}} hrs', { replace: { hours } })
: t('1 hr')
: null,
minutes
? (minutes > 1)
? t('{{minutes}} mins', { replace: { minutes } })
: t('1 min')
: null,
seconds
? (seconds > 1)
? t('{{seconds}} s', { replace: { seconds } })
: t('1 s')
: null
]
.filter((s): s is string => !!s)
.slice(0, 2)
.join(' ')}`,
time
];
}
function useBlockTimeImpl (blocks: number | BN = BN_ONE, apiOverride?: ApiPromise | null): Result {
const { t } = useTranslation();
const blockTime = useBlockInterval(apiOverride);
return useMemo(
() => calcBlockTime(blockTime, bnToBn(blocks), t),
[blockTime, blocks, t]
);
}
export const useBlockTime = createNamedHook('useBlockTime', useBlockTimeImpl);
@@ -0,0 +1,22 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useMemo } from 'react';
import { BN, bnToBn } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useBlockInterval } from './useBlockInterval.js';
export const A_DAY = new BN(24 * 60 * 60 * 1000);
function useBlocksPerDaysImpl (days: BN | number = 1): BN {
const blockTime = useBlockInterval();
return useMemo(
() => A_DAY.mul(bnToBn(days)).div(blockTime),
[blockTime, days]
);
}
export const useBlocksPerDays = createNamedHook('useBlocksPerDays', useBlocksPerDaysImpl);
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option } from '@pezkuwi/types';
import type { PalletBrokerConfigRecord } from '@pezkuwi/types/lookup';
import type { PalletBrokerConfigRecord as SimplifiedPalletBrokerConfigRecord } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@pezkuwi/react-hooks';
function extractInfo (config: Option<PalletBrokerConfigRecord>): SimplifiedPalletBrokerConfigRecord {
const c = config.unwrap();
return {
advanceNotice: c.advanceNotice?.toNumber(),
contributionTimeout: c.contributionTimeout?.toNumber(),
idealBulkProportion: c.idealBulkProportion,
interludeLength: c.interludeLength?.toNumber(),
leadinLength: c.leadinLength?.toNumber(),
limitCoresOffered: c.limitCoresOffered?.isSome ? c.limitCoresOffered?.unwrap().toNumber() : 0,
regionLength: c.regionLength?.toNumber(),
renewalBump: c.renewalBump
};
}
function useBrokerConfigImpl (api: ApiPromise, ready: boolean) {
const config = useCall<Option<PalletBrokerConfigRecord>>(ready && api?.query.broker.configuration);
const [state, setState] = useState<SimplifiedPalletBrokerConfigRecord | undefined>();
useEffect((): void => {
!!config && !!config.isSome && !!config.toJSON() &&
setState(
extractInfo(config)
);
}, [config]);
return state;
}
export const useBrokerConfig = createNamedHook('useBrokerConfig', useBrokerConfigImpl);
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Vec } from '@pezkuwi/types';
import type { PalletBrokerLeaseRecordItem } from '@pezkuwi/types/lookup';
import type { LegacyLease } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@pezkuwi/react-hooks';
function useBrokerLeasesImpl (api: ApiPromise, ready: boolean): LegacyLease[] | undefined {
const leases = useCall<Vec<PalletBrokerLeaseRecordItem>>(ready && api?.query?.broker?.leases);
const [state, setState] = useState<LegacyLease[]>();
useEffect((): void => {
if (!leases) {
return;
}
setState(
leases.map((info, index: number) => ({
core: index,
task: info.task.toString(),
until: info.until.toNumber()
})
));
}, [leases]);
return state;
}
export const useBrokerLeases = createNamedHook('useBrokerLeases', useBrokerLeasesImpl);
@@ -0,0 +1,75 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option, StorageKey, u32 } from '@pezkuwi/types';
import type { PalletBrokerPotentialRenewalId, PalletBrokerPotentialRenewalRecord } from '@pezkuwi/types/lookup';
import type { PotentialRenewal } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall, useMapKeys } from '@pezkuwi/react-hooks';
import { BN_ZERO } from '@pezkuwi/util';
import { processHexMask } from './utils/dataProcessing.js';
function extractInfo (info: Option<PalletBrokerPotentialRenewalRecord>, item: PalletBrokerPotentialRenewalId): PotentialRenewal | undefined {
const unwrapped: PalletBrokerPotentialRenewalRecord | null = info.isSome ? info.unwrap() : null;
let mask: string[] = [];
let task = '';
if (!unwrapped) {
return;
}
const completion = unwrapped?.completion;
if (completion?.isComplete) {
const complete = completion?.asComplete[0];
task = complete.assignment.isTask ? complete?.assignment.asTask.toString() : complete?.assignment.isPool ? 'Pool' : 'Idle';
mask = processHexMask(complete.mask);
} else if (completion?.isPartial) {
mask = processHexMask(completion?.asPartial);
task = '';
} else {
mask = [];
}
return {
// How much of a core has been assigned or, if completely assigned, the workload itself.
completion: completion?.type,
core: item?.core.toNumber(),
mask,
maskBits: mask?.length,
price: unwrapped?.price.toBn() || BN_ZERO,
task,
when: item?.when.toNumber()
};
}
const OPT_KEY = {
transform: (keys: StorageKey<[u32]>[]): u32[] =>
keys.map(({ args: [id] }) => id)
};
function useBrokerPotentialRenewalsImpl (api: ApiPromise, ready: boolean): PotentialRenewal[] | undefined {
const keys = useMapKeys(ready && api?.query.broker.potentialRenewals, [], OPT_KEY);
const potentialRenewals = useCall<[[PalletBrokerPotentialRenewalId[]], Option<PalletBrokerPotentialRenewalRecord>[]]>(ready && api?.query.broker.potentialRenewals.multi, [keys], { withParams: true });
const [state, setState] = useState<PotentialRenewal[] | undefined>();
useEffect((): void => {
if (!potentialRenewals) {
return;
}
const renewals = potentialRenewals[0][0].map((info, index) => extractInfo(potentialRenewals[1][index], info));
setState(renewals.filter((renewal): renewal is PotentialRenewal => !!renewal));
}, [potentialRenewals]);
return state;
}
export const useBrokerPotentialRenewals = createNamedHook('useBrokerPotentialRenewals', useBrokerPotentialRenewalsImpl);
@@ -0,0 +1,38 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Vec } from '@pezkuwi/types';
import type { PalletBrokerScheduleItem } from '@pezkuwi/types/lookup';
import type { Reservation } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@pezkuwi/react-hooks';
import { processHexMask } from './utils/dataProcessing.js';
function useBrokerReservationsImpl (api: ApiPromise, ready: boolean): Reservation[] | undefined {
const reservations = useCall<[any, Vec<Vec<PalletBrokerScheduleItem>>[]]>(ready && api?.query.broker.reservations);
const [state, setState] = useState<Reservation[]>();
useEffect((): void => {
if (!reservations) {
return;
}
setState(
reservations.map((info: PalletBrokerScheduleItem[]) => {
return {
mask: processHexMask(info[0]?.mask),
maskBits: processHexMask(info[0]?.mask)?.length ?? 0,
task: info[0]?.assignment?.isTask ? info[0]?.assignment?.asTask.toString() : info[0]?.assignment?.isPool ? 'Pool' : ''
};
}
));
}, [reservations]);
return state;
}
export const useBrokerReservations = createNamedHook('useBrokerReservations', useBrokerReservationsImpl);
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option } from '@pezkuwi/types';
import type { PalletBrokerSaleInfoRecord } from '@pezkuwi/types/lookup';
import type { PalletBrokerSaleInfoRecord as SimplifiedPalletBrokerSaleInfoRecord } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@pezkuwi/react-hooks';
import { BN } from '@pezkuwi/util';
function extractInfo (record: Option<PalletBrokerSaleInfoRecord>): SimplifiedPalletBrokerSaleInfoRecord {
const v = record.unwrap();
return {
coresOffered: v.coresOffered?.toNumber(),
coresSold: v.coresSold?.toNumber(),
endPrice: v.endPrice,
firstCore: v.firstCore?.toNumber(),
idealCoresSold: v.idealCoresSold?.toNumber(),
leadinLength: v.leadinLength?.toNumber(),
regionBegin: v.regionBegin?.toNumber(),
regionEnd: v.regionEnd?.toNumber(),
saleStart: v.saleStart?.toNumber(),
selloutPrice: v.selloutPrice?.isSome ? v.selloutPrice?.unwrap() : new BN(0)
};
}
function useBrokerSalesInfoImpl (api: ApiPromise, ready: boolean) {
const record = useCall<Option<PalletBrokerSaleInfoRecord>>(ready && api?.query.broker.saleInfo);
const [state, setState] = useState<SimplifiedPalletBrokerSaleInfoRecord | undefined>();
useEffect((): void => {
!!record && !!record.isSome && !!record.toJSON() &&
setState(
extractInfo(record)
);
}, [record]);
return state;
}
export const useBrokerSalesInfo = createNamedHook('useBrokerSalesInfo', useBrokerSalesInfoImpl);
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option } from '@pezkuwi/types';
import type { PalletBrokerStatusRecord } from '@pezkuwi/types/lookup';
import type { BrokerStatus } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@pezkuwi/react-hooks';
function useBrokerStatusImpl (api: ApiPromise, ready: boolean): BrokerStatus | undefined {
const status = useCall<Option<PalletBrokerStatusRecord>>(ready && api?.query.broker?.status);
const [state, setState] = useState<BrokerStatus | undefined>();
useEffect((): void => {
if (!!status && status.isSome) {
const s = status.unwrap();
setState({
coreCount: s.coreCount?.toNumber(),
lastCommittedTimeslice: s.lastCommittedTimeslice?.toNumber(),
lastTimeslice: s.lastTimeslice?.toNumber(),
privatePoolSize: s.privatePoolSize?.toNumber(),
systemPoolSize: s.systemPoolSize?.toNumber()
});
}
}, [status]);
return state;
}
export const useBrokerStatus = createNamedHook('useBrokerStatus', useBrokerStatusImpl);
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useMemo } from 'react';
import store from 'store';
import { useApi } from './useApi.js';
// create a chain-specific key for the local cache
// FIXME Since we use generics, this cannot be a createNamedHook as of yet
export function useCacheKey <T> (storageKeyBase: string): [(defaultValue?: T) => T | undefined, (value: T) => T] {
const { api, isDevelopment } = useApi();
const storageKey = useMemo(
() => `${storageKeyBase}:${isDevelopment ? 'development' : api.genesisHash.toHex()}`,
[api, isDevelopment, storageKeyBase]
);
// FIXME both these want "T"... incorrect
const getter = useCallback(
(): T | undefined => store.get(storageKey) as T,
[storageKey]
);
const setter = useCallback(
(value: T): T => store.set(storageKey, value) as T,
[storageKey]
);
return [getter, setter];
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { PromiseResult, QueryableStorageEntry } from '@pezkuwi/api/types';
import type { StorageEntryTypeLatest } from '@pezkuwi/types/interfaces';
import type { AnyFunction, Codec } from '@pezkuwi/types/types';
import type { CallOptions, CallParam, CallParams } from './types.js';
import type { MountedRef } from './useIsMountedRef.js';
import { useEffect, useRef, useState } from 'react';
import { isFunction, isNull, isUndefined, nextTick } from '@pezkuwi/util';
import { useApi } from './useApi.js';
import { useIsMountedRef } from './useIsMountedRef.js';
type VoidFn = () => void;
// This should be VoidFn, however the API actually does allow us to use any general single-shot queries with
// a result callback, so `api.query.system.account.at(<blockHash>, <account>, (info) => {... })` does work
// (The same applies to e.g. keys or entries). So where we actually use the unsub, we cast `unknown` to `VoidFn`
// to cater for our usecase.
type TrackFnResult = Promise<unknown>;
interface QueryTrackFn {
(...params: CallParam[]): TrackFnResult;
meta?: {
type?: StorageEntryTypeLatest;
};
}
interface QueryMapFn extends QueryTrackFn {
meta: {
type: StorageEntryTypeLatest;
};
}
type QueryFn =
QueryableStorageEntry<'promise', []> |
QueryableStorageEntry<'promise', []>['entries'] |
QueryableStorageEntry<'promise', []>['keys'] |
QueryableStorageEntry<'promise', []>['multi'];
type CallFn = (...params: unknown[]) => Promise<VoidFn>;
export type TrackFn = PromiseResult<AnyFunction> | QueryFn;
export interface Tracker {
error: Error | null;
fn: TrackFn | undefined | null | false;
isActive: boolean;
serialized: string | null;
subscriber: TrackFnResult | null;
type: 'useCall' | 'useCallMulti';
}
interface TrackerRef {
current: Tracker;
}
// the default transform, just returns what we have
export function transformIdentity <T> (value: unknown): T {
return value as T;
}
function isMapFn (fn: unknown): fn is QueryMapFn {
return !!(fn as QueryTrackFn).meta?.type?.isMap;
}
function isQuery (fn: unknown): fn is QueryableStorageEntry<'promise', []> {
return !!fn && !isUndefined((fn as QueryableStorageEntry<'promise', []>).creator);
}
// extract the serialized and mapped params, all ready for use in our call
function extractParams <T> (fn: unknown, params: unknown[], { paramMap = transformIdentity }: CallOptions<T> = {}): [string, CallParams | null] {
return [
JSON.stringify({ f: (fn as { name: string })?.name, p: params }),
params.length === 0 || !params.some((param) => isNull(param) || isUndefined(param))
? paramMap(params)
: null
];
}
export function handleError (error: Error, tracker: TrackerRef, fn?: unknown): void {
console.error(
tracker.current.error = new Error(`${tracker.current.type}(${
isQuery(fn)
? `${fn.creator.section}.${fn.creator.method}`
: '...'
}):: ${error.message}:: ${error.stack || '<unknown>'}`)
);
}
// unsubscribe and remove from the tracker
export function unsubscribe (tracker: TrackerRef): void {
tracker.current.isActive = false;
if (tracker.current.subscriber) {
tracker.current.subscriber
.then((u) => isFunction(u) && (u as VoidFn)())
.catch((e) => handleError(e as Error, tracker));
tracker.current.subscriber = null;
}
}
// subscribe, trying to play nice with the browser threads
function subscribe <T> (api: ApiPromise, mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: any) => void, { transform = transformIdentity, withParams, withParamsTransform }: CallOptions<T> = {}): void {
const validParams = params.filter((p) => !isUndefined(p));
unsubscribe(tracker);
nextTick((): void => {
if (mountedRef.current) {
const canQuery = !!fn && (
isMapFn(fn)
? fn.meta.type.asMap.hashers.length === validParams.length
: true
);
if (canQuery) {
// swap to active mode
tracker.current.isActive = true;
tracker.current.subscriber = (fn as CallFn)(...params, (value: Codec): void => {
// we use the isActive flag here since .subscriber may not be set on immediate callback)
if (mountedRef.current && tracker.current.isActive) {
try {
setValue(
withParams
? [params, transform(value, api)]
: withParamsTransform
? transform([params, value], api)
: transform(value, api)
);
} catch (error) {
handleError(error as Error, tracker, fn);
}
}
}).catch((error) => handleError(error as Error, tracker, fn));
} else {
tracker.current.subscriber = null;
}
}
});
}
export function throwOnError (tracker: Tracker): void {
if (tracker.error) {
const error = tracker.error;
tracker.error = null;
throw error;
}
}
// tracks a stream, typically an api.* call (derive, rpc, query) that
// - returns a promise with an unsubscribe function
// - has a callback to set the value
// FIXME The typings here need some serious TLC
// FIXME This is generic, we cannot really use createNamedHook
export function useCall <T> (fn: TrackFn | undefined | null | false, params?: CallParams | null, options?: CallOptions<T>): T | undefined {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const tracker = useRef<Tracker>({ error: null, fn: null, isActive: false, serialized: null, subscriber: null, type: 'useCall' });
const [value, setValue] = useState<T | undefined>(options?.defaultValue);
// initial effect, we need an un-subscription
useEffect((): () => void => {
return () => unsubscribe(tracker);
}, []);
// on changes, re-subscribe
useEffect((): void => {
// check if we have a function & that we are mounted
if (mountedRef.current && fn) {
const [serialized, mappedParams] = extractParams(fn, params || [], options);
if (mappedParams && ((fn !== tracker.current.fn) || (serialized !== tracker.current.serialized))) {
tracker.current.fn = fn;
tracker.current.serialized = serialized;
subscribe(api, mountedRef, tracker, fn, mappedParams, setValue, options);
}
}
}, [api, fn, options, mountedRef, params]);
// throwOnError(tracker.current);
return value;
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { QueryableStorageMultiArg } from '@pezkuwi/api/types';
import type { Tracker } from './useCall.js';
import type { MountedRef } from './useIsMountedRef.js';
import { useEffect, useRef, useState } from 'react';
import { isUndefined, nextTick } from '@pezkuwi/util';
import { useApi } from './useApi.js';
import { handleError, transformIdentity, unsubscribe } from './useCall.js';
import { useIsMountedRef } from './useIsMountedRef.js';
interface TrackerRef {
current: Tracker;
}
interface CallOptions <T> {
defaultValue?: T;
transform?: (value: any, api: ApiPromise) => T;
}
// subscribe, trying to play nice with the browser threads
function subscribe <T> (api: ApiPromise, mountedRef: MountedRef, tracker: TrackerRef, calls: QueryableStorageMultiArg<'promise'>[], setValue: (value: T) => void, { transform = transformIdentity }: CallOptions<T> = {}): void {
unsubscribe(tracker);
nextTick((): void => {
if (mountedRef.current) {
const included = calls.map((c) => !!c && (!Array.isArray(c) || !!c[0]));
const filtered = calls.filter((_, index) => included[index]);
if (filtered.length) {
// swap to active mode
tracker.current.isActive = true;
tracker.current.subscriber = api.queryMulti(filtered, (value): void => {
// we use the isActive flag here since .subscriber may not be set on immediate callback)
if (mountedRef.current && tracker.current.isActive) {
let valueIndex = -1;
try {
setValue(
transform(
calls.map((_, index) =>
included[index]
? value[++valueIndex]
: undefined
),
api
)
);
} catch (error) {
handleError(error as Error, tracker);
}
}
}).catch((error) => handleError(error as Error, tracker));
} else {
tracker.current.subscriber = null;
}
}
});
}
// very much copied from useCall
// FIXME This is generic, we cannot really use createNamedHook
export function useCallMulti <T> (calls?: QueryableStorageMultiArg<'promise'>[] | null | false, options?: CallOptions<T>): T {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const tracker = useRef<Tracker>({ error: null, fn: null, isActive: false, serialized: null, subscriber: null, type: 'useCallMulti' });
const [value, setValue] = useState<T>(() => (isUndefined(options?.defaultValue) ? [] : options?.defaultValue) as unknown as T);
// initial effect, we need an un-subscription
useEffect((): () => void => {
return () => unsubscribe(tracker);
}, []);
// on changes, re-subscribe
useEffect((): void => {
// check if we have a function & that we are mounted
if (mountedRef.current && calls) {
const serialized = JSON.stringify(calls);
if (serialized !== tracker.current.serialized) {
tracker.current.serialized = serialized;
subscribe(api, mountedRef, tracker, calls, setValue, options);
}
}
}, [api, calls, options, mountedRef]);
// throwOnError(tracker.current);
return value;
}
@@ -0,0 +1,32 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { CollectiveType } from './types.js';
import { useMemo } from 'react';
import { useApi } from '@pezkuwi/react-hooks';
import { isFunction } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
function useCollectiveInstanceImpl (instanceType: CollectiveType, instanceIndex?: number): CollectiveType | null {
const { api } = useApi();
return useMemo(
(): CollectiveType | null => {
const index = instanceIndex || 0;
const instances = api.registry.getModuleInstances(api.runtimeVersion.specName.toString(), instanceType);
const instance = instances && (index < instances.length)
? instances[index] as 'council'
: instanceType;
return api.tx[instance] && isFunction(api.tx[instance].close)
? instance
: null;
},
[api, instanceIndex, instanceType]
);
}
export const useCollectiveInstance = createNamedHook('useCollectiveInstance', useCollectiveInstanceImpl);
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId } from '@pezkuwi/types/interfaces';
import type { CollectiveType } from './types.js';
import { useMemo } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
interface Result {
isMember: boolean;
members: string[];
prime?: string | null;
}
const OPT_MEM = {
transform: (accounts: AccountId[]): string[] =>
accounts.map((a) => a.toString())
};
const OPT_PRM = {
transform: (accountId: AccountId | null): string | null =>
accountId?.toString() || null
};
function useCollectiveMembersImpl (collective: CollectiveType): Result {
const { api } = useApi();
const { allAccounts } = useAccounts();
const members = useCall(api.derive[collective as 'council']?.members, [], OPT_MEM);
const prime = useCall(api.derive[collective as 'council']?.prime, [], OPT_PRM);
return useMemo(
() => ({
isMember: (members || []).some((a) => allAccounts.includes(a)),
members: (members || []),
prime
}),
[allAccounts, members, prime]
);
}
export const useCollectiveMembers = createNamedHook('useCollectiveMembers', useCollectiveMembersImpl);
@@ -0,0 +1,70 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { StorageKey, u32 } from '@pezkuwi/types';
import type { PalletBrokerCoretimeInterfaceCoreAssignment, PezkuwiRuntimeTeyrchainsAssignerCoretimeAssignmentState, PezkuwiRuntimeTeyrchainsAssignerCoretimeCoreDescriptor, PezkuwiRuntimeTeyrchainsAssignerCoretimeQueueDescriptor, PezkuwiRuntimeTeyrchainsAssignerCoretimeWorkState } from '@pezkuwi/types/lookup';
import type { CoreDescriptor } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall, useMapKeys } from '@pezkuwi/react-hooks';
import { BN } from '@pezkuwi/util';
function extractInfo (info: PezkuwiRuntimeTeyrchainsAssignerCoretimeCoreDescriptor, core: number): CoreDescriptor {
const currentWork: PezkuwiRuntimeTeyrchainsAssignerCoretimeWorkState | null = info?.currentWork.isSome ? info.currentWork.unwrap() : null;
const queue: PezkuwiRuntimeTeyrchainsAssignerCoretimeQueueDescriptor | null = info?.queue.isSome ? info.queue.unwrap() : null;
const assignments = currentWork?.assignments || [];
return {
core,
info: {
currentWork: {
assignments: assignments?.map((one: [PalletBrokerCoretimeInterfaceCoreAssignment, PezkuwiRuntimeTeyrchainsAssignerCoretimeAssignmentState]) => {
return ({
isPool: one[0]?.isPool,
isTask: one[0]?.isTask,
ratio: one[1]?.ratio.toNumber(),
remaining: one[1]?.remaining.toNumber(),
task: one[0]?.isTask ? one[0]?.asTask.toString() : one[0]?.isPool ? 'Pool' : 'Idle'
});
}),
endHint: currentWork?.endHint.isSome ? currentWork?.endHint?.unwrap().toBn() : null,
pos: currentWork?.pos.toNumber() || 0,
step: currentWork?.step.toNumber() || 0
},
queue: {
first: queue?.first.toBn() || new BN(0),
last: queue?.last.toBn() || new BN(0)
}
}
};
}
const OPT_KEY = {
transform: (keys: StorageKey<[u32]>[]): u32[] =>
keys.map(({ args: [id] }) => id)
};
function useCoreDescriptorImpl (api: ApiPromise, ready: boolean): CoreDescriptor[] | undefined {
const keys = useMapKeys(ready && api.query.coretimeAssignmentProvider.coreDescriptors, [], OPT_KEY);
const sanitizedKeys = keys?.map((_, index) => {
return index;
});
sanitizedKeys?.pop();
const coreDescriptors = useCall<[[number[]], PezkuwiRuntimeTeyrchainsAssignerCoretimeCoreDescriptor[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true });
const [state, setState] = useState<CoreDescriptor[] | undefined>();
useEffect((): void => {
coreDescriptors &&
setState(coreDescriptors[0][0].map((info, index) => extractInfo(coreDescriptors[1][index], info))
);
}, [coreDescriptors]);
return state;
}
export const useCoreDescriptor = createNamedHook('useCoreDescriptor', useCoreDescriptorImpl);
@@ -0,0 +1,49 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainConstants } from './types.js';
import { useEffect, useState } from 'react';
import { useApi, useBlockTime } from '@pezkuwi/react-hooks';
import { BN, BN_ONE, BN_ZERO } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
function useCoretimeConstsImpl (): ChainConstants | undefined {
const { api, apiCoretime, isApiReady } = useApi();
const [coretimeConstants, setCoretimeConstants] = useState<ChainConstants | undefined>();
const [blockTimeMsRelayChain] = useBlockTime(BN_ONE, api);
const blockTimeMsCoretimeChain = apiCoretime?.consts.aura?.slotDuration;
const blocksPerTimesliceRelayChain = apiCoretime?.consts.broker?.timeslicePeriod;
useEffect(() => {
if (!isApiReady || !blockTimeMsRelayChain || !blockTimeMsCoretimeChain || !blocksPerTimesliceRelayChain) {
return;
}
const blockTimeMsCoretimeChainBN = new BN(blockTimeMsCoretimeChain.toString());
const blocksPerTimesliceRelayChainBN = new BN(blocksPerTimesliceRelayChain);
const relationConstant = blockTimeMsCoretimeChainBN.div(new BN(blockTimeMsRelayChain));
const blocksPerTimesliceCoretimeChain = relationConstant.gtn(0)
? blocksPerTimesliceRelayChainBN.div(relationConstant)
: BN_ZERO;
setCoretimeConstants({
coretime: {
blocksPerTimeslice: blocksPerTimesliceCoretimeChain.toNumber(),
blocktimeMs: blockTimeMsCoretimeChainBN.toNumber()
},
relay: {
blocksPerTimeslice: blocksPerTimesliceRelayChainBN.toNumber(),
blocktimeMs: blockTimeMsRelayChain
}
});
}, [isApiReady, blockTimeMsRelayChain, blockTimeMsCoretimeChain, blocksPerTimesliceRelayChain]);
return coretimeConstants;
}
export const useCoretimeConsts = createNamedHook('useCoretimeConsts', useCoretimeConstsImpl);
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import { useMemo } from 'react';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { isString } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
const endpoints = createWsEndpoints((k, v) => v?.toString() || k);
export function getCoretimeEndpoint (curApiInfo?: string): LinkOption | null {
return endpoints.find(({ info }) => isString(info) && isString(curApiInfo) && info.toLowerCase().includes('coretime') && info.toLowerCase().includes(curApiInfo.toLowerCase())) || null;
}
function useCoretimeEndpointImpl (relayInfo?: string): LinkOption | null {
return useMemo(() => getCoretimeEndpoint(relayInfo), [relayInfo]);
}
export const useCoretimeEndpoint = createNamedHook('useCoretimeEndpoint', useCoretimeEndpointImpl);
@@ -0,0 +1,207 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { ChainInformation, CoretimeInformation, CoreWorkload, CoreWorkloadInfo, LegacyLease, PotentialRenewal, Reservation } from './types.js';
import { useEffect, useMemo, useState } from 'react';
import { createNamedHook, useApi, useBrokerConfig, useBrokerLeases, useBrokerReservations, useBrokerSalesInfo, useBrokerStatus, useCoreDescriptor, useRegions, useWorkloadInfos, useWorkplanInfos } from '@pezkuwi/react-hooks';
import { BN, BN_ZERO } from '@pezkuwi/util';
import { ChainRenewalStatus, CoreTimeTypes } from './constants.js';
import { useBrokerPotentialRenewals } from './useBrokerPotentialRenewals.js';
import { useCoretimeConsts } from './useCoretimeConsts.js';
const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined, isPool: boolean): CoreTimeTypes => {
if (isPool) {
return CoreTimeTypes['On Demand'];
}
return reservation ? CoreTimeTypes.Reservation : lease ? CoreTimeTypes.Lease : CoreTimeTypes['Bulk Coretime'];
};
function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeInformation | undefined {
const { apiCoretime, isApiReady } = useApi();
const [workloadData, setWorkloadData] = useState<CoreWorkload[]>([]);
const [taskIds, setTaskIds] = useState<number[]>([]);
const [blocksPerTimesliceCoretimeChain, setBlocksPerTimesliceCoretimeChain] = useState<BN | undefined>();
/** coretime API calls */
const status = useBrokerStatus(apiCoretime, isApiReady);
const leases = useBrokerLeases(apiCoretime, isApiReady);
const reservations = useBrokerReservations(apiCoretime, isApiReady);
const salesInfo = useBrokerSalesInfo(apiCoretime, isApiReady);
const workloads = useWorkloadInfos(apiCoretime, isApiReady);
const workplans = useWorkplanInfos(apiCoretime, isApiReady);
const config = useBrokerConfig(apiCoretime, isApiReady);
const potentialRenewals = useBrokerPotentialRenewals(apiCoretime, isApiReady);
const region = useRegions(apiCoretime);
const coretimeConstants = useCoretimeConsts();
/** Coretime constants */
useEffect(() => {
if (!coretimeConstants?.coretime.blocksPerTimeslice) {
return;
}
setBlocksPerTimesliceCoretimeChain(new BN(coretimeConstants.coretime.blocksPerTimeslice));
}, [coretimeConstants]);
/** *******************/
const coreInfos = useCoreDescriptor(api, ready);
const paraIds = useMemo(() => coreInfos && [...new Set(coreInfos?.map((a) => a.info.currentWork.assignments.map((ass) => ass.task)).flat().filter((id) => id !== 'Pool'))], [coreInfos]);
const isInterludePhase = useMemo(() => {
if (!salesInfo || !config || !status || !blocksPerTimesliceCoretimeChain) {
return false;
}
const currentRegionStart = new BN(salesInfo?.regionBegin).sub(new BN(config.regionLength));
const interludeLengthTs = blocksPerTimesliceCoretimeChain.gt(BN_ZERO) ? new BN(config?.interludeLength).div(blocksPerTimesliceCoretimeChain) : BN_ZERO;
const interludeEndTs = currentRegionStart.add(interludeLengthTs);
return interludeEndTs.gte(new BN(status?.lastCommittedTimeslice));
}, [status, salesInfo, config, blocksPerTimesliceCoretimeChain]);
const potentialRenewalsCurrentRegion = useMemo(() => {
if (!isInterludePhase || !config || !salesInfo) {
return [];
}
// when - The point in time that the renewable workload on core ends and a fresh renewal may begin.
return potentialRenewals?.filter((renewal: PotentialRenewal) => renewal.when.toString() === salesInfo?.regionBegin.toString());
}, [potentialRenewals, salesInfo, config, isInterludePhase]);
const [state, setState] = useState<CoretimeInformation | undefined>();
useEffect(() => {
if (paraIds?.length && !taskIds.length) {
const simpleIds = paraIds.map((p) => Number(p));
const renewalIds = potentialRenewals?.map((r) => Number(r.task));
if (renewalIds) {
const numbers = [...new Set(simpleIds.concat(renewalIds))];
if (numbers?.length > simpleIds.length) {
setTaskIds(numbers.sort((a, b) => a - b));
return;
}
}
setTaskIds(simpleIds);
}
}, [potentialRenewals, paraIds, taskIds]);
useEffect(() => {
if (workloads?.length) {
setWorkloadData(workloads);
}
if (!!coreInfos?.length && !workloads?.length) {
const parsedCoreInfos = coreInfos?.map((coreInfo) => ({
core: -1,
info: coreInfo.info.currentWork.assignments.map((assignment) => (
{
isPool: assignment.isPool,
isTask: assignment.isTask,
mask: [],
maskBits: 0,
task: assignment.task
}
) as CoreWorkloadInfo)
}));
setWorkloadData(parsedCoreInfos as unknown as CoreWorkload[]);
}
}, [workloads, coreInfos]);
useEffect((): void => {
if (!workloadData?.length || !reservations?.length || !coretimeConstants) {
return;
}
const chainInfo: Record<string, ChainInformation> = {};
taskIds?.forEach((id) => {
const taskId = id.toString();
const lease = leases?.length ? leases?.find((lease) => lease.task === taskId) : undefined;
const reservation = reservations?.find((reservation) => reservation.task === taskId);
const workloads = workloadData?.filter((one) => one.info.task === taskId);
const workTaskInfo = workloads.map((workload) => {
// teyrchain can be renewed on a different core
const workplan = workplans?.filter((workplan) => workplan.info.task.toString() === taskId);
const type = getOccupancyType(lease, reservation, workload?.info.isPool ?? false);
const potentialRenewal = potentialRenewalsCurrentRegion?.find((renewal) => renewal.task.toString() === taskId);
const chainRenewedCore = type === CoreTimeTypes['Bulk Coretime'] && !!workplan?.length;
let renewalStatus = ChainRenewalStatus.None;
let renewalStatusMessage = '';
if (potentialRenewal) {
renewalStatus = ChainRenewalStatus.Eligible;
}
if (chainRenewedCore) {
renewalStatus = ChainRenewalStatus.Renewed;
renewalStatusMessage = `Next cycle on core ${workplan[0].core}`;
}
const chainRegionEnd = (renewalStatus === ChainRenewalStatus.Renewed ? salesInfo?.regionEnd : salesInfo?.regionBegin);
const targetTimeslice = lease?.until || chainRegionEnd;
const lastBlock = targetTimeslice ? targetTimeslice * coretimeConstants?.relay.blocksPerTimeslice : 0;
return {
lastBlock,
renewal: potentialRenewal,
renewalStatus,
renewalStatusMessage,
type,
workload,
workplan
};
});
chainInfo[id.toString()] = {
id,
lease,
reservation,
workTaskInfo
};
});
if (chainInfo && config && region && salesInfo && status && coretimeConstants) {
setState({
chainInfo,
config,
constants: coretimeConstants,
region,
salesInfo,
status,
taskIds
});
}
}, [
config,
coretimeConstants,
taskIds,
workloadData,
potentialRenewalsCurrentRegion,
salesInfo,
leases,
reservations,
region,
status,
workplans
]);
return state;
}
export const useCoretimeInformation = createNamedHook('useCoretimeInformation', useCoretimeInformationImpl);
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useState } from 'react';
import { useIsMountedRef } from './useIsMountedRef.js';
const DEFAULT_DELAY = 250;
// FIXE Due to generics, cannot use createNamedHook
export function useDebounce <T> (value: T, delay = DEFAULT_DELAY): T {
const mountedRef = useIsMountedRef();
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect((): () => void => {
const timeoutId = setTimeout(() => {
mountedRef.current && setDebouncedValue(value);
}, delay);
// each time something changes, we clears
return (): void => {
clearTimeout(timeoutId);
};
}, [delay, value, mountedRef]);
return debouncedValue;
}
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PalletDemocracyVoteVoting } from '@pezkuwi/types/lookup';
import { useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
import { createNamedHook } from './createNamedHook.js';
function useDelegationsImpl (): PalletDemocracyVoteVoting[] | undefined {
const { api } = useApi();
const { allAccounts } = useAccounts();
return useCall<PalletDemocracyVoteVoting[]>(api.query.democracy?.votingOf?.multi, [allAccounts]);
}
export const useDelegations = createNamedHook('useDelegations', useDelegationsImpl);
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveAccountFlags } from '@pezkuwi/api-derive/types';
import type { AccountId, Address } from '@pezkuwi/types/interfaces';
import { createNamedHook } from './createNamedHook.js';
import { useCall } from './useCall.js';
import { useSystemApi } from './useSystemApi.js';
function useDeriveAccountFlagsImpl (value?: AccountId | Address | Uint8Array | string | null): DeriveAccountFlags | undefined {
const api = useSystemApi();
return useCall<DeriveAccountFlags>(api?.derive.accounts.flags, [value]);
}
export const useDeriveAccountFlags = createNamedHook('useDeriveAccountFlags', useDeriveAccountFlagsImpl);
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveAccountInfo } from '@pezkuwi/api-derive/types';
import type { AccountId, AccountIndex, Address } from '@pezkuwi/types/interfaces';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
function useDeriveAccountInfoImpl (value?: AccountId | AccountIndex | Address | Uint8Array | string | null): DeriveAccountInfo | undefined {
const { apiIdentity } = useApi();
return useCall<DeriveAccountInfo>(apiIdentity?.derive.accounts.info, [value]);
}
export const useDeriveAccountInfo = createNamedHook('useDeriveAccountInfo', useDeriveAccountInfoImpl);
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import type { ElementPosition } from '@pezkuwi/react-components/Popup/types';
import { useEffect, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useScroll } from './useScroll.js';
import { useWindowSize } from './useWindowSize.js';
function useElementPositionImpl (ref: React.MutableRefObject<HTMLElement | undefined | null>): ElementPosition | undefined {
const [elementPosition, setElementPosition] = useState<ElementPosition>();
const mountedRef = useIsMountedRef();
const windowSize = useWindowSize();
const scrollY = useScroll();
useEffect(() => {
if (mountedRef.current && ref?.current) {
const { height, width, x, y } = ref.current.getBoundingClientRect();
setElementPosition({
height,
width,
x,
y
});
}
}, [mountedRef, ref, scrollY, windowSize]);
return elementPosition;
}
export const useElementPosition = createNamedHook('useElementPosition', useElementPositionImpl);
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import { useMemo } from 'react';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { createNamedHook } from './createNamedHook.js';
const endpoints = createWsEndpoints((k, v) => v?.toString() || k);
export function getEndpoint (apiUrl?: string): LinkOption | null {
return endpoints.find(({ value }) => value === apiUrl) || null;
}
function useEndpointImpl (apiUrl?: string): LinkOption | null {
return useMemo(() => getEndpoint(apiUrl), [apiUrl]);
}
export const useEndpoint = createNamedHook('useEndpoint', useEndpointImpl);
@@ -0,0 +1,72 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { EventRecord } from '@pezkuwi/types/interfaces';
import type { Codec } from '@pezkuwi/types/types';
import type { BN } from '@pezkuwi/util';
import type { EventCheck } from './useEventTrigger.js';
import { useEffect, useState } from 'react';
import { isFunction } from '@pezkuwi/util';
import { useApi } from './useApi.js';
import { useEventTrigger } from './useEventTrigger.js';
import { useMemoValue } from './useMemoValue.js';
export interface Changes<T extends Codec> {
added?: T[];
removed?: T[];
}
function interleave <T extends Codec> (existing: T[] = [], { added = [], removed = [] }: Changes<T>): T[] {
if (!added.length && !removed.length) {
return existing;
}
const map: Record<string, T> = {};
[existing, added].forEach((m) =>
m.forEach((v): void => {
map[v.toHex()] = v;
})
);
removed.forEach((v): void => {
delete map[v.toHex()];
});
const adjusted = Object
.entries(map)
.sort((a, b) =>
// for BN-like objects, we use the built-in compare for sorting
isFunction((a[1] as unknown as BN).cmp)
? (a[1] as unknown as BN).cmp(b[1] as unknown as BN)
: a[0].localeCompare(b[0])
)
.map(([, v]) => v);
return adjusted.length !== existing.length || adjusted.find((e, i) => !e.eq(existing[i]))
? adjusted
: existing;
}
export function useEventChanges <T extends Codec, A> (checks: EventCheck[], filter: (records: EventRecord[], api: ApiPromise, additional?: A) => Changes<T>, startValue?: T[], additional?: A): T[] | undefined {
const { api } = useApi();
const [state, setState] = useState<T[] | undefined>();
const memoChecks = useMemoValue(checks);
const { blockHash, events } = useEventTrigger(memoChecks);
// when startValue changes, we do a full refresh
useEffect((): void => {
startValue && setState((prev) => interleave(prev, { added: startValue }));
}, [startValue]);
// add/remove any additional items detected (only when actual events occur)
useEffect((): void => {
blockHash && setState((prev) => interleave(prev, filter(events, api, additional)));
}, [additional, api, blockHash, events, filter]);
return state;
}
@@ -0,0 +1,57 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AugmentedEvent } from '@pezkuwi/api/types';
import type { Vec } from '@pezkuwi/types';
import type { EventRecord } from '@pezkuwi/types/interfaces';
import { useEffect, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useMemoValue } from './useMemoValue.js';
export type EventCheck = AugmentedEvent<'promise'> | false | undefined | null;
interface Result {
blockHash: string;
events: EventRecord[];
}
const EMPTY_RESULT: Result = {
blockHash: '',
events: []
};
const IDENTITY_FILTER = () => true;
function useEventTriggerImpl (checks: EventCheck[], filter: (record: EventRecord) => boolean = IDENTITY_FILTER): Result {
const { api } = useApi();
const [state, setState] = useState(() => EMPTY_RESULT);
const memoChecks = useMemoValue(checks);
const mountedRef = useIsMountedRef();
const eventRecords = useCall<Vec<EventRecord>>(api.query.system.events);
useEffect((): void => {
if (mountedRef.current && eventRecords) {
const events = eventRecords.filter((r) =>
r.event &&
memoChecks.some((c) => c && c.is(r.event)) &&
filter(r)
);
if (events.length) {
setState({
blockHash: eventRecords.createdAtHash?.toHex() || '',
events
});
}
}
}, [eventRecords, filter, memoChecks, mountedRef]);
return state;
}
export const useEventTrigger = createNamedHook('useEventTrigger', useEventTriggerImpl);
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/types';
import type { SignedBlockExtended } from '@pezkuwi/api-derive/types';
import { useEffect, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useMemoValue } from './useMemoValue.js';
type ExtrinsicCheck = SubmittableExtrinsicFunction<'promise'> | false | undefined | null;
function useExtrinsicTriggerImpl (checks: ExtrinsicCheck[]): string {
const { api } = useApi();
const [trigger, setTrigger] = useState('0');
const mountedRef = useIsMountedRef();
const memoChecks = useMemoValue(checks);
const block = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks);
useEffect((): void => {
mountedRef.current && block?.extrinsics?.filter(({ extrinsic }) =>
extrinsic &&
memoChecks.some((c) => c && c.is(extrinsic))
).length && setTrigger(() => block.createdAtHash?.toHex() || '');
}, [block, memoChecks, mountedRef]);
return trigger;
}
export const useExtrinsicTrigger = createNamedHook('useExtrinsicTrigger', useExtrinsicTriggerImpl);
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useMemo, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useCacheKey } from './useCacheKey.js';
// hook for favorites with local storage
function useFavoritesImpl (storageKeyBase: string): [string[], (address: string) => void] {
const [getCache, setCache] = useCacheKey<string[]>(storageKeyBase);
const [favorites, setFavorites] = useState<string[]>(() => getCache() || []);
const toggleFavorite = useCallback(
(address: string): void => setFavorites(
(favorites) => setCache(
favorites.includes(address)
? favorites.filter((a) => address !== a)
: [...favorites, address]
)
),
[setCache]
);
return useMemo(
() => [favorites, toggleFavorite],
[favorites, toggleFavorite]
);
}
export const useFavorites = createNamedHook('useFavorites', useFavoritesImpl);
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useMemo, useState } from 'react';
import { isUndefined } from '@pezkuwi/util';
export type FormField<T> = [
T | null,
boolean,
(_?: T | null) => void
];
type ValidateFn<T> = (_: T) => boolean;
const defaultValidate = (): boolean => true;
// FIXME Since we use generics, this cannot be a createNamedHook as of yet
export function useFormField<T> (defaultValue: T | null, validate: ValidateFn<T> = defaultValidate): FormField<T> {
const [value, setValue] = useState<T | null>(defaultValue);
const isValid = useMemo(
() => !!value && validate(value),
[validate, value]
);
const setter = useCallback(
(value?: T | null) => !isUndefined(value) && setValue(value),
[]
);
return [value, isValid, setter];
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useIsMountedRef } from './useIsMountedRef.js';
function useIncrementImpl (defaultValue = 1): [number, () => void, (value: number) => void] {
const mountedRef = useIsMountedRef();
const [value, setValue] = useState(defaultValue);
const increment = useCallback(
(): void => {
mountedRef.current && setValue((value: number) => ++value);
},
[mountedRef]
);
return [value, increment, setValue];
}
export const useIncrement = createNamedHook('useIncrement', useIncrementImpl);
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { BN } from '@pezkuwi/util';
import type { Inflation } from './types.js';
import { useEffect, useState } from 'react';
import { getInflationParams } from '@pezkuwi/apps-config';
import { BN_MILLION, BN_ZERO } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
const EMPTY: Inflation = { idealInterest: 0, idealStake: 0, inflation: 0, stakedFraction: 0, stakedReturn: 0 };
function calcInflation (api: ApiPromise, totalStaked: BN, totalIssuance: BN, numAuctions: BN): Inflation {
const { auctionAdjust, auctionMax, falloff, maxInflation, minInflation, stakeTarget } = getInflationParams(api);
const stakedFraction = totalStaked.isZero() || totalIssuance.isZero()
? 0
: totalStaked.mul(BN_MILLION).div(totalIssuance).toNumber() / BN_MILLION.toNumber();
// Ideal is less based on the actual auctions, see
// https://github.com/pezkuwichain/pezkuwi/blob/816cb64ea16102c6c79f6be2a917d832d98df757/runtime/dicle/src/lib.rs#L531
const idealStake = stakeTarget - (Math.min(auctionMax, numAuctions.toNumber()) * auctionAdjust);
const idealInterest = idealStake === 0
? 0
: maxInflation / idealStake;
// inflation calculations, see
// https://github.com/pezkuwichain/bizinikiwi/blob/0ba251c9388452c879bfcca425ada66f1f9bc802/frame/staking/reward-fn/src/lib.rs#L28-L54
const inflation = 100 * (minInflation + (
stakedFraction <= idealStake
? (stakedFraction * (idealInterest - (minInflation / idealStake)))
: ((maxInflation - minInflation) * Math.pow(2, (idealStake - stakedFraction) / falloff))
));
return {
idealInterest,
idealStake,
inflation,
stakedFraction,
stakedReturn: stakedFraction
? (inflation / stakedFraction)
: 0
};
}
function useInflationImpl (totalStaked?: BN): Inflation {
const { api } = useApi();
const totalIssuance = useCall<BN>(api.query.balances?.totalIssuance);
const auctionCounter = useCall<BN>(api.query.auctions?.auctionCounter);
const [state, setState] = useState<Inflation>(EMPTY);
useEffect((): void => {
const numAuctions = api.query.auctions
? auctionCounter
: BN_ZERO;
numAuctions && totalIssuance && totalStaked && setState(
calcInflation(api, totalStaked, totalIssuance, numAuctions)
);
}, [api, auctionCounter, totalIssuance, totalStaked]);
return state;
}
export const useInflation = createNamedHook('useInflation', useInflationImpl);
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
const KNOWN = ['ipfs', 'ipns'];
const SECTIONS = KNOWN.map((part) => `/${part}/`);
const LOCAL_IPFS = '.ipfs.localhost';
const LOCAL_IPNS = '.ipns.localhost';
interface State {
ipnsChain: string | null;
ipnsDomain: string | null;
ipfsHash: string | null;
ipfsPath: string | null;
isIpfs: boolean;
isIpns: boolean;
}
function extractLocalIpfs (url: string): State {
const [,, _ipfsPath] = url.split('/');
const ipfsPath = _ipfsPath.split(':')[0];
return {
ipfsHash: ipfsPath.replace(LOCAL_IPFS, ''),
ipfsPath,
ipnsChain: null,
ipnsDomain: null,
isIpfs: true,
isIpns: false
};
}
function extractLocalIpns (url: string): State {
const [,, _ipfsPath] = url.split('/');
const ipfsPath = _ipfsPath.split(':')[0];
const dnsLink = ipfsPath.replace(LOCAL_IPNS, '');
const linkParts = dnsLink.split('.');
let ipnsChain: string | null = null;
let ipnsDomain: string | null = null;
if (linkParts.length > 2) {
ipnsChain = linkParts[0];
ipnsDomain = linkParts.slice(1).join('.');
} else {
ipnsDomain = dnsLink;
}
return {
ipfsHash: null,
ipfsPath,
ipnsChain,
ipnsDomain,
isIpfs: true,
isIpns: true
};
}
function extractOther (url: string): State {
const isIpfs = SECTIONS.some((part) => url.includes(part));
const isIpns = url.includes(SECTIONS[1]);
// individual sections, with the index of the ipfs portion
const urlParts = url.split('/');
const index = urlParts.indexOf(isIpns ? KNOWN[1] : KNOWN[0]);
// the parts of the path for ipfs re-construction
let ipfsHash: string | null = null;
let ipfsPath: string | null = null;
let ipnsChain: string | null = null;
let ipnsDomain: string | null = null;
// setup the ipfs part and dnslink domain (if available)
if (index !== -1) {
ipfsPath = urlParts.slice(0, index + 1).join('/');
if (isIpns) {
const dnsLink = urlParts[index + 1];
const linkParts = dnsLink.split('.');
if (linkParts.length > 2) {
ipnsChain = linkParts[0];
ipnsDomain = linkParts.slice(1).join('.');
} else {
ipnsDomain = dnsLink;
}
} else {
ipfsHash = urlParts[index + 1];
}
}
return {
ipfsHash,
ipfsPath,
ipnsChain,
ipnsDomain,
isIpfs,
isIpns
};
}
export function extractIpfsDetails (): State {
// get url and check to see if we are ipfs/ipns
const [url] = window.location.href.split('#');
return url.includes(LOCAL_IPFS)
? extractLocalIpfs(url)
: url.includes(LOCAL_IPNS)
? extractLocalIpns(url)
: extractOther(url);
}
function useIpfsImpl (): State {
const [state] = useState(() => extractIpfsDetails());
return state;
}
export const useIpfs = createNamedHook('useIpfs', useIpfsImpl);
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useMemo } from 'react';
import { createNamedHook } from './createNamedHook.js';
interface Result {
ipfsHash: string;
ipfsShort: string;
ipfsUrl: string;
}
function useIpfsLinkImpl (ipfsHash?: string | null): Result | null {
return useMemo(
() => ipfsHash
? {
ipfsHash,
ipfsShort: `${ipfsHash.substring(0, 4)}${ipfsHash.slice(-4)}`,
// ipfsUrl: `https://cloudflare-ipfs.com/ipfs/${ipfs}`
ipfsUrl: `https://ipfs.io/ipfs/${ipfsHash}`
}
: null,
[ipfsHash]
);
}
export const useIpfsLink = createNamedHook('useIpfsLink', useIpfsLinkImpl);
@@ -0,0 +1,26 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import { useEffect, useRef } from 'react';
import { createNamedHook } from './createNamedHook.js';
export type MountedRef = React.MutableRefObject<boolean>;
function useIsMountedRefImpl (): MountedRef {
const isMounted = useRef(false);
useEffect((): () => void => {
isMounted.current = true;
return (): void => {
isMounted.current = false;
};
}, []);
return isMounted;
}
export const useIsMountedRef = createNamedHook('useIsMountedRef', useIsMountedRefImpl);
@@ -0,0 +1,26 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { UseJudgements } from '@pezkuwi/react-components/types';
import { useMemo } from 'react';
import { getJudgements } from './utils/getJudgements.js';
import { matchRegistrarAccountsWithIndexes } from './utils/matchRegistrarAccountsWithIndexes.js';
import { createNamedHook } from './createNamedHook.js';
import { useAccountInfo } from './useAccountInfo.js';
import { useRegistrars } from './useRegistrars.js';
function useJudgementsImpl (address: string): UseJudgements {
const { identity } = useAccountInfo(address);
const { registrars: allRegistrars } = useRegistrars();
const judgementsWithRegistrarIndexes = useMemo(() => getJudgements(identity), [identity]);
return useMemo(
() => matchRegistrarAccountsWithIndexes(judgementsWithRegistrarIndexes, allRegistrars),
[allRegistrars, judgementsWithRegistrarIndexes]
);
}
export const useJudgements = createNamedHook('useJudgements', useJudgementsImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Accounts, Addresses } from './ctx/types.js';
import { useContext } from 'react';
import { KeyringCtx } from './ctx/Keyring.js';
import { createNamedHook } from './createNamedHook.js';
function useKeyringImpl (): { accounts: Accounts, addresses: Addresses } {
return useContext(KeyringCtx);
}
export const useKeyring = createNamedHook('useKeyring', useKeyringImpl);
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// This is for the use of `Ledger`
//
/* eslint-disable deprecation/deprecation */
import type { ApiPromise } from '@pezkuwi/api';
import type { TransportType } from '@pezkuwi/hw-ledger-transports/types';
import { useCallback, useMemo } from 'react';
import { Ledger, LedgerGeneric } from '@pezkuwi/hw-ledger';
import { knownGenesis, knownLedger } from '@pezkuwi/networks/defaults';
import { settings } from '@pezkuwi/ui-settings';
import { assert } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
interface StateBase {
hasLedgerChain: boolean;
hasWebUsb: boolean;
isLedgerCapable: boolean;
isLedgerEnabled: boolean;
}
interface State extends StateBase {
getLedger: () => LedgerGeneric | Ledger;
}
const EMPTY_STATE: StateBase = {
hasLedgerChain: false,
hasWebUsb: false,
isLedgerCapable: false,
isLedgerEnabled: false
};
const hasWebUsb = !!(window as unknown as { USB?: unknown }).USB;
const ledgerChains = Object
.keys(knownGenesis)
.filter((n) => knownLedger[n]);
const ledgerHashes = ledgerChains.reduce<string[]>((all, n) => [...all, ...knownGenesis[n]], []);
let ledger: LedgerGeneric | Ledger | null = null;
let ledgerType: TransportType | null = null;
let ledgerApp: string | null;
function retrieveLedger (api: ApiPromise): LedgerGeneric | Ledger {
const currType = settings.get().ledgerConn as TransportType;
const currApp = settings.get().ledgerApp;
if (!ledger || ledgerType !== currType || currApp !== ledgerApp) {
const genesisHex = api.genesisHash.toHex();
const network = ledgerChains.find((network) => knownGenesis[network].includes(genesisHex));
assert(network, `Unable to find a known Ledger config for genesisHash ${genesisHex}`);
if (currApp === 'generic') {
// All chains use the `slip44` from pezkuwi in their derivation path in ledger.
// This interface is specific to the underlying PezkuwiGenericApp.
ledger = new LedgerGeneric(currType, network, knownLedger.pezkuwi);
} else if (currApp === 'migration') {
ledger = new LedgerGeneric(currType, network, knownLedger[network]);
} else if (currApp === 'chainSpecific') {
ledger = new Ledger(currType, network);
} else {
// This will never get touched since it will always hit the above two. This satisfies the compiler.
ledger = new LedgerGeneric(currType, network, knownLedger.pezkuwi);
}
ledgerType = currType;
ledgerApp = currApp;
}
return ledger;
}
function getState (api: ApiPromise): StateBase {
const hasLedgerChain = ledgerHashes.includes(api.genesisHash.toHex());
const isLedgerCapable = hasWebUsb && hasLedgerChain;
return {
hasLedgerChain,
hasWebUsb,
isLedgerCapable,
isLedgerEnabled: isLedgerCapable && settings.ledgerConn !== 'none'
};
}
function useLedgerImpl (): State {
const { api, isApiReady } = useApi();
const getLedger = useCallback(
() => retrieveLedger(api),
[api]
);
return useMemo(
() => ({ ...(isApiReady ? getState(api) : EMPTY_STATE), getLedger }),
[api, getLedger, isApiReady]
);
}
export const useLedger = createNamedHook('useLedger', useLedgerImpl);
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { QueryableStorageEntry } from '@pezkuwi/api/types';
import { useEffect, useRef, useState } from 'react';
import { stringify } from '@pezkuwi/util';
interface Options <T> {
transform?: (value: any[]) => T;
}
// FIXME This is generic, we cannot really use createNamedHook
export function useMapEntries <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params?: unknown[] | null, { transform }: Options<T> = {}, at?: string | null | false): T | undefined {
const [state, setState] = useState<T | undefined>();
const checkRef = useRef<string | null>(null);
useEffect((): void => {
if (entry && params) {
const check = stringify({ at, params });
if (check !== checkRef.current) {
checkRef.current = check;
(
at && at !== '0'
// eslint-disable-next-line deprecation/deprecation
? entry.entriesAt(at, ...params)
: entry.entries(...params)
).then((entries) => setState(
transform
? transform(entries)
: entries as unknown as T
)).catch(console.error);
}
}
}, [at, entry, params, transform]);
return state;
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { QueryableStorageEntry } from '@pezkuwi/api/types';
import { useEffect, useRef, useState } from 'react';
import { stringify } from '@pezkuwi/util';
interface Options <T> {
transform?: (value: any[]) => T[];
}
// FIXME This is generic, we cannot really use createNamedHook
export function useMapKeys <T = any> (entry: QueryableStorageEntry<'promise'> | null | false | undefined, params?: unknown[] | null, { transform }: Options<T> = {}, at?: string | null | false): T[] | undefined {
const [state, setState] = useState<T[] | undefined>();
const checkRef = useRef<string | null>(null);
useEffect((): void => {
if (entry && params) {
const check = stringify({ at, params });
if (check !== checkRef.current) {
checkRef.current = check;
(
at && at !== '0'
// eslint-disable-next-line deprecation/deprecation
? entry.keysAt(at, ...params)
: entry.keys(...params)
).then((keys) => setState(
transform
? transform(keys)
: keys as unknown as T[]
)).catch(console.error);
}
}
}, [at, entry, params, transform]);
return state;
}
@@ -0,0 +1,82 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { stringify } from '@pezkuwi/util';
import { getMemoValue, isDifferent } from './useMemoValue.js';
describe('useMemoValue', (): void => {
describe('isDifferent', (): void => {
it('works on straight references', (): void => {
expect(isDifferent(1, 2)).toEqual(true);
expect(isDifferent(null, false)).toEqual(true);
expect(isDifferent({}, [])).toEqual(true);
expect(isDifferent(2, 2)).toEqual(false);
});
it('compares flat arrays', (): void => {
expect(isDifferent([1, 2, 3], [1, 2, 3])).toEqual(false);
expect(isDifferent([1, 2, 3], [4, 5, 6])).toEqual(true);
expect(isDifferent([1, 2, 3, 4], [1, 2, 3])).toEqual(true);
});
it('compares 1-level-nested arrays', (): void => {
expect(isDifferent(
[1, [2, 3]],
[1, [2, 3]]
)).toEqual(false);
expect(isDifferent(
[1, [2, 3]],
[1, [2]]
)).toEqual(true);
expect(isDifferent(
[1, [2, 3]],
[1, [3, 2]]
)).toEqual(true);
expect(isDifferent(
[1, [2, 3], 4],
[1, [2, 3], 4]
)).toEqual(false);
});
it('compares 2-level-nested arrays', (): void => {
const V34 = [3, 4];
// this one is same since the [3, 4] compares by ref
expect(isDifferent(
[1, [2, V34]],
[1, [2, V34]]
)).toEqual(false);
// this one is different since the [3, 4] is at level 2
expect(isDifferent(
[1, [2, V34]],
[1, [2, [3, 4]]]
)).toEqual(true);
// this one is different since the [3, 4] is at level 2
expect(isDifferent(
[1, [2, [3, 4]]],
[1, [2, [3, 4]]]
)).toEqual(true);
});
});
describe('getMemoValue', (): void => {
it('returns a new value when the previous is empty', (): void => {
expect(getMemoValue({ current: null }, 2)).toEqual(2);
});
it('returns a new value when the previous is a different type', (): void => {
expect(getMemoValue<unknown>({ current: { stringified: stringify({ value: 2 }), value: 2 } }, '2')).toEqual('2');
});
it('returns the previous value when different objects, but the same representation', (): void => {
const a = { some: { thing: 'test', zaz: [1, 2, 3] } };
const b = { some: { thing: 'test', zaz: [1, 2, 3] } };
expect(a === b).toEqual(false);
expect(getMemoValue({ current: { stringified: stringify({ value: a }), value: a } }, b) === a).toEqual(true);
});
});
});
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useMemo, useRef } from 'react';
import { stringify } from '@pezkuwi/util';
interface State<T> {
stringified: string;
value: T;
}
// since we use these in tests, we are not using React.MutableRef<State<T>>
interface Ref<T> {
current: State<T> | null;
}
/**
* @internal
*
* Does a check between two values to determine if they are equivalent. For
* non-array values it does a simple === compare, for arrays is checks the
* lengths and the individual items. (This is limited to a depth)
*/
export function isDifferent (a: unknown, b: unknown, depth = -1): boolean {
// increase the depth for arrays (we start at -1, so 0 is top-level)
depth++;
// check the actual value references for an exact match
return a !== b
// check if both are arrays with matching length
? depth < 2 && Array.isArray(a) && Array.isArray(b) && a.length === b.length
// check for any differences inside the arrays (with depth)
? a.some((ai, i) => isDifferent(ai, b[i], depth))
// not equal and not an array
: true
// exact value match found
: false;
}
/**
* @internal
*
* Checks the supplied value against the previous state, returning either the
* previous state (if we have a match) or a new object for future compares.
**/
export function getMemoValue <T> (ref: Ref<T>, value: T): T {
// check that either we have no previous or the value changed
if (!ref.current || isDifferent(ref.current.value, value)) {
const stringified = stringify({ value });
// no previous or the stringified result is different
if (!ref.current || ref.current.stringified !== stringified) {
ref.current = { stringified, value };
}
}
return ref.current.value;
}
// NOTE: Generic, cannot be used in named hook
export function useMemoValue <T> (value: T): T {
const ref = useRef<State<T> | null>(null);
return useMemo(
() => getMemoValue(ref, value),
[ref, value]
);
}
@@ -0,0 +1,106 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/x-textencoder/shim';
import '@pezkuwi/x-textdecoder/shim';
import type { CallOptions } from './types.js';
import { useEffect, useMemo, useState } from 'react';
import { useIsMountedRef } from './useIsMountedRef.js';
interface Options <T> extends CallOptions<T> {
transform?: (value: any) => T
}
export function normalizeMetadataLink (link?: string): string {
if (!link) {
return '';
} else if (link.toLowerCase().startsWith('http')) {
return link;
}
// handle V0 CID
const matchCidV0 = link.match(/Qm[A-Za-z0-9]{44}(?![A-Za-z0-9])/);
if (matchCidV0 !== null) {
return matchCidV0[0];
}
// handle V1 CID
const matchCidV1 = link.match(/[a-z0-9]{59}(?![A-Za-z0-9])/);
if (matchCidV1 !== null) {
return matchCidV1[0];
}
return '';
}
const cache = new Map<string, any>();
async function fetchMetadata <T> (metadataLinks: string[]): Promise<Map<string, T>> {
const result = new Map();
const promises = metadataLinks.map((metadataLink) => {
if (cache.has(metadataLink)) {
result.set(metadataLink, cache.get(metadataLink));
return Promise.resolve();
}
const fetchLink = metadataLink.startsWith('http') ? metadataLink : `https://ipfs.io/ipfs/${metadataLink}`;
return fetch(fetchLink)
.then(async (res) => {
const response = res.status >= 200 && res.status < 300 ? await res.text() : null;
cache.set(metadataLink, response);
result.set(metadataLink, response);
});
});
await Promise.allSettled(promises);
return result;
}
function postProcessData <T> (fetchedMetadata: Map<string, T>, { transform }: Options<T> = {}) {
if (!transform) {
return fetchedMetadata;
}
for (const [key, value] of fetchedMetadata.entries()) {
fetchedMetadata.set(key, transform(value));
}
return fetchedMetadata;
}
// FIXME This is generic, we cannot really use createNamedHook
export function useMetadataFetch <T> (rawLinks: string[] | undefined, options?: Options<T>): Map<string, T> | undefined {
const mountedRef = useIsMountedRef();
const [value, setValue] = useState<Map<string, T> | undefined>();
const metadataLinks = useMemo(() => {
if (!rawLinks) {
return undefined;
}
return rawLinks
.map((hash) => normalizeMetadataLink(hash))
.filter((hash) => !!hash);
}, [rawLinks]);
useEffect((): void => {
if (mountedRef.current && metadataLinks) {
fetchMetadata<T>(metadataLinks)
.then((fetchedMetadata) => setValue(postProcessData<T>(fetchedMetadata, options)))
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => { });
}
}, [metadataLinks, options, mountedRef]);
return value;
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ModalState } from './types.js';
import { useCallback } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useToggle } from './useToggle.js';
function useModalImpl (defaultIsOpen?: boolean, onOpen?: () => void, onClose?: () => void): ModalState {
const [isOpen, , setIsOpen] = useToggle(defaultIsOpen || false);
const _onOpen = useCallback(
(): void => {
setIsOpen(true);
onOpen && onOpen();
},
[onOpen, setIsOpen]
);
const _onClose = useCallback(
(): void => {
setIsOpen(false);
onClose && onClose();
},
[onClose, setIsOpen]
);
return { isOpen, onClose: _onClose, onOpen: _onOpen };
}
export const useModal = createNamedHook('useModal', useModalImpl);
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useState } from 'react';
import { nextTick } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
function useNextTickImpl (): boolean {
const [isNextTick, setIsNextTick] = useState(false);
useEffect((): void => {
nextTick(() => setIsNextTick(true));
}, []);
return isNextTick;
}
export const useNextTick = createNamedHook('useNextTick', useNextTickImpl);
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { FormField } from './useFormField.js';
import { createNamedHook } from './createNamedHook.js';
import { useFormField } from './useFormField.js';
function isValid (value?: string | null): boolean {
return (value && value.length > 0) || false;
}
function useNonEmptyStringImpl (initialValue = ''): FormField<string> {
return useFormField(initialValue, isValid);
}
export const useNonEmptyString = createNamedHook('useNonEmptyString', useNonEmptyStringImpl);
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { FormField } from './useFormField.js';
import { useMemo } from 'react';
import { BN_ZERO, bnToBn } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useFormField } from './useFormField.js';
function isValid (value: BN): boolean {
return !value.isZero();
}
function useNonZeroBnImpl (initialValue: BN | number = BN_ZERO): FormField<BN> {
const value = useMemo(() => bnToBn(initialValue), [initialValue]);
return useFormField(value, isValid);
}
export const useNonZeroBn = createNamedHook('useNonZeroBn', useNonZeroBnImpl);
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import { useCallback, useEffect } from 'react';
import { createNamedHook } from './createNamedHook.js';
function isRefClicked (refs: React.RefObject<HTMLDivElement>[], e: MouseEvent): boolean {
return refs.some((r) =>
r.current &&
r.current.contains(e.target as HTMLElement)
);
}
function useOutsideClickImpl (refs: React.RefObject<HTMLDivElement>[], callback: () => void): void {
const handleClick = useCallback(
(e: MouseEvent): void => {
if (refs.length && !isRefClicked(refs, e)) {
callback();
}
},
[refs, callback]
);
useEffect((): () => void => {
document.addEventListener('click', handleClick, true);
return (): void => {
document.removeEventListener('click', handleClick, true);
};
}, [handleClick, callback]);
}
export const useOutsideClick = createNamedHook('useOutsideClick', useOutsideClickImpl);
@@ -0,0 +1,211 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Dispatch, SetStateAction } from 'react';
import type { ApiPromise } from '@pezkuwi/api';
import type { DeriveEraPoints, DeriveEraRewards, DeriveStakerReward } from '@pezkuwi/api-derive/types';
import type { u32, Vec } from '@pezkuwi/types';
import type { EraIndex } from '@pezkuwi/types/interfaces';
import type { PalletStakingStakingLedger } from '@pezkuwi/types/lookup';
import type { StakerState } from './types.js';
import { useCallback, useEffect, useState } from 'react';
import { BN_ZERO } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useEventTrigger } from './useEventTrigger.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useOwnStashIds } from './useOwnStashes.js';
interface State {
allRewards?: Record<string, DeriveStakerReward[]> | null;
isLoadingRewards: boolean;
rewardCount: number;
}
interface ValidatorWithEras {
eras: EraIndex[];
stashId: string;
}
interface Filtered {
filteredEras: EraIndex[];
validatorEras: ValidatorWithEras[];
}
const EMPTY_FILTERED: Filtered = {
filteredEras: [],
validatorEras: []
};
const EMPTY_STATE: State = {
isLoadingRewards: true,
rewardCount: 0
};
function getLegacyRewards (ledger: PalletStakingStakingLedger, claimedRewardsEras?: Vec<u32>): u32[] {
const legacyRewards = ledger.legacyClaimedRewards || (ledger as unknown as { claimedRewards: u32[] }).claimedRewards || [];
return legacyRewards.concat(claimedRewardsEras?.toArray() || []);
}
function getRewards ([[stashIds], available]: [[string[], EraIndex[]], DeriveStakerReward[][]]): State {
const allRewards: Record<string, DeriveStakerReward[]> = {};
stashIds.forEach((stashId, index): void => {
allRewards[stashId] = available[index].filter(({ eraReward }) => !eraReward.isZero());
});
return {
allRewards,
isLoadingRewards: false,
rewardCount: Object.values(allRewards).filter((rewards) => rewards.length !== 0).length
};
}
function getValRewards (api: ApiPromise, validatorEras: ValidatorWithEras[], erasPoints: DeriveEraPoints[], erasRewards: DeriveEraRewards[]): State {
const allRewards: Record<string, DeriveStakerReward[]> = {};
validatorEras.forEach(({ eras, stashId }): void => {
eras.forEach((era): void => {
const eraPoints = erasPoints.find((p) => p.era.eq(era));
const eraRewards = erasRewards.find((r) => r.era.eq(era));
if (eraPoints?.eraPoints.gt(BN_ZERO) && eraPoints?.validators[stashId] && eraRewards) {
const reward = eraPoints.validators[stashId].mul(eraRewards.eraReward).div(eraPoints.eraPoints);
if (!reward.isZero()) {
const total = api.createType('Balance', reward);
if (!allRewards[stashId]) {
allRewards[stashId] = [];
}
allRewards[stashId].push({
era,
eraReward: eraRewards.eraReward,
isClaimed: false,
isEmpty: false,
isValidator: true,
nominating: [],
validators: {
[stashId]: {
total,
value: total
}
}
});
}
}
});
});
return {
allRewards,
isLoadingRewards: false,
rewardCount: Object.values(allRewards).filter((rewards) => rewards.length !== 0).length
};
}
function useStakerRewards (filteredEras: EraIndex[], setState: Dispatch<SetStateAction<State>>, ownValidators?: StakerState[], additional?: string[]) {
const { api } = useApi();
const stashIds = useOwnStashIds(additional);
const trigger = useEventTrigger([api.events.staking?.PayoutStarted, api.events.staking?.Rewarded]);
const [stakerRewards, setStakerRewards] = useState<[[string[], EraIndex[]], DeriveStakerReward[][]]>();
const onFetchStakeRewards = useCallback(async () => {
if (!ownValidators?.length && !!filteredEras.length && stashIds) {
setState((e) => ({ ...e, isLoadingRewards: true }));
const stakerRewards = await api.derive.staking?.stakerRewardsMultiEras(stashIds, filteredEras);
setStakerRewards([[stashIds, filteredEras], stakerRewards]);
}
}, [api.derive.staking, filteredEras, ownValidators?.length, setState, stashIds]);
useEffect(() => {
onFetchStakeRewards().catch(console.error);
}, [onFetchStakeRewards]);
useEffect(() => {
if (trigger.blockHash) {
onFetchStakeRewards().catch(console.error);
}
}, [onFetchStakeRewards, trigger.blockHash]);
return stakerRewards;
}
function useOwnEraRewardsImpl (maxEras?: number, ownValidators?: StakerState[], additional?: string[]): State {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const allEras = useCall<EraIndex[]>(api.derive.staking?.erasHistoric);
const [{ filteredEras, validatorEras }, setFiltered] = useState<Filtered>(EMPTY_FILTERED);
const [state, setState] = useState<State>(EMPTY_STATE);
const stakerRewards = useStakerRewards(filteredEras, setState, ownValidators, additional);
const erasPoints = useCall<DeriveEraPoints[]>(!!validatorEras.length && !!filteredEras.length && api.derive.staking._erasPoints, [filteredEras, false]);
const erasRewards = useCall<DeriveEraRewards[]>(!!validatorEras.length && !!filteredEras.length && api.derive.staking._erasRewards, [filteredEras, false]);
useEffect((): void => {
setState({ allRewards: null, isLoadingRewards: true, rewardCount: 0 });
}, [maxEras, ownValidators]);
useEffect((): void => {
if (allEras && maxEras) {
const filteredEras = allEras.slice(-1 * maxEras);
const validatorEras: ValidatorWithEras[] = [];
if (allEras.length === 0) {
setState({
allRewards: {},
isLoadingRewards: false,
rewardCount: 0
});
setFiltered({ filteredEras, validatorEras });
} else if (ownValidators?.length) {
ownValidators.forEach(({ claimedRewardsEras, stakingLedger, stashId }): void => {
if (stakingLedger) {
const eras = filteredEras.filter((era) => !getLegacyRewards(stakingLedger, claimedRewardsEras).some((c) => era.eq(c)));
if (eras.length) {
validatorEras.push({ eras, stashId });
}
}
});
// When we have just claimed, we have filtered eras, but no validator eras - set accordingly
if (filteredEras.length && !validatorEras.length) {
setState({
allRewards: {},
isLoadingRewards: false,
rewardCount: 0
});
}
}
setFiltered({ filteredEras, validatorEras });
}
}, [allEras, maxEras, ownValidators]);
useEffect((): void => {
mountedRef.current && stakerRewards && !ownValidators && setState(
getRewards(stakerRewards)
);
}, [mountedRef, ownValidators, stakerRewards]);
useEffect((): void => {
mountedRef && erasPoints && erasRewards && ownValidators && setState(
getValRewards(api, validatorEras, erasPoints, erasRewards)
);
}, [api, erasPoints, erasRewards, mountedRef, ownValidators, validatorEras]);
return state;
}
export const useOwnEraRewards = createNamedHook('useOwnEraRewards', useOwnEraRewardsImpl);
@@ -0,0 +1,127 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { CombinatorFunction } from '@pezkuwi/api/promise/Combinator';
import type { DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import type { AccountId, ValidatorPrefs } from '@pezkuwi/types/interfaces';
import type { Codec, ITuple } from '@pezkuwi/types/types';
import type { StakerState } from './types.js';
import { useEffect, useMemo, useState } from 'react';
import { u8aConcat, u8aToHex } from '@pezkuwi/util';
import { isEmpty } from './utils/isEmpty.js';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useOwnStashes } from './useOwnStashes.js';
type ValidatorInfo = ITuple<[ValidatorPrefs, Codec]> | ValidatorPrefs;
type Queried = Record<string, [boolean, DeriveStakingAccount, ValidatorInfo]>;
function toIdString (id?: AccountId | null): string | null {
return id
? id.toString()
: null;
}
const QUERY_OPTS = {
withClaimedRewardsEras: true,
withDestination: true,
withLedger: true,
withNominations: true,
withPrefs: true
};
function getStakerState (stashId: string, allAccounts: string[], [isOwnStash, { claimedRewardsEras, controllerId: _controllerId, exposureMeta, exposurePaged, nextSessionIds: _nextSessionIds, nominators, rewardDestination, sessionIds: _sessionIds, stakingLedger, validatorPrefs }, validateInfo]: [boolean, DeriveStakingAccount, ValidatorInfo]): StakerState {
const isStashNominating = !!(nominators?.length);
const isStashValidating = !(Array.isArray(validateInfo) ? isEmpty(validateInfo[1] as ValidatorPrefs) : isEmpty(validateInfo));
const nextSessionIds = _nextSessionIds instanceof Map
? [..._nextSessionIds.values()]
: _nextSessionIds;
const nextConcat = u8aConcat(...nextSessionIds.map((id) => id.toU8a()));
const sessionIds = _sessionIds instanceof Map
? [..._sessionIds.values()]
: _sessionIds;
const currConcat = u8aConcat(...sessionIds.map((id) => id.toU8a()));
const controllerId = toIdString(_controllerId);
return {
claimedRewardsEras,
controllerId,
destination: rewardDestination,
exposureMeta,
exposurePaged,
hexSessionIdNext: u8aToHex(nextConcat, 48),
hexSessionIdQueue: u8aToHex(currConcat.length ? currConcat : nextConcat, 48),
isLoading: false,
isOwnController: allAccounts.includes(controllerId || ''),
isOwnStash,
isStashNominating,
isStashValidating,
// we assume that all ids are non-null
nominating: nominators?.map(toIdString) as string[],
sessionIds: (
nextSessionIds.length
? nextSessionIds
: sessionIds
).map(toIdString) as string[],
stakingLedger,
stashId,
validatorPrefs
};
}
function useOwnStashInfosImpl (apiOverride?: ApiPromise): StakerState[] | undefined {
const { api: connectedApi } = useApi();
const api = useMemo(() => apiOverride ?? connectedApi, [apiOverride, connectedApi]);
const { allAccounts } = useAccounts();
const mountedRef = useIsMountedRef();
const ownStashes = useOwnStashes(undefined, api);
const [queried, setQueried] = useState<Queried | undefined>();
useEffect((): () => void => {
let unsub: (() => void) | undefined;
if (ownStashes) {
if (ownStashes.length) {
const stashIds = ownStashes.map(([stashId]) => stashId);
const fns = [
[api.derive.staking.accounts, stashIds, QUERY_OPTS],
[api.query.staking.validators.multi, stashIds]
] as unknown as CombinatorFunction[];
api.combineLatest<[DeriveStakingAccount[], ValidatorInfo[]]>(fns, ([accounts, validators]): void => {
mountedRef.current && ownStashes.length === accounts.length && ownStashes.length === validators.length && setQueried(
ownStashes.reduce((queried: Queried, [stashId, isOwnStash], index): Queried => ({
...queried,
[stashId]: [isOwnStash, accounts[index], validators[index]]
}), {})
);
}).then((u): void => {
unsub = u;
}).catch(console.error);
} else {
mountedRef.current && setQueried({});
}
}
return (): void => {
unsub && unsub();
};
}, [api, mountedRef, ownStashes]);
return useMemo(
() => ownStashes && queried && ownStashes.length === Object.keys(queried).length
? ownStashes
.filter(([stashId]) => queried[stashId])
.map(([stashId]) => getStakerState(stashId, allAccounts, queried[stashId]))
: undefined,
[allAccounts, ownStashes, queried]
);
}
export const useOwnStashInfos = createNamedHook('useOwnStashInfos', useOwnStashInfosImpl);
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option } from '@pezkuwi/types';
import type { AccountId, StakingLedger } from '@pezkuwi/types/interfaces';
import { useMemo } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
type IsInKeyring = boolean;
function getStashes (allAccounts: string[], ownBonded: Option<AccountId>[], ownLedger: Option<StakingLedger>[]): [string, IsInKeyring][] {
const result: [string, IsInKeyring][] = [];
ownBonded.forEach((value, index): void => {
value.isSome && result.push([allAccounts[index], true]);
});
ownLedger.forEach((ledger): void => {
if (ledger.isSome) {
const stashId = ledger.unwrap().stash.toString();
!result.some(([accountId]) => accountId === stashId) && result.push([stashId, false]);
}
});
return result;
}
function useOwnStashesImpl (additional?: string[], apiOverride?: ApiPromise): [string, IsInKeyring][] | undefined {
const { allAccounts } = useAccounts();
const { api: connectedApi } = useApi();
const api = useMemo(() => apiOverride ?? connectedApi, [apiOverride, connectedApi]);
const ids = useMemo(
() => allAccounts.concat(additional || []),
[allAccounts, additional]
);
const ownBonded = useCall<Option<AccountId>[]>(ids.length !== 0 && api.query.staking?.bonded.multi, [ids]);
const ownLedger = useCall<Option<StakingLedger>[]>(ids.length !== 0 && api.query.staking?.ledger.multi, [ids]);
return useMemo(
() => ids.length
? ownBonded && ownLedger
? getStashes(ids, ownBonded, ownLedger)
: undefined
: [],
[ids, ownBonded, ownLedger]
);
}
export const useOwnStashes = createNamedHook('useOwnStashes', useOwnStashesImpl);
function useOwnStashIdsImpl (additional?: string[]): string[] | undefined {
const ownStashes = useOwnStashes(additional);
return useMemo(
() => ownStashes
? ownStashes.map(([stashId]) => stashId)
: undefined,
[ownStashes]
);
}
export const useOwnStashIds = createNamedHook('useOwnStashIds', useOwnStashIdsImpl);
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import type { BN } from '@pezkuwi/util';
import { useEffect, useState } from 'react';
import { arrayShuffle } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApiUrl } from './useApiUrl.js';
import { useIsMountedRef } from './useIsMountedRef.js';
import { useParaEndpoints } from './useParaEndpoints.js';
interface Result {
api?: ApiPromise | null;
endpoints: LinkOption[];
urls: string[];
}
function useParaApiImpl (paraId: BN | number): Result {
const mountedRef = useIsMountedRef();
const endpoints = useParaEndpoints(paraId);
const [state, setState] = useState<Result>(() => ({
api: null,
endpoints,
urls: []
}));
const api = useApiUrl(state.urls);
useEffect((): void => {
mountedRef.current && setState({
api: null,
endpoints,
urls: arrayShuffle(
endpoints
.filter(({ isDisabled, isUnreachable }) => !isDisabled && !isUnreachable)
.map(({ value }) => value))
});
}, [endpoints, mountedRef]);
useEffect((): void => {
mountedRef.current && setState(({ endpoints, urls }) => ({
api,
endpoints,
urls
}));
}, [api, mountedRef]);
return state;
}
export const useParaApi = createNamedHook('useParaApi', useParaApiImpl);
@@ -0,0 +1,67 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import type { BN } from '@pezkuwi/util';
import { useMemo } from 'react';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { bnToBn } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
const endpoints = createWsEndpoints((k, v) => v?.toString() || k);
function extractRelayEndpoints (genesisHash: string): LinkOption[] {
return endpoints.filter(({ genesisHashRelay }) =>
genesisHash === genesisHashRelay
);
}
function extractParaEndpoints (allEndpoints: LinkOption[], paraId: BN | number): LinkOption[] {
const numId = bnToBn(paraId).toNumber();
return allEndpoints.filter(({ paraId }) =>
paraId === numId
);
}
function useRelayEndpointsImpl (): LinkOption[] {
const { api } = useApi();
return useMemo(
() => extractRelayEndpoints(api.genesisHash.toHex()),
[api]
);
}
export const useRelayEndpoints = createNamedHook('useRelayEndpoints', useRelayEndpointsImpl);
function useParaEndpointsImpl (paraId: BN | number): LinkOption[] {
const endpoints = useRelayEndpoints();
return useMemo(
() => extractParaEndpoints(endpoints, paraId),
[endpoints, paraId]
);
}
export const useParaEndpoints = createNamedHook('useParaEndpoints', useParaEndpointsImpl);
function useIsParasLinkedImpl (ids?: (BN | number)[] | null): Record<string, boolean> {
const endpoints = useRelayEndpoints();
return useMemo(
() => ids
? ids.reduce((all: Record<string, boolean>, id) => ({
...all,
[id.toString()]: extractParaEndpoints(endpoints, id).length !== 0
}), {})
: {},
[endpoints, ids]
);
}
export const useIsParasLinked = createNamedHook('useIsParasLinked', useIsParasLinkedImpl);
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import { useEffect, useState } from 'react';
import { keyring } from '@pezkuwi/ui-keyring';
import { createNamedHook } from './createNamedHook.js';
interface PasswordProps {
password: string;
setPassword: React.Dispatch<string>;
isPasswordValid: boolean;
setIsPasswordValid: React.Dispatch<boolean>;
}
function usePasswordImpl (): PasswordProps {
const [password, setPassword] = useState('');
const [isPasswordValid, setIsPasswordValid] = useState(false);
useEffect((): void => {
setIsPasswordValid(keyring.isPassValid(password));
}, [password]);
return {
isPasswordValid,
password,
setIsPasswordValid,
setPassword
};
}
export const usePassword = createNamedHook('usePassword', usePasswordImpl);
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PayWithAsset } from './ctx/types.js';
import { useContext } from 'react';
import { PayWithAssetCtx } from './ctx/PayWithAsset.js';
import { createNamedHook } from './createNamedHook.js';
function usePayWithAssetImpl (): PayWithAsset {
return useContext(PayWithAssetCtx);
}
export const usePayWithAsset = createNamedHook('usePayWithAsset', usePayWithAssetImpl);
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import { useMemo } from 'react';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { isString } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
const endpoints = createWsEndpoints((k, v) => v?.toString() || k);
export function getPeopleEndpoint (curApiInfo?: string): LinkOption | null {
return endpoints.find(({ info, isPeople }) => isPeople && isString(info) && isString(curApiInfo) && info.toLowerCase().includes(curApiInfo.toLowerCase())) || null;
}
function usePeopleEndpointImpl (relayInfo?: string): LinkOption | null {
return useMemo(() => getPeopleEndpoint(relayInfo), [relayInfo]);
}
export const usePeopleEndpoint = createNamedHook('usePeopleEndpoint', usePeopleEndpointImpl);
@@ -0,0 +1,54 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import type { HorizontalPosition, VerticalPosition } from '@pezkuwi/react-components/Popup/types';
import { useEffect, useMemo, useState } from 'react';
import { getPosition } from '@pezkuwi/react-components/Popup/utils';
import { createNamedHook } from './createNamedHook.js';
import { useElementPosition } from './useElementPosition.js';
import { useScroll } from './useScroll.js';
import { useWindowSize } from './useWindowSize.js';
interface Coord {
x: number;
y: number;
}
interface Result {
pointerStyle: VerticalPosition;
renderCoords: Coord;
}
const COORD_0: Coord = { x: 0, y: 0 };
function usePopupWindowImpl (windowRef: React.RefObject<HTMLDivElement>, triggerRef: React.RefObject<HTMLDivElement>, position: HorizontalPosition): Result {
const [renderCoords, setRenderCoords] = useState<Coord>(COORD_0);
const [pointerStyle, setPointerStyle] = useState<VerticalPosition>('top');
const windowCoords = useElementPosition(windowRef);
const triggerCoords = useElementPosition(triggerRef);
const scrollY = useScroll();
const windowSize = useWindowSize();
useEffect(() => {
if (windowSize && triggerCoords) {
setPointerStyle((triggerCoords.y > windowSize.height / 2) ? 'top' : 'bottom');
}
}, [triggerCoords, windowSize]);
useEffect(() => {
if (windowCoords && triggerCoords) {
setRenderCoords(getPosition(triggerCoords, position, pointerStyle, windowCoords, scrollY, windowSize));
}
}, [position, scrollY, triggerCoords, pointerStyle, windowCoords, windowSize]);
return useMemo(
() => ({ pointerStyle, renderCoords }),
[renderCoords, pointerStyle]
);
}
export const usePopupWindow = createNamedHook('usePopupWindow', usePopupWindowImpl);
+258
View File
@@ -0,0 +1,258 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Bytes, u32, u128 } from '@pezkuwi/types';
import type { AccountId, Hash } from '@pezkuwi/types/interfaces';
import type { FrameSupportPreimagesBounded, PalletPreimageRequestStatus } from '@pezkuwi/types/lookup';
import type { ITuple } from '@pezkuwi/types/types';
import type { HexString } from '@pezkuwi/util/types';
import type { Preimage, PreimageDeposit, PreimageStatus } from './types.js';
import { useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
import { Option } from '@pezkuwi/types';
import { BN, BN_ZERO, formatNumber, isString, isU8a, objectSpread, u8aToHex } from '@pezkuwi/util';
type BytesParamsType = [[proposalHash: HexString, proposalLength: BN]] | [proposalHash: HexString];
interface BytesParams {
paramsBytes?: BytesParamsType;
resultPreimageFor?: PreimageStatus;
}
interface StatusParams {
inlineData?: Uint8Array;
paramsStatus?: [HexString];
proposalHash?: HexString;
resultPreimageHash?: PreimageStatus;
}
type Result = 'unknown' | 'hash' | 'hashAndLen';
interface OldUnrequested {
deposit: ITuple<[AccountId, u128]>;
}
interface OldRequested {
deposit: Option<ITuple<[AccountId, u128]>>;
len: Option<u32>;
}
/**
* @internal Determine if we are working with current generation (H256,u32)
* or previous generation H256 params to the preimageFor storage entry
*/
export function getParamType (api: ApiPromise): Result {
if ((
api.query.preimage &&
api.query.preimage.preimageFor &&
api.query.preimage.preimageFor.creator.meta.type.isMap
)) {
const { type } = api.registry.lookup.getTypeDef(api.query.preimage.preimageFor.creator.meta.type.asMap.key);
if (type === 'H256') {
return 'hash';
} else if (type === '(H256,u32)') {
return 'hashAndLen';
} else {
// we are clueless :()
}
}
return 'unknown';
}
/** @internal Unwraps a passed preimage hash into components */
export function getPreimageHash (api: ApiPromise, hashOrBounded: Hash | HexString | FrameSupportPreimagesBounded): StatusParams {
let proposalHash: HexString | undefined;
let inlineData: Uint8Array | undefined;
if (isString(hashOrBounded)) {
proposalHash = hashOrBounded;
} else if (isU8a(hashOrBounded)) {
proposalHash = hashOrBounded.toHex();
} else {
const bounded = hashOrBounded;
if (bounded.isInline) {
inlineData = bounded.asInline.toU8a(true);
proposalHash = u8aToHex(api.registry.hash(inlineData));
} else if (hashOrBounded.isLegacy) {
proposalHash = hashOrBounded.asLegacy.hash_.toHex();
} else if (hashOrBounded.isLookup) {
proposalHash = hashOrBounded.asLookup.hash_.toHex();
} else {
console.error(`Unhandled FrameSupportPreimagesBounded type ${hashOrBounded.type}`);
}
}
return {
inlineData,
paramsStatus: proposalHash && [proposalHash],
proposalHash,
resultPreimageHash: proposalHash && {
count: 0,
isCompleted: false,
isHashParam: getParamType(api) === 'hash',
proposalHash,
proposalLength: inlineData && new BN(inlineData.length),
status: null
}
};
}
/** @internal Creates a final result */
function createResult (api: ApiPromise, interimResult: PreimageStatus, optBytes: Option<Bytes> | Uint8Array): Preimage {
const callData = isU8a(optBytes)
? optBytes
: optBytes.unwrapOr(null);
const result = (preimage: Pick<Preimage, 'proposal' | 'proposalLength' | 'proposalWarning' | 'proposalError'>) => objectSpread<Preimage>({}, interimResult, {
isCompleted: true,
...preimage
});
if (!callData) {
return result({ proposalWarning: 'No preimage bytes found' });
}
try {
const tx = api.tx(callData.toString());
const proposal = api.createType('Call', tx.method);
if (tx.toHex() === callData.toString()) {
return result({ proposal });
}
} catch {}
try {
const proposal = api.registry.createType('Call', callData);
const callLength = proposal.encodedLength;
if (interimResult.proposalLength) {
const storeLength = interimResult.proposalLength.toNumber();
return result({
proposal,
proposalWarning: callLength !== storeLength ? `Decoded call length does not match on-chain stored preimage length (${formatNumber(callLength)} bytes vs ${formatNumber(storeLength)} bytes)` : null
});
} else {
// for the old style, we set the actual length
return result({
proposal,
proposalLength: new BN(callLength)
});
}
} catch (error) {
console.error(error);
}
return result({ proposalError: 'Unable to decode preimage bytes into a valid Call' });
}
/** @internal Helper to unwrap a deposit tuple into a structure */
function convertDeposit (deposit?: [AccountId, u128] | null): PreimageDeposit | undefined {
return deposit
? {
amount: deposit[1],
who: deposit[0].toString()
}
: undefined;
}
/** @internal Returns the parameters required for a call to bytes */
function getBytesParams (interimResult: PreimageStatus, someOptStatus: Option<PalletPreimageRequestStatus>): BytesParams {
const result = objectSpread<PreimageStatus>({}, interimResult, {
status: someOptStatus.unwrapOr(null)
});
if (result.status) {
if (result.status.isRequested) {
const asRequested = result.status.asRequested;
if (asRequested instanceof Option) {
// FIXME Cannot recall how to deal with these
// (unlike Unrequested below, didn't have an example)
} else {
result.count = asRequested.count.toNumber();
result.deposit = convertDeposit(
asRequested.maybeTicket
? asRequested.maybeTicket.unwrapOr(null)
: (asRequested as unknown as OldRequested).deposit.unwrapOr(null)
);
result.proposalLength = asRequested.maybeLen
? asRequested.maybeLen.unwrapOr(BN_ZERO)
: (asRequested as unknown as OldRequested).len.unwrapOr(BN_ZERO);
}
} else if (result.status.isUnrequested) {
const asUnrequested = result.status.asUnrequested;
if (asUnrequested instanceof Option) {
result.deposit = convertDeposit(
// old-style conversion
(asUnrequested as Option<ITuple<[AccountId, u128]>>).unwrapOr(null)
);
} else {
result.deposit = convertDeposit(asUnrequested.ticket || (asUnrequested as unknown as OldUnrequested).deposit);
result.proposalLength = asUnrequested.len;
}
} else {
console.error(`Unhandled PalletPreimageRequestStatus type: ${result.status.type}`);
}
}
return {
paramsBytes: result.isHashParam
? [result.proposalHash]
: [[result.proposalHash, result.proposalLength || BN_ZERO]],
resultPreimageFor: result
};
}
function usePreimageImpl (hashOrBounded?: Hash | HexString | FrameSupportPreimagesBounded | null): Preimage | undefined {
const { api } = useApi();
// retrieve the status using only the hash of the image
const { inlineData, paramsStatus, resultPreimageHash } = useMemo(
() => hashOrBounded
? getPreimageHash(api, hashOrBounded)
: {},
[api, hashOrBounded]
);
// api.query.preimage.statusFor has been deprecated in favor of api.query.preimage.requestStatusFor.
// To ensure we get all preimages correctly we query both storages. see: https://github.com/pezkuwi-js/apps/pull/10310
const optStatus = useCall<Option<PalletPreimageRequestStatus>>(!inlineData && paramsStatus && api.query.preimage?.statusFor, paramsStatus);
const optRequstStatus = useCall<Option<PalletPreimageRequestStatus>>(!inlineData && paramsStatus && api.query.preimage?.requestStatusFor, paramsStatus);
const someOptStatus = optStatus?.isSome ? optStatus : optRequstStatus;
// from the retrieved status (if any), get the on-chain stored bytes
const { paramsBytes, resultPreimageFor } = useMemo(
() => resultPreimageHash && someOptStatus
? getBytesParams(resultPreimageHash, someOptStatus)
: {},
[someOptStatus, resultPreimageHash]
);
const optBytes = useCall<Option<Bytes>>(paramsBytes && api.query.preimage?.preimageFor, paramsBytes);
// extract all the preimage info we have retrieved
return useMemo(
() => resultPreimageFor
? optBytes
? createResult(api, resultPreimageFor, optBytes)
: resultPreimageFor
: resultPreimageHash
? inlineData
? createResult(api, resultPreimageHash, inlineData)
: resultPreimageHash
: undefined,
[api, inlineData, optBytes, resultPreimageHash, resultPreimageFor]
);
}
export const usePreimage = createNamedHook('usePreimage', usePreimageImpl);
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { AccountId } from '@pezkuwi/types/interfaces';
import type { KitchensinkRuntimeProxyType, PalletProxyProxyDefinition } from '@pezkuwi/types/lookup';
import type { BN } from '@pezkuwi/util';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
const OPTS = {
transform: (result: [([AccountId, KitchensinkRuntimeProxyType] | PalletProxyProxyDefinition)[], BN][], api: ApiPromise): [PalletProxyProxyDefinition[], BN][] =>
api.tx.proxy.addProxy.meta.args.length === 3
? result as [PalletProxyProxyDefinition[], BN][]
: (result as [[AccountId, KitchensinkRuntimeProxyType][], BN][]).map(([arr, bn]): [PalletProxyProxyDefinition[], BN] =>
[arr.map(([delegate, proxyType]): PalletProxyProxyDefinition =>
api.createType('ProxyDefinition', {
delegate,
proxyType
})), bn]
)
};
function useProxiesImpl (): [PalletProxyProxyDefinition[], BN][] | undefined {
const { api } = useApi();
const { allAccounts } = useAccounts();
return useCall<[PalletProxyProxyDefinition[], BN][]>(api.query.proxy?.proxies.multi, [allAccounts], OPTS);
}
export const useProxies = createNamedHook('useProxies', useProxiesImpl);
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { QueueProps } from '@pezkuwi/react-components/Status/types';
import { useContext } from 'react';
import { QueueCtx } from './ctx/Queue.js';
import { createNamedHook } from './createNamedHook.js';
function useQueueImpl (): QueueProps {
return useContext(QueueCtx);
}
export const useQueue = createNamedHook('useQueue', useQueueImpl);
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { OnDemandQueueStatus } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
function extractInfo (value: OnDemandQueueStatus) {
return {
freedIndices: value.freedIndices,
nextIndex: value.nextIndex,
smallestIndex: value.smallestIndex,
traffic: value.traffic
};
}
function useQueueStatusImpl (): OnDemandQueueStatus | undefined {
const { api } = useApi();
const queue = useCall<OnDemandQueueStatus>(api.query.onDemandAssignmentProvider?.queueStatus);
const [state, setState] = useState<OnDemandQueueStatus | undefined>();
useEffect((): void => {
queue &&
setState(
extractInfo(queue)
);
}, [queue]);
return state;
}
export const useQueueStatus = createNamedHook('useQueueStatus', useQueueStatusImpl);
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Option, StorageKey } from '@pezkuwi/types';
import type { PalletBrokerRegionId, PalletBrokerRegionRecord } from '@pezkuwi/types/lookup';
import type { RegionInfo } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall, useMapKeys } from '@pezkuwi/react-hooks';
function extractInfo (core: number, start: number, end: number, owner: string, paid: string, mask: `0x${string}`) {
return {
core,
end,
mask,
owner,
paid,
start
};
}
const OPT_KEY = {
transform: (keys: StorageKey<[PalletBrokerRegionId]>[]): PalletBrokerRegionId[] =>
keys.map(({ args: [regionId] }) => regionId)
};
function useRegionsImpl (api: ApiPromise): RegionInfo[] | undefined {
const regionKeys = useMapKeys(api?.query?.broker.regions, [], OPT_KEY);
const regionInfo = useCall<[[PalletBrokerRegionId[]], Option<PalletBrokerRegionRecord>[]]>(api?.query?.broker.regions.multi, [regionKeys], { withParams: true });
const [state, setState] = useState<RegionInfo[] | undefined>();
useEffect((): void => {
regionInfo &&
regionInfo[0][0].length > 0 &&
setState(
regionInfo[0][0].map((info, index) =>
extractInfo(info.core.toNumber(), info.begin.toNumber(), regionInfo[1][index].unwrap().end.toNumber(), regionInfo[1][index].unwrap().owner.toString(), regionInfo[1][index].unwrap().paid.toString(), info.mask.toHex())
)
);
}, [regionInfo]);
return state;
}
export const useRegions = createNamedHook('useRegions', useRegionsImpl);
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@pezkuwi/types';
import type { RegistrarInfo } from '@pezkuwi/types/interfaces';
import type { Registrar } from './types.js';
import { useMemo } from 'react';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
interface RegistrarNull {
address: string | null;
index: number;
}
interface State {
isRegistrar: boolean;
registrars: Registrar[];
skipQuery?: boolean;
}
function useRegistrarsImpl (skipQuery?: boolean): State {
const { apiIdentity } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const query = useCall<Option<RegistrarInfo>[]>(!skipQuery && apiIdentity.query.identity?.registrars);
// determine if we have a registrar or not - registrars are allowed to approve
return useMemo(
(): State => {
const registrars = (query || [])
.map((registrar, index): RegistrarNull => ({
address: registrar.isSome
? registrar.unwrap().account.toString()
: null,
index
}))
.filter((registrar): registrar is Registrar => !!registrar.address);
return {
isRegistrar: hasAccounts && registrars.some(({ address }) => allAccounts.includes(address)),
registrars
};
},
[allAccounts, hasAccounts, query]
);
}
export const useRegistrars = createNamedHook('useRegistrars', useRegistrarsImpl);
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
import { useEffect, useState } from 'react';
import store from 'store';
import { isBoolean } from '@pezkuwi/util';
type Flags = Record<string, boolean>;
type Setters<T extends Flags> = Record<keyof T, (value: boolean) => void>;
type State<T extends Flags> = [T, Setters<T>];
function getInitial <T extends Flags> (storageKey: string, initial: T): T {
const saved = store.get(`flags:${storageKey}`, {}) as T;
return Object.keys(initial).reduce((result, key: keyof T): T => {
if (isBoolean(saved[key])) {
result[key] = saved[key];
}
return result;
}, { ...initial });
}
function getSetters <T extends Flags> (flags: T, setFlags: React.Dispatch<React.SetStateAction<T>>): Setters<T> {
const setFlag = (key: keyof T) =>
(value: boolean) =>
setFlags((state) => ({ ...state, [key]: value }));
return Object.keys(flags).reduce((setters, key: keyof T): Setters<T> => {
setters[key] = setFlag(key);
return setters;
}, {} as Setters<T>);
}
// TODO Uses generics, we cannot use createNameHook as of yet
export function useSavedFlags <T extends Flags> (storageKey: string, initial: T): State<T> {
const [flags, setFlags] = useState(() => getInitial(storageKey, initial));
const [setters] = useState(() => getSetters(initial, setFlags));
useEffect(
(): void => {
store.set(`flags:${storageKey}`, flags);
},
[flags, storageKey]
);
return [flags, setters];
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useEffect, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
function useScrollImpl (): number {
const [scrollY, setScrollY] = useState(window.scrollY);
const setYOffset = useCallback((): void => setScrollY(window.scrollY), []);
useEffect(() => {
function watchScroll () {
window.addEventListener('scroll', setYOffset);
}
watchScroll();
return () => {
window.removeEventListener('scroll', setYOffset);
};
}, [setYOffset]);
return scrollY;
}
export const useScroll = createNamedHook('useScroll', useScrollImpl);
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { StakingAsyncApis } from './ctx/types.js';
import { useContext } from 'react';
import { StakingAsyncApisCtx } from './ctx/StakingAsync.js';
import { createNamedHook } from './createNamedHook.js';
function useStakingAsyncApisImpl (): StakingAsyncApis {
return useContext(StakingAsyncApisCtx);
}
export const useStakingAsyncApis = createNamedHook('useStakingAsyncApis', useStakingAsyncApisImpl);
@@ -0,0 +1,16 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
function useStakingInfoImpl (accountId: string | null): DeriveStakingAccount | undefined {
const { api } = useApi();
return useCall<DeriveStakingAccount>(api.derive.staking?.account, [accountId]);
}
export const useStakingInfo = createNamedHook('useStakingInfo', useStakingInfoImpl);
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useMemo, useState } from 'react';
import { createNamedHook } from './createNamedHook.js';
type Result = [number, () => void, () => void, (step: number) => void];
function useStepperImpl (): Result {
const [step, setStep] = useState(1);
const nextStep = useCallback(
() => setStep((step) => step + 1),
[]
);
const prevStep = useCallback(
() => setStep((step) => step - 1),
[]
);
return useMemo(
() => [step, nextStep, prevStep, setStep],
[step, nextStep, prevStep, setStep]
);
}
export const useStepper = createNamedHook('useStepper', useStepperImpl);

Some files were not shown because too many files have changed in this diff Show More