feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
type SetStateContext = undefined | (([address, onUpdateName]: [string | null, (() => void) | null]) => void);
export const AccountSidebarCtx = React.createContext<SetStateContext>(undefined);
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiProps } from './types.js';
import React from 'react';
export const ApiCtx = React.createContext<ApiProps>({} as unknown as ApiProps);
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { ProviderStats } from '@pezkuwi/rpc-provider/types';
import type { ApiStats } from './types.js';
import React, { useCallback } from 'react';
import { useApi } from '../useApi.js';
import { useTimer } from '../useTimer.js';
interface Props {
children?: React.ReactNode;
}
const MAX_NUM = 60; // 5 minutes
const INTERVAL = 5_000;
const EMPTY_STATE: ApiStats[] = [];
function getStats (...apis: ApiPromise[]): { stats: ProviderStats, when: number } {
const stats: ProviderStats = {
active: {
requests: 0,
subscriptions: 0
},
total: {
bytesRecv: 0,
bytesSent: 0,
cached: 0,
errors: 0,
requests: 0,
subscriptions: 0,
timeout: 0
}
};
for (let i = 0, count = apis.length; i < count; i++) {
const s = apis[i].stats;
if (s) {
stats.active.requests += s.active.requests;
stats.active.subscriptions += s.active.subscriptions;
stats.total.bytesRecv += s.total.bytesRecv;
stats.total.bytesSent += s.total.bytesSent;
stats.total.cached += s.total.cached;
stats.total.errors += s.total.errors;
stats.total.requests += s.total.requests;
stats.total.subscriptions += s.total.subscriptions;
stats.total.timeout += s.total.timeout;
}
}
return {
stats,
when: Date.now()
};
}
function mergeStats (curr: ApiStats, prev: ApiStats[]): ApiStats[] {
return prev.length === 0
? [curr]
: prev.length === MAX_NUM
? prev.concat(curr).slice(-MAX_NUM)
: prev.concat(curr);
}
export const ApiStatsCtx = React.createContext<ApiStats[]>(EMPTY_STATE);
export function ApiStatsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const stateFn = useCallback(
(prev: ApiStats[]): ApiStats[] =>
mergeStats(getStats(api), prev),
[api]
);
const stats = useTimer(stateFn, EMPTY_STATE, INTERVAL);
return (
<ApiStatsCtx.Provider value={stats}>
{children}
</ApiStatsCtx.Provider>
);
}
@@ -0,0 +1,104 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { EraRewardPoints } from '@pezkuwi/types/interfaces';
import type { AugmentedBlockHeader, BlockAuthors } from './types.js';
import React, { useEffect, useState } from 'react';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import { formatNumber } from '@pezkuwi/util';
interface Props {
children: React.ReactNode;
}
const MAX_HEADERS = 75;
const byAuthor: Record<string, string> = {};
const eraPoints: Record<string, string> = {};
const EMPTY_STATE: BlockAuthors = { byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] };
export const BlockAuthorsCtx = React.createContext<BlockAuthors>(EMPTY_STATE);
export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const queryPoints = useCall<EraRewardPoints>(isApiReady && api.derive.staking?.currentPoints);
const [state, setState] = useState<BlockAuthors>(EMPTY_STATE);
// No unsub, global context - destroyed on app close
useEffect((): void => {
api.isReady.then((): void => {
let lastHeaders: AugmentedBlockHeader[] = [];
let lastBlockAuthors: string[] = [];
let lastBlockNumber = '';
// subscribe to new headers
api.derive.chain.subscribeNewHeads(async (header: HeaderExtended): Promise<void> => {
if (header?.number) {
const timestamp = await ((await api.at(header.hash)).query.timestamp.now());
const lastHeader = Object.assign(header, { timestamp }) as AugmentedBlockHeader;
const blockNumber = lastHeader.number.unwrap();
let thisBlockAuthor = '';
if (lastHeader.author) {
thisBlockAuthor = lastHeader.author.toString();
}
const thisBlockNumber = formatNumber(blockNumber);
if (thisBlockAuthor) {
byAuthor[thisBlockAuthor] = thisBlockNumber;
if (thisBlockNumber !== lastBlockNumber) {
lastBlockNumber = thisBlockNumber;
lastBlockAuthors = [thisBlockAuthor];
} else {
lastBlockAuthors.push(thisBlockAuthor);
}
}
lastHeaders = lastHeaders
.filter((old, index) => index < MAX_HEADERS && old.number.unwrap().lt(blockNumber))
.reduce((next, header): AugmentedBlockHeader[] => {
next.push(header);
return next;
}, [lastHeader])
.sort((a, b) => b.number.unwrap().cmp(a.number.unwrap()));
setState({ byAuthor, eraPoints, lastBlockAuthors: lastBlockAuthors.slice(), lastBlockNumber, lastHeader, lastHeaders });
}
}).catch(console.error);
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect((): void => {
if (queryPoints) {
const entries = [...queryPoints.individual.entries()]
.map(([accountId, points]) => [accountId.toString(), formatNumber(points)]);
const current = Object.keys(eraPoints);
// we have an update, clear all previous
if (current.length !== entries.length) {
current.forEach((accountId): void => {
delete eraPoints[accountId];
});
}
entries.forEach(([accountId, points]): void => {
eraPoints[accountId] = points;
});
}
}, [queryPoints]);
return (
<BlockAuthorsCtx.Provider value={state}>
{children}
</BlockAuthorsCtx.Provider>
);
}
@@ -0,0 +1,119 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { Vec } from '@pezkuwi/types';
import type { EventRecord } from '@pezkuwi/types/interfaces';
import type { BlockEvents, IndexedEvent, KeyedEvent } from './types.js';
import React, { useEffect, useRef, useState } from 'react';
import { stringify, stringToU8a } from '@pezkuwi/util';
import { xxhashAsHex } from '@pezkuwi/util-crypto';
import { useApi } from '../useApi.js';
import { useCall } from '../useCall.js';
interface Props {
children: React.ReactNode;
}
interface PrevHashes {
block: string | null;
event: string | null;
}
const DEFAULT_EVENTS: BlockEvents = { eventCount: 0, events: [] };
const MAX_EVENTS = 75;
export const BlockEventsCtx = React.createContext<BlockEvents>(DEFAULT_EVENTS);
async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec<EventRecord>, setState: React.Dispatch<React.SetStateAction<BlockEvents>>): Promise<void> {
const newEvents: IndexedEvent[] = records
.map((record, index) => ({ indexes: [index], record }))
.filter(({ record: { event: { method, section } } }) =>
section !== 'system' &&
(
!['balances', 'treasury'].includes(section) ||
!['Deposit', 'UpdatedInactive', 'Withdraw'].includes(method)
) &&
(
!['transactionPayment'].includes(section) ||
!['TransactionFeePaid'].includes(method)
) &&
(
!['paraInclusion', 'parasInclusion', 'inclusion'].includes(section) ||
!['CandidateBacked', 'CandidateIncluded'].includes(method)
) &&
(
!['relayChainInfo'].includes(section) ||
!['CurrentBlockNumbers'].includes(method)
)
)
.reduce((combined: IndexedEvent[], e): IndexedEvent[] => {
const prev = combined.find(({ record: { event: { method, section } } }) =>
e.record.event.section === section &&
e.record.event.method === method
);
if (prev) {
prev.indexes.push(...e.indexes);
} else {
combined.push(e);
}
return combined;
}, [])
.reverse();
const newEventHash = xxhashAsHex(stringToU8a(stringify(newEvents)));
if (newEventHash !== prev.event && newEvents.length) {
prev.event = newEventHash;
// retrieve the last header, this will map to the current state
const header = await api.rpc.chain.getHeader(records.createdAtHash);
const blockNumber = header.number.unwrap();
const blockHash = header.hash.toHex();
if (blockHash !== prev.block) {
prev.block = blockHash;
setState(({ events }) => ({
eventCount: records.length,
events: [
...newEvents.map(({ indexes, record }): KeyedEvent => ({
blockHash,
blockNumber,
indexes,
key: `${blockNumber.toNumber()}-${blockHash}-${indexes.join('.')}`,
record
})),
// remove all events for the previous same-height blockNumber
...events.filter((p) => !p.blockNumber?.eq(blockNumber))
].slice(0, MAX_EVENTS)
}));
}
} else {
setState(({ events }) => ({
eventCount: records.length,
events
}));
}
}
export function BlockEventsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const [state, setState] = useState<BlockEvents>(DEFAULT_EVENTS);
const records = useCall<Vec<EventRecord>>(isApiReady && api.query.system.events);
const prevHashes = useRef({ block: null, event: null });
useEffect((): void => {
records && manageEvents(api, prevHashes.current, records, setState).catch(console.error);
}, [api, prevHashes, records, setState]);
return (
<BlockEventsCtx.Provider value={state}>
{children}
</BlockEventsCtx.Provider>
);
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubjectInfo } from '@pezkuwi/ui-keyring/observable/types';
import type { Accounts, Addresses } from './types.js';
import React, { useEffect, useState } from 'react';
import { combineLatest, map } from 'rxjs';
import { keyring } from '@pezkuwi/ui-keyring';
import { u8aToHex } from '@pezkuwi/util';
import { decodeAddress } from '@pezkuwi/util-crypto';
import { useApi } from '../useApi.js';
interface Props {
children?: React.ReactNode;
}
interface State {
accounts: Accounts;
addresses: Addresses;
}
const EMPTY_IS = () => false;
const EMPTY: State = {
accounts: { allAccounts: [], allAccountsHex: [], areAccountsLoaded: false, hasAccounts: false, isAccount: EMPTY_IS },
addresses: { allAddresses: [], allAddressesHex: [], areAddressesLoaded: false, hasAddresses: false, isAddress: EMPTY_IS }
};
export const KeyringCtx = React.createContext<State>(EMPTY);
/**
* @internal Helper function to dedupe a list of items, only adding it if
*
* 1. It is not already present in our list of results
* 2. It does not exist against a secondary list to check
*
* The first check ensures that we never have dupes in the original. The second
* ensures that e.g. an address is not also available as an account
**/
function filter (isEthereum: boolean, items: string[], others: string[] = []): string[] {
const allowedLength = isEthereum
? 20
: 32;
return items.reduce<string[]>((result, a) => {
if (!result.includes(a) && !others.includes(a)) {
try {
if (decodeAddress(a).length >= allowedLength) {
result.push(a);
} else {
console.warn(`Not adding address ${a}, not in correct format for chain (requires publickey from address)`);
}
} catch {
console.error(a, allowedLength);
}
}
return result;
}, []);
}
/**
* @internal Helper function to convert a list of ss58 addresses into hex
**/
function toHex (items: string[]): string[] {
return items
.map((a): string | null => {
try {
return u8aToHex(decodeAddress(a));
} catch (error) {
// This is actually just a failsafe - the keyring really should
// not be passing through invalid ss58 values, but never say never
console.error(`Unable to convert address ${a} to hex`, (error as Error).message);
return null;
}
})
.filter((a): a is string => !!a);
}
/**
* @internal Helper to create an is{Account, Address} check
**/
function createCheck (items: string[]): Accounts['isAccount'] {
return (a?: string | null | { toString: () => string }): boolean =>
!!a && items.includes(a.toString());
}
function extractAccounts (isEthereum: boolean, accounts: SubjectInfo = {}): Accounts {
const allAccounts = filter(isEthereum, Object.keys(accounts));
return {
allAccounts,
allAccountsHex: toHex(allAccounts),
areAccountsLoaded: true,
hasAccounts: allAccounts.length !== 0,
isAccount: createCheck(allAccounts)
};
}
function extractAddresses (isEthereum: boolean, addresses: SubjectInfo = {}, accounts: string[]): Addresses {
const allAddresses = filter(isEthereum, Object.keys(addresses), accounts);
return {
allAddresses,
allAddressesHex: toHex(allAddresses),
areAddressesLoaded: true,
hasAddresses: allAddresses.length !== 0,
isAddress: createCheck(allAddresses)
};
}
export function KeyringCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { isApiReady, isEthereum } = useApi();
const [state, setState] = useState(EMPTY);
useEffect((): () => void => {
let sub: null | { unsubscribe: () => void } = null;
// Defer keyring injection until the API is ready - we need to have the chain
// info to determine which type of addresses we can use (before subscribing)
if (isApiReady) {
sub = combineLatest([
keyring.accounts.subject.pipe(
map((accInfo) => extractAccounts(isEthereum, accInfo))
),
keyring.addresses.subject
])
.pipe(
map(([accounts, addrInfo]): State => ({
accounts,
addresses: extractAddresses(isEthereum, addrInfo, accounts.allAccounts)
}))
)
.subscribe((state) => setState(state));
}
return (): void => {
sub && sub.unsubscribe();
};
}, [isApiReady, isEthereum]);
return (
<KeyringCtx.Provider value={state}>
{children}
</KeyringCtx.Provider>
);
}
@@ -0,0 +1,92 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { AssetInfoComplete } from '../types.js';
import type { PayWithAsset } from './types.js';
import React, { useCallback, useMemo, useState } from 'react';
import { useApi, useAssetIds, useAssetInfos } from '@pezkuwi/react-hooks';
interface Props {
children?: React.ReactNode;
}
const EMPTY_STATE: PayWithAsset = {
assetOptions: [],
isDisabled: true,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange: () => {},
selectedFeeAsset: null
};
export const PayWithAssetCtx = React.createContext<PayWithAsset>(EMPTY_STATE);
export function PayWithAssetCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { isApiReady } = useApi();
return isApiReady ? <PayWithAssetProvider>{children}</PayWithAssetProvider> : <>{children}</>;
}
function PayWithAssetProvider ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const ids = useAssetIds();
const assetInfos = useAssetInfos(ids);
const [selectedFeeAsset, setSelectedFeeAsset] = useState<AssetInfoComplete | null>(null);
const nativeAsset = useMemo(
() => api.registry.chainTokens[0],
[api]
);
const completeAssetInfos = useMemo(
() => (assetInfos
?.filter((i): i is AssetInfoComplete =>
!!(i.details && i.metadata) && !i.details.supply.isZero() && !!i.details?.toJSON().isSufficient)
) || [],
[assetInfos]
);
const assetOptions = useMemo(
() => [
{ text: `${nativeAsset} (Native)`, value: nativeAsset },
...completeAssetInfos.map(({ id, metadata }) => ({
text: `${metadata.name.toUtf8()} (${id.toString()})`,
value: id.toString()
}))],
[completeAssetInfos, nativeAsset]
);
const onChange = useCallback((assetId: BN, cb?: () => void) => {
const selectedFeeAsset = completeAssetInfos.find((a) => a.id.toString() === assetId.toString());
setSelectedFeeAsset(selectedFeeAsset ?? null);
cb?.();
}, [completeAssetInfos]);
const isEnabled = useMemo(() =>
api.registry.signedExtensions.some(
(a) => a === 'ChargeAssetTxPayment'
) &&
!!api.tx.assetConversion &&
!!api.call.assetConversionApi &&
completeAssetInfos.length > 0,
[api.call.assetConversionApi, api.registry.signedExtensions, api.tx.assetConversion, completeAssetInfos.length]
);
const values: PayWithAsset = useMemo(() => {
return {
assetOptions,
isDisabled: !isEnabled,
onChange,
selectedFeeAsset
};
}, [assetOptions, isEnabled, onChange, selectedFeeAsset]);
return (
<PayWithAssetCtx.Provider value={values}>
{children}
</PayWithAssetCtx.Provider>
);
}
+307
View File
@@ -0,0 +1,307 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableResult } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/promise/types';
import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueProps, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from '@pezkuwi/react-components/Status/types';
import type { DispatchError, EventRecord } from '@pezkuwi/types/interfaces';
import type { ITuple, Registry, SignerPayloadJSON } from '@pezkuwi/types/types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { getDispatchError, getIncompleteMessage } from '@pezkuwi/react-components/Status/checks';
import { STATUS_COMPLETE } from '@pezkuwi/react-components/Status/constants';
import { getContractAbi } from '@pezkuwi/react-components/util';
import jsonrpc from '@pezkuwi/types/interfaces/jsonrpc';
export interface Props {
children: React.ReactNode;
}
interface StatusCount {
count: number;
status: ActionStatusPartial;
}
const EMPTY_STATE: Partial<QueueProps> = {
stqueue: [] as QueueStatus[],
txqueue: [] as QueueTx[]
};
let nextId = 0;
const EVENT_MESSAGE = 'extrinsic event';
const REMOVE_TIMEOUT = 7500;
const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic;
export const QueueCtx = React.createContext<QueueProps>(EMPTY_STATE as QueueProps);
function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] {
let others: ActionStatus | null = null;
const initial = status
.reduce((result: StatusCount[], status): StatusCount[] => {
const prev = result.find(({ status: prev }) =>
prev.action === status.action &&
prev.status === status.status
);
if (prev) {
prev.count++;
} else {
result.push({ count: 1, status });
}
return result;
}, [])
.map(({ count, status }): ActionStatusPartial =>
count === 1
? status
: { ...status, action: `${status.action} (x${count})` }
)
.filter((status): boolean => {
if (status.message !== EVENT_MESSAGE) {
return true;
}
if (others) {
if (status.action.startsWith('system.ExtrinsicSuccess')) {
(others.action as string[]).unshift(status.action);
} else {
(others.action as string[]).push(status.action);
}
} else {
others = {
...status,
action: [status.action]
};
}
return false;
});
return others
? initial.concat(others)
: initial;
}
function extractEvents (result?: SubmittableResult): ActionStatus[] {
return mergeStatus(
(result?.events || [])
// filter events handled globally, or those we are not interested in, these are
// handled by the global overview, so don't add them here
.filter((record) =>
!!record.event &&
record.event.section !== 'democracy'
)
.map((record): ActionStatusPartial => {
const { event: { data, method, section } } = record;
if (section === 'system' && method === 'ExtrinsicFailed') {
const [dispatchError] = data as unknown as ITuple<[DispatchError]>;
return {
action: `${section}.${method}`,
message: getDispatchError(dispatchError),
status: 'error'
};
}
const incomplete = getIncompleteMessage(record);
if (incomplete) {
return {
action: `${section}.${method}`,
message: incomplete,
status: 'eventWarn'
};
} else if (section === 'contracts') {
if (method === 'ContractExecution' && data.length === 2) {
// see if we have info for this contract
const [accountId, encoded] = data;
try {
const abi = getContractAbi(accountId.toString());
if (abi) {
const decoded = abi.decodeEvent(encoded as EventRecord);
return {
action: decoded.event.identifier,
message: 'contract event',
status: 'event'
};
}
} catch (error) {
// ABI mismatch?
console.error(error);
}
} else if (method === 'Evicted') {
return {
action: `${section}.${method}`,
message: 'contract evicted',
status: 'error'
};
}
}
return {
action: `${section}.${method}`,
message: EVENT_MESSAGE,
status: 'event'
};
})
);
}
const EMPTY_QUEUE_ST: QueueStatus[] = [];
const EMPTY_QUEUE_TX: QueueTx[] = [];
export function QueueCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [stqueue, _setStQueue] = useState<QueueStatus[]>(EMPTY_QUEUE_ST);
const [txqueue, _setTxQueue] = useState<QueueTx[]>(EMPTY_QUEUE_TX);
const stRef = useRef(stqueue);
const txRef = useRef(txqueue);
const setStQueue = useCallback(
(st: QueueStatus[]): void => {
stRef.current = st;
_setStQueue(st);
},
[]
);
const setTxQueue = useCallback(
(tx: QueueTx[]): void => {
txRef.current = tx;
_setTxQueue(tx);
},
[]
);
const addToTxQueue = useCallback(
(value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => {
const id = ++nextId;
const removeItem = () => setTxQueue([
...txRef.current.map((item): QueueTx =>
item.id === id
? { ...item, status: 'completed' }
: item
)
]);
setTxQueue([...txRef.current, {
...value,
id,
removeItem,
rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC,
status: 'queued'
}]);
},
[setTxQueue]
);
const queueAction = useCallback(
(_status: ActionStatus | ActionStatus[]): void => {
const status = Array.isArray(_status) ? _status : [_status];
status.length && setStQueue([
...stRef.current,
...(status.map((item): QueueStatus => {
const id = ++nextId;
const removeItem = (): void =>
setStQueue([...stRef.current.filter((item) => item.id !== id)]);
setTimeout(removeItem, REMOVE_TIMEOUT);
return {
...item,
id,
isCompleted: false,
removeItem
};
}))
]);
},
[setStQueue]
);
const queueExtrinsic = useCallback(
(value: PartialQueueTxExtrinsic) => addToTxQueue({ ...value }),
[addToTxQueue]
);
const queuePayload = useCallback(
(registry: Registry, payload: SignerPayloadJSON, signerCb: SignerCallback): void => {
addToTxQueue({
accountId: payload.address,
// this is not great, but the Extrinsic doesn't need a submittable
extrinsic: registry.createType('Extrinsic',
{ method: registry.createType('Call', payload.method) },
{ version: payload.version }
) as unknown as SubmittableExtrinsic,
payload,
signerCb
});
},
[addToTxQueue]
);
const queueRpc = useCallback(
(value: PartialQueueTxRpc) => addToTxQueue({ ...value }),
[addToTxQueue]
);
const queueSetTxStatus = useCallback(
(id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => {
setTxQueue([
...txRef.current.map((item): QueueTx =>
item.id === id
? {
...item,
error: error === undefined
? item.error
: error,
result: result === undefined
? item.result as SubmittableResult
: result,
status: item.status === 'completed'
? item.status
: status
}
: item
)
]);
queueAction(extractEvents(result));
if (STATUS_COMPLETE.includes(status)) {
setTimeout((): void => {
const item = txRef.current.find((item) => item.id === id);
item && item.removeItem();
}, REMOVE_TIMEOUT);
}
},
[queueAction, setTxQueue]
);
const value = useMemo(
() => ({
queueAction,
queueExtrinsic,
queuePayload,
queueRpc,
queueSetTxStatus,
stqueue,
txqueue
}),
[queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue]
);
return (
<QueueCtx.Provider value={value}>
{children}
</QueueCtx.Provider>
);
}
@@ -0,0 +1,100 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PropsWithChildren } from 'react';
import type { StakingAsyncApis } from './types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { createWsEndpoints } from '@pezkuwi/apps-config';
import { useApi } from '@pezkuwi/react-hooks';
const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k);
export const getApi = async (url: string[]|string) => {
const api = await ApiPromise.create({
provider: new WsProvider(url)
});
await api.isReadyOrError;
return api;
};
const EMPTY_STATE: StakingAsyncApis = {
ahEndPoints: [],
isRelayChain: false,
isStakingAsync: false,
rcEndPoints: []
};
export const StakingAsyncApisCtx = React.createContext<StakingAsyncApis>(EMPTY_STATE);
export const StakingAsyncApisCtxRoot = ({ children }: PropsWithChildren) => {
const { api, isApiReady } = useApi();
return isApiReady &&
!!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient))
? <StakingAsyncProvider>{children}</StakingAsyncProvider>
: <>{children}</>;
};
export const StakingAsyncProvider = ({ children }: PropsWithChildren) => {
const { api, apiEndpoint } = useApi();
const [ahApi, setAhApi] = useState<ApiPromise>();
const [rcApi, setRcApi] = useState<ApiPromise>();
const isRelayChain = useMemo(() => {
return !!api.tx.stakingAhClient;
}, [api]);
const isStakingAsync = useMemo(() => {
return !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient));
}, [api]);
const rcEndPoints = useMemo(() => {
return (isRelayChain
? apiEndpoint?.providers
: apiEndpoint?.valueRelay)?.filter((e) => e.startsWith('wss://')) || [];
}, [apiEndpoint, isRelayChain]);
const ahEndPoints: string[] = useMemo(() => {
if (isRelayChain) {
return allEndPoints.find(({ genesisHashRelay, paraId }) =>
paraId === 1000 && genesisHashRelay === api.genesisHash.toHex()
)?.providers || [];
}
return apiEndpoint?.providers?.filter((e) => e.startsWith('wss://')) || [];
}, [api, apiEndpoint, isRelayChain]);
useEffect(() => {
if (isRelayChain) {
const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length));
setRcApi(api);
!!ahUrl && getApi(ahUrl).then(setAhApi).catch(console.error);
} else {
const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length));
setAhApi(api);
!!rcUrl && getApi(rcUrl).then(setRcApi).catch(console.error);
}
}, [ahEndPoints, api, isRelayChain, rcEndPoints]);
const state = useMemo(() => ({
ahApi,
ahEndPoints,
isRelayChain,
isStakingAsync,
rcApi,
rcEndPoints
}), [ahApi, ahEndPoints, isRelayChain, isStakingAsync, rcApi, rcEndPoints]);
return <StakingAsyncApisCtx.Provider value={state}>
{children}
</StakingAsyncApisCtx.Provider>;
};
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import React from 'react';
interface SectionType {
icon?: IconName;
text?: string;
}
export const TabsCtx = React.createContext<SectionType>({});
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Adapted from https://hackernoon.com/simplifying-responsive-layouts-with-react-hooks-19db73893a7a
import type { WindowSize } from './types.js';
import React, { useEffect, useState } from 'react';
interface Props {
children: React.ReactNode;
}
function getDimensions (): WindowSize {
return {
height: window.innerHeight,
width: window.innerWidth
};
}
export const WindowSizeCtx = React.createContext<WindowSize>(getDimensions());
export function WindowSizeCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [dimensions, setDimensions] = useState(() => getDimensions());
useEffect((): () => void => {
function handleResize (): void {
setDimensions(getDimensions());
}
window.addEventListener('resize', handleResize);
return (): void => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<WindowSizeCtx.Provider value={dimensions}>
{children}
</WindowSizeCtx.Provider>
);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { ApiStatsCtxRoot } from './ApiStats.js';
export { BlockAuthorsCtxRoot } from './BlockAuthors.js';
export { BlockEventsCtxRoot } from './BlockEvents.js';
export { KeyringCtxRoot } from './Keyring.js';
export { PayWithAssetCtxRoot } from './PayWithAsset.js';
export { QueueCtxRoot } from './Queue.js';
export { StakingAsyncApisCtxRoot } from './StakingAsync.js';
export { WindowSizeCtxRoot } from './WindowSize.js';
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/promise/types';
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import type { InjectedExtension } from '@pezkuwi/extension-inject/types';
import type { ProviderStats } from '@pezkuwi/rpc-provider/types';
import type { BlockNumber, EventRecord, Moment } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
import type { AssetInfoComplete } from '../types.js';
export interface ApiState {
apiDefaultTx: SubmittableExtrinsicFunction;
apiDefaultTxSudo: SubmittableExtrinsicFunction;
chainSS58: number;
fork: Blockchain | null;
hasInjectedAccounts: boolean;
isApiReady: boolean;
isDevelopment: boolean;
isEthereum: boolean;
specName: string;
specVersion: string;
systemChain: string;
systemName: string;
systemVersion: string;
}
export interface ApiProps extends ApiState {
api: ApiPromise;
apiEndpoint: LinkOption | null;
apiError: string | null;
apiIdentity: ApiPromise;
apiCoretime: ApiPromise;
enableIdentity: boolean;
apiRelay: ApiPromise | null;
apiSystemPeople: ApiPromise | null;
apiUrl?: string;
createLink: (path: string, apiUrl?: string) => string;
extensions?: InjectedExtension[];
isApiConnected: boolean;
isApiInitialized: boolean;
isElectron: boolean;
isWaitingInjected: boolean;
isLocalFork?: boolean;
}
export interface Accounts {
allAccounts: string[];
allAccountsHex: string[];
areAccountsLoaded: boolean;
hasAccounts: boolean;
isAccount: (address?: string | null | { toString: () => string }) => boolean;
}
export interface Addresses {
allAddresses: string[];
allAddressesHex: string[];
areAddressesLoaded: boolean;
hasAddresses: boolean;
isAddress: (address?: string | null | { toString: () => string }) => boolean;
}
export interface ApiStats {
stats: ProviderStats;
when: number;
}
export interface PayWithAsset {
isDisabled: boolean;
assetOptions: {text: string, value: string}[];
onChange: (assetId: BN, cb?: () => void) => void;
selectedFeeAsset: AssetInfoComplete | null;
}
interface BlockTime {
readonly timestamp: Moment;
}
export type AugmentedBlockHeader = HeaderExtended & BlockTime;
export interface BlockAuthors {
byAuthor: Record<string, string>;
eraPoints: Record<string, string>;
lastBlockAuthors: string[];
lastBlockNumber?: string;
lastHeader?: AugmentedBlockHeader;
lastHeaders: AugmentedBlockHeader[];
}
export interface BlockEvents {
eventCount: number;
events: KeyedEvent[];
}
export interface IndexedEvent {
indexes: number[];
record: EventRecord;
}
export interface KeyedEvent extends IndexedEvent {
blockHash?: string;
blockNumber?: BlockNumber;
key: string;
}
export type SidebarState = [string | null, (() => void) | null];
export type Sidebar = undefined | (([address, onUpdateName]: SidebarState) => void);
export interface WindowSize {
height: number;
width: number;
}
export interface StakingAsyncApis {
rcApi?: ApiPromise,
ahApi?: ApiPromise,
ahEndPoints: string[],
isRelayChain: boolean,
isStakingAsync: boolean,
rcEndPoints: string[]
}