mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-30 07:18:03 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
Reference in New Issue
Block a user