mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-25 23:07:58 +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,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[]
|
||||
}
|
||||
Reference in New Issue
Block a user