mirror of
https://github.com/pezkuwichain/pezkuwi-sdk-ui.git
synced 2026-06-12 19:21:09 +00:00
Initial commit: Pezkuwi SDK UI
Comprehensive web interface for interacting with Pezkuwi blockchain. Features: - Blockchain explorer - Wallet management - Staking interface - Governance participation - Developer tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Blockchain } from '@acala-network/chopsticks-core';
|
||||
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
|
||||
import type { InjectedExtension } from '@pezkuwi/extension-inject/types';
|
||||
import type { ChainProperties, ChainType } from '@pezkuwi/types/interfaces';
|
||||
import type { KeyringStore } from '@pezkuwi/ui-keyring/types';
|
||||
import type { ApiProps, ApiState, InjectedAccountExt } from './types.js';
|
||||
|
||||
import { ChopsticksProvider, setStorage } from '@acala-network/chopsticks-core';
|
||||
import * as Sc from '@substrate/connect';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import store from 'store';
|
||||
|
||||
import { ApiPromise, ScProvider, WsProvider } from '@pezkuwi/api';
|
||||
import { deriveMapCache, setDeriveCache } from '@pezkuwi/api-derive/util';
|
||||
import { ethereumChains, typesBundle } from '@pezkuwi/apps-config';
|
||||
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
|
||||
import { TokenUnit } from '@pezkuwi/react-components/InputConsts/units';
|
||||
import { useApiUrl, useCoretimeEndpoint, useEndpoint, usePeopleEndpoint, useQueue } from '@pezkuwi/react-hooks';
|
||||
import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
|
||||
import { ApiSigner } from '@pezkuwi/react-signer/signers';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { settings } from '@pezkuwi/ui-settings';
|
||||
import { formatBalance, isNumber, isTestChain, objectSpread, stringify } from '@pezkuwi/util';
|
||||
import { defaults as addressDefaults } from '@pezkuwi/util-crypto/address/defaults';
|
||||
|
||||
import { lightSpecs, relaySpecs } from './light/index.js';
|
||||
import { statics } from './statics.js';
|
||||
import { decodeUrlTypes } from './urlTypes.js';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
apiUrl: string;
|
||||
isElectron: boolean;
|
||||
store?: KeyringStore;
|
||||
beforeApiInit?: React.ReactNode
|
||||
}
|
||||
|
||||
interface ChainData {
|
||||
injectedAccounts: InjectedAccountExt[];
|
||||
properties: ChainProperties;
|
||||
systemChain: string;
|
||||
systemChainType: ChainType;
|
||||
systemName: string;
|
||||
systemVersion: string;
|
||||
}
|
||||
|
||||
interface CreateApiReturn {
|
||||
types: Record<string, Record<string, string>>;
|
||||
fork: Blockchain | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_DECIMALS = statics.registry.createType('u32', 12);
|
||||
export const DEFAULT_SS58 = statics.registry.createType('u32', addressDefaults.prefix);
|
||||
export const DEFAULT_AUX = ['Aux1', 'Aux2', 'Aux3', 'Aux4', 'Aux5', 'Aux6', 'Aux7', 'Aux8', 'Aux9'];
|
||||
|
||||
const DISALLOW_EXTENSIONS: string[] = [];
|
||||
const EMPTY_STATE = { hasInjectedAccounts: false, isApiReady: false } as unknown as ApiState;
|
||||
|
||||
function isKeyringLoaded () {
|
||||
try {
|
||||
return !!keyring.keyring;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevTypes (): Record<string, Record<string, string>> {
|
||||
const types = decodeUrlTypes() || store.get('types', {}) as Record<string, Record<string, string>>;
|
||||
const names = Object.keys(types);
|
||||
|
||||
names.length && console.log('Injected types:', names.join(', '));
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
async function getInjectedAccounts (injectedPromise: Promise<InjectedExtension[]>): Promise<InjectedAccountExt[]> {
|
||||
try {
|
||||
await injectedPromise;
|
||||
|
||||
const accounts = await web3Accounts();
|
||||
|
||||
return accounts.map(({ address, meta, type }, whenCreated): InjectedAccountExt => ({
|
||||
address,
|
||||
meta: objectSpread({}, meta, {
|
||||
name: `${meta.name || 'unknown'} (${meta.source === 'pezkuwi-js' ? 'extension' : meta.source})`,
|
||||
whenCreated
|
||||
}),
|
||||
type: type || 'sr25519'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('web3Accounts', error);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function makeCreateLink (baseApiUrl: string, isElectron: boolean): (path: string) => string {
|
||||
return (path: string, apiUrl?: string): string =>
|
||||
`${isElectron
|
||||
? 'https://pezkuwichain.app/'
|
||||
: `${window.location.origin}${window.location.pathname}`
|
||||
}?rpc=${encodeURIComponent(apiUrl || baseApiUrl)}#${path}`;
|
||||
}
|
||||
|
||||
async function retrieve (api: ApiPromise, injectedPromise: Promise<InjectedExtension[]>): Promise<ChainData> {
|
||||
const [systemChain, systemChainType, systemName, systemVersion, injectedAccounts] = await Promise.all([
|
||||
api.rpc.system.chain(),
|
||||
api.rpc.system.chainType
|
||||
? api.rpc.system.chainType()
|
||||
: Promise.resolve(statics.registry.createType('ChainType', 'Live')),
|
||||
api.rpc.system.name(),
|
||||
api.rpc.system.version(),
|
||||
getInjectedAccounts(injectedPromise)
|
||||
]);
|
||||
|
||||
return {
|
||||
injectedAccounts: injectedAccounts.filter(({ meta: { source } }) =>
|
||||
!DISALLOW_EXTENSIONS.includes(source)
|
||||
),
|
||||
properties: statics.registry.createType('ChainProperties', {
|
||||
isEthereum: api.registry.chainIsEthereum,
|
||||
ss58Format: api.registry.chainSS58,
|
||||
tokenDecimals: api.registry.chainDecimals,
|
||||
tokenSymbol: api.registry.chainTokens
|
||||
}),
|
||||
systemChain: (systemChain || '<unknown>').toString(),
|
||||
systemChainType,
|
||||
systemName: systemName.toString(),
|
||||
systemVersion: systemVersion.toString()
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOnReady (api: ApiPromise, endpoint: LinkOption | null, fork: Blockchain | null, injectedPromise: Promise<InjectedExtension[]>, store: KeyringStore | undefined, types: Record<string, Record<string, string>>, urlIsEthereum = false): Promise<ApiState> {
|
||||
statics.registry.register(types);
|
||||
|
||||
const { injectedAccounts, properties, systemChain, systemChainType, systemName, systemVersion } = await retrieve(api, injectedPromise);
|
||||
const chainSS58 = properties.ss58Format.unwrapOr(DEFAULT_SS58).toNumber();
|
||||
const ss58Format = settings.prefix === -1
|
||||
? chainSS58
|
||||
: settings.prefix;
|
||||
const tokenSymbol = properties.tokenSymbol.unwrapOr([formatBalance.getDefaults().unit, ...DEFAULT_AUX]);
|
||||
const tokenDecimals = properties.tokenDecimals.unwrapOr([DEFAULT_DECIMALS]);
|
||||
const isEthereum = properties.isEthereum.isTrue || ethereumChains.includes(api.runtimeVersion.specName.toString()) || urlIsEthereum;
|
||||
const isDevelopment = (systemChainType.isDevelopment || systemChainType.isLocal || isTestChain(systemChain));
|
||||
|
||||
console.log(`chain: ${systemChain} (${systemChainType.toString()}), ${stringify(properties)}`);
|
||||
|
||||
// explicitly override the ss58Format as specified
|
||||
statics.registry.setChainProperties(
|
||||
statics.registry.createType('ChainProperties', {
|
||||
isEthereum,
|
||||
ss58Format,
|
||||
tokenDecimals,
|
||||
tokenSymbol
|
||||
})
|
||||
);
|
||||
|
||||
// first setup the UI helpers
|
||||
formatBalance.setDefaults({
|
||||
decimals: tokenDecimals.map((b) => b.toNumber()),
|
||||
unit: tokenSymbol[0].toString()
|
||||
});
|
||||
TokenUnit.setAbbr(tokenSymbol[0].toString());
|
||||
|
||||
// finally load the keyring
|
||||
isKeyringLoaded() || keyring.loadAll({
|
||||
genesisHash: api.genesisHash,
|
||||
genesisHashAdd: !isEthereum && endpoint && isNumber(endpoint.paraId) && (endpoint.paraId < 2000) && endpoint.genesisHashRelay
|
||||
? [endpoint.genesisHashRelay]
|
||||
: [],
|
||||
isDevelopment,
|
||||
ss58Format,
|
||||
store,
|
||||
type: isEthereum ? 'ethereum' : 'ed25519'
|
||||
},
|
||||
isEthereum
|
||||
? injectedAccounts.map((account) => {
|
||||
const copy = { ...account };
|
||||
|
||||
copy.type = 'ethereum';
|
||||
|
||||
return copy;
|
||||
})
|
||||
: injectedAccounts
|
||||
);
|
||||
|
||||
const defaultSection = Object.keys(api.tx)[0];
|
||||
const defaultMethod = Object.keys(api.tx[defaultSection])[0];
|
||||
const apiDefaultTx = api.tx[defaultSection][defaultMethod];
|
||||
const apiDefaultTxSudo = api.tx.system?.setCode || apiDefaultTx;
|
||||
|
||||
setDeriveCache(api.genesisHash.toHex(), deriveMapCache);
|
||||
|
||||
return {
|
||||
apiDefaultTx,
|
||||
apiDefaultTxSudo,
|
||||
chainSS58,
|
||||
fork,
|
||||
hasInjectedAccounts: injectedAccounts.length !== 0,
|
||||
isApiReady: true,
|
||||
isDevelopment,
|
||||
isEthereum,
|
||||
specName: api.runtimeVersion.specName.toString(),
|
||||
specVersion: api.runtimeVersion.specVersion.toString(),
|
||||
systemChain,
|
||||
systemName,
|
||||
systemVersion
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Creates a ScProvider from a <relay>[/teyrchain] string
|
||||
*/
|
||||
async function getLightProvider (chain: string): Promise<ScProvider> {
|
||||
const [sc, relayName, paraName] = chain.split('/');
|
||||
|
||||
if (sc !== 'substrate-connect') {
|
||||
throw new Error(`Cannot connect to non substrate-connect protocol ${chain}`);
|
||||
} else if (!relaySpecs[relayName] || (paraName && !lightSpecs[relayName]?.[paraName])) {
|
||||
throw new Error(`Unable to construct light chain ${chain}`);
|
||||
}
|
||||
|
||||
// @ts-expect-error WellKnownChain type mismatch with @substrate/connect
|
||||
const relay = new ScProvider(Sc, relaySpecs[relayName]);
|
||||
|
||||
if (!paraName) {
|
||||
return relay;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const specMod = await import(`${lightSpecs[relayName][paraName]}`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
// @ts-expect-error WellKnownChain type mismatch with @substrate/connect
|
||||
return new ScProvider(Sc, JSON.stringify(specMod.default), relay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async function createApi (apiUrl: string, signer: ApiSigner, isLocalFork: boolean, onError: (error: unknown) => void): Promise<CreateApiReturn> {
|
||||
const types = getDevTypes();
|
||||
const isLight = apiUrl.startsWith('light://');
|
||||
let provider;
|
||||
|
||||
let chopsticksFork: Blockchain | null = null;
|
||||
let chopsticksProvider;
|
||||
let setupChopsticksSuccess = false;
|
||||
|
||||
if (isLocalFork) {
|
||||
try {
|
||||
chopsticksProvider = await ChopsticksProvider.fromEndpoint(apiUrl);
|
||||
chopsticksFork = chopsticksProvider.chain;
|
||||
await setStorage(chopsticksFork, {
|
||||
System: {
|
||||
Account: [
|
||||
[['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], { data: { free: 5000 * 1e12 }, providers: 1 }]
|
||||
]
|
||||
}
|
||||
});
|
||||
setupChopsticksSuccess = true;
|
||||
} catch (error) {
|
||||
store.set('localFork', '');
|
||||
const msg = `Local fork failed, please refresh to switch back to default API provider. This is likely due to chain not supported by chopsticks.
|
||||
Please consider to send an issue to https://github.com/AcalaNetwork/chopsticks.`;
|
||||
|
||||
onError(new Error(msg));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isLight) {
|
||||
provider = await getLightProvider(apiUrl.replace('light://', ''));
|
||||
} else if (isLocalFork && setupChopsticksSuccess) {
|
||||
provider = chopsticksProvider;
|
||||
} else {
|
||||
provider = new WsProvider(apiUrl);
|
||||
}
|
||||
|
||||
statics.api = new ApiPromise({
|
||||
provider,
|
||||
registry: statics.registry,
|
||||
signer,
|
||||
types,
|
||||
typesBundle
|
||||
});
|
||||
|
||||
// See https://github.com/pezkuwi-js/api/pull/4672#issuecomment-1078843960
|
||||
if (isLight) {
|
||||
await provider?.connect();
|
||||
}
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
return { fork: chopsticksFork, types };
|
||||
}
|
||||
|
||||
export function ApiCtxRoot ({ apiUrl, beforeApiInit, children, isElectron, store: keyringStore }: Props): React.ReactElement<Props> | null {
|
||||
const { queuePayload, queueSetTxStatus } = useQueue();
|
||||
const [state, setState] = useState<ApiState>(EMPTY_STATE);
|
||||
const [isApiConnected, setIsApiConnected] = useState(false);
|
||||
const [isApiInitialized, setIsApiInitialized] = useState(false);
|
||||
const [apiError, setApiError] = useState<null | string>(null);
|
||||
const [extensions, setExtensions] = useState<InjectedExtension[] | undefined>();
|
||||
const isLocalFork = useMemo(() => store.get('localFork') === apiUrl, [apiUrl]);
|
||||
const apiEndpoint = useEndpoint(apiUrl);
|
||||
const peopleEndpoint = usePeopleEndpoint(apiEndpoint?.relayName || apiEndpoint?.info);
|
||||
const coreTimeEndpoint = useCoretimeEndpoint(apiEndpoint?.relayName || apiEndpoint?.info);
|
||||
const relayUrls = useMemo(
|
||||
() => (apiEndpoint?.valueRelay && isNumber(apiEndpoint.paraId) && (apiEndpoint.paraId < 2000))
|
||||
? apiEndpoint.valueRelay
|
||||
: null,
|
||||
[apiEndpoint]
|
||||
);
|
||||
const peopleUrls = useMemo(
|
||||
() => (peopleEndpoint?.isPeople && !apiEndpoint?.isPeople && peopleEndpoint?.providers && apiEndpoint?.isPeopleForIdentity)
|
||||
? peopleEndpoint.providers
|
||||
: null,
|
||||
[apiEndpoint, peopleEndpoint]
|
||||
);
|
||||
const coretimeUrls = useMemo(
|
||||
() => (coreTimeEndpoint?.providers)
|
||||
? coreTimeEndpoint.providers
|
||||
: null,
|
||||
[coreTimeEndpoint]
|
||||
);
|
||||
const apiRelay = useApiUrl(relayUrls);
|
||||
const apiCoretime = useApiUrl(coretimeUrls);
|
||||
const apiSystemPeople = useApiUrl(peopleUrls);
|
||||
const createLink = useMemo(
|
||||
() => makeCreateLink(apiUrl, isElectron),
|
||||
[apiUrl, isElectron]
|
||||
);
|
||||
const enableIdentity = apiEndpoint?.isPeople ||
|
||||
// Ensure that teyrchains that don't have isPeopleForIdentity set, can access there own identity pallet.
|
||||
(isNumber(apiEndpoint?.paraId) && (apiEndpoint?.paraId >= 2000) && !apiEndpoint?.isPeopleForIdentity) ||
|
||||
// Ensure that when isPeopleForIdentity is set to false that it enables the identity pallet access.
|
||||
(typeof apiEndpoint?.isPeopleForIdentity === 'boolean' && !apiEndpoint?.isPeopleForIdentity);
|
||||
const value = useMemo<ApiProps>(
|
||||
() => objectSpread({}, state, { api: statics.api, apiCoretime, apiEndpoint, apiError, apiIdentity: ((apiEndpoint?.isPeopleForIdentity && apiSystemPeople) || statics.api), apiRelay, apiSystemPeople, apiUrl, createLink, enableIdentity, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, isWaitingInjected: !extensions }),
|
||||
[apiError, createLink, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, state, apiEndpoint, apiCoretime, apiRelay, apiUrl, apiSystemPeople, enableIdentity]
|
||||
);
|
||||
|
||||
// initial initialization
|
||||
useEffect((): void => {
|
||||
const onError = (error: unknown): void => {
|
||||
console.error(error);
|
||||
|
||||
setApiError((error as Error).message);
|
||||
};
|
||||
|
||||
createApi(apiUrl, new ApiSigner(statics.registry, queuePayload, queueSetTxStatus), isLocalFork, onError)
|
||||
.then(({ fork, types }): void => {
|
||||
statics.api.on('connected', () => setIsApiConnected(true));
|
||||
statics.api.on('disconnected', () => setIsApiConnected(false));
|
||||
statics.api.on('error', onError);
|
||||
statics.api.on('ready', (): void => {
|
||||
const injectedPromise = web3Enable('pezkuwi-js/apps');
|
||||
|
||||
injectedPromise
|
||||
.then(setExtensions)
|
||||
.catch(console.error);
|
||||
|
||||
const urlIsEthereum = !!location.href.includes('keyring-type=ethereum');
|
||||
|
||||
loadOnReady(statics.api, apiEndpoint, fork, injectedPromise, keyringStore, types, urlIsEthereum)
|
||||
.then(setState)
|
||||
.catch(onError);
|
||||
});
|
||||
|
||||
if (isLocalFork) {
|
||||
// clear localFork from local storage onMount after api setup
|
||||
store.set('localFork', '');
|
||||
statics.api.connect()
|
||||
.catch(onError);
|
||||
}
|
||||
|
||||
setIsApiInitialized(true);
|
||||
})
|
||||
.catch(onError);
|
||||
}, [apiEndpoint, apiUrl, queuePayload, queueSetTxStatus, keyringStore, isLocalFork]);
|
||||
|
||||
if (!value.isApiInitialized) {
|
||||
return <>{beforeApiInit}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ApiCtx.Provider value={value}>
|
||||
{children}
|
||||
</ApiCtx.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiProps, SubtractProps } from '../types.js';
|
||||
import type { DefaultProps } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
|
||||
import { assert } from '@pezkuwi/util';
|
||||
|
||||
export default function withApi <P extends ApiProps> (Inner: React.ComponentType<P>, defaultProps: DefaultProps = {}): React.ComponentType<any> {
|
||||
class WithApi extends React.PureComponent<SubtractProps<P, ApiProps>> {
|
||||
private component: any = React.createRef();
|
||||
|
||||
public override render (): React.ReactNode {
|
||||
return (
|
||||
<ApiCtx.Consumer>
|
||||
{(apiProps?: ApiProps): React.ReactNode => {
|
||||
assert(apiProps?.api, 'Application root must be wrapped inside \'react-api/Api\' to provide API context');
|
||||
|
||||
return (
|
||||
<Inner
|
||||
{...defaultProps}
|
||||
{...(apiProps as any)}
|
||||
{...this.props}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
ref={this.component}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ApiCtx.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithApi;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// SInce this file is deemed deprecated (and awaiting removal), we just don't care
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import type { ApiProps, CallState as State, OnChangeCb, SubtractProps } from '../types.js';
|
||||
import type { Options } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { assert, isNull, isUndefined, nextTick } from '@pezkuwi/util';
|
||||
|
||||
import echoTransform from '../transform/echo.js';
|
||||
import { isEqual, triggerChange } from '../util/index.js';
|
||||
import withApi from './api.js';
|
||||
|
||||
// FIXME This is not correct, we need some junction of derive, query & consts
|
||||
interface Method {
|
||||
(...params: unknown[]): Promise<any>;
|
||||
at: (hash: Uint8Array | string, ...params: unknown[]) => Promise<any>;
|
||||
meta: any;
|
||||
multi: (params: unknown[], cb: (value?: any) => void) => Promise<any>;
|
||||
}
|
||||
|
||||
type ApiMethodInfo = [Method, unknown[], string];
|
||||
|
||||
const NOOP = (): void => {
|
||||
// ignore
|
||||
};
|
||||
|
||||
const NO_SKIP = (): boolean => false;
|
||||
|
||||
// a mapping of actual error messages that has already been shown
|
||||
const errorred: Record<string, boolean> = {};
|
||||
|
||||
export default function withCall<P extends ApiProps> (endpoint: string, { at, atProp, callOnResult, fallbacks, isMulti = false, paramName, paramPick, paramValid = false, params = [], propName, skipIf = NO_SKIP, transform = echoTransform, withIndicator = false }: Options = {}): (Inner: React.ComponentType<ApiProps>) => React.ComponentType<any> {
|
||||
return (Inner: React.ComponentType<ApiProps>): React.ComponentType<SubtractProps<P, ApiProps>> => {
|
||||
class WithPromise extends React.Component<P, State> {
|
||||
public override state: State = {
|
||||
callResult: undefined,
|
||||
callUpdated: false,
|
||||
callUpdatedAt: 0
|
||||
};
|
||||
|
||||
private destroy?: () => void;
|
||||
|
||||
private isActive = false;
|
||||
|
||||
private propName: string;
|
||||
|
||||
private timerId = -1;
|
||||
|
||||
constructor (props: P) {
|
||||
super(props);
|
||||
|
||||
const [, section, method] = endpoint.split('.');
|
||||
|
||||
this.propName = `${section}_${method}`;
|
||||
}
|
||||
|
||||
public override componentDidUpdate (prevProps: any): void {
|
||||
const oldParams = this.getParams(prevProps);
|
||||
const newParams = this.getParams(this.props);
|
||||
|
||||
if (this.isActive && !isEqual(newParams, oldParams)) {
|
||||
this
|
||||
.subscribe(newParams)
|
||||
.then(NOOP)
|
||||
.catch(NOOP);
|
||||
}
|
||||
}
|
||||
|
||||
public override componentDidMount (): void {
|
||||
this.isActive = true;
|
||||
|
||||
if (withIndicator) {
|
||||
this.timerId = window.setInterval((): void => {
|
||||
const elapsed = Date.now() - (this.state.callUpdatedAt || 0);
|
||||
const callUpdated = elapsed <= 1500;
|
||||
|
||||
if (callUpdated !== this.state.callUpdated) {
|
||||
this.nextState({ callUpdated });
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// The attachment takes time when a lot is available, set a timeout
|
||||
// to first handle the current queue before subscribing
|
||||
nextTick((): void => {
|
||||
this
|
||||
.subscribe(this.getParams(this.props))
|
||||
.then(NOOP)
|
||||
.catch(NOOP);
|
||||
});
|
||||
}
|
||||
|
||||
public override componentWillUnmount (): void {
|
||||
this.isActive = false;
|
||||
|
||||
this.unsubscribe()
|
||||
.then(NOOP)
|
||||
.catch(NOOP);
|
||||
|
||||
if (this.timerId !== -1) {
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
}
|
||||
|
||||
private nextState (state: Partial<State>): void {
|
||||
if (this.isActive) {
|
||||
this.setState(state as State);
|
||||
}
|
||||
}
|
||||
|
||||
private getParams (props: any): [boolean, unknown[]] {
|
||||
const paramValue = paramPick
|
||||
? paramPick(props)
|
||||
: paramName
|
||||
? props[paramName]
|
||||
: undefined;
|
||||
|
||||
if (atProp) {
|
||||
at = props[atProp];
|
||||
}
|
||||
|
||||
// When we are specifying a param and have an invalid, don't use it. For 'params',
|
||||
// we default to the original types, i.e. no validation (query app uses this)
|
||||
if (!paramValid && paramName && (isUndefined(paramValue) || isNull(paramValue))) {
|
||||
return [false, []];
|
||||
}
|
||||
|
||||
const values = isUndefined(paramValue)
|
||||
? params
|
||||
: params.concat(
|
||||
(Array.isArray(paramValue) && !(paramValue as any).toU8a)
|
||||
? paramValue
|
||||
: [paramValue]
|
||||
);
|
||||
|
||||
return [true, values];
|
||||
}
|
||||
|
||||
private constructApiSection = (endpoint: string): [Record<string, Method>, string, string, string] => {
|
||||
const { api } = this.props;
|
||||
const [area, section, method, ...others] = endpoint.split('.');
|
||||
|
||||
assert(area.length && section.length && method.length && others.length === 0, `Invalid API format, expected <area>.<section>.<method>, found ${endpoint}`);
|
||||
assert(['consts', 'rpc', 'query', 'derive'].includes(area), `Unknown api.${area}, expected consts, rpc, query or derive`);
|
||||
assert(!at || area === 'query', 'Only able to do an \'at\' query on the api.query interface');
|
||||
|
||||
const apiSection = (api as any)[area][section];
|
||||
|
||||
return [
|
||||
apiSection,
|
||||
area,
|
||||
section,
|
||||
method
|
||||
];
|
||||
};
|
||||
|
||||
private getApiMethod (newParams: unknown[]): ApiMethodInfo {
|
||||
if (endpoint === 'subscribe') {
|
||||
const [fn, ...params] = newParams;
|
||||
|
||||
return [
|
||||
fn as Method,
|
||||
params,
|
||||
'subscribe'
|
||||
];
|
||||
}
|
||||
|
||||
const endpoints = [endpoint].concat(fallbacks || []);
|
||||
const expanded = endpoints.map(this.constructApiSection);
|
||||
const [apiSection, area, section, method] = expanded.find(([apiSection]): boolean =>
|
||||
!!apiSection
|
||||
) || [{}, expanded[0][1], expanded[0][2], expanded[0][3]];
|
||||
|
||||
assert(apiSection?.[method], `Unable to find api.${area}.${section}.${method}`);
|
||||
|
||||
const meta = apiSection[method].meta;
|
||||
|
||||
if (area === 'query' && meta?.type.isMap) {
|
||||
const arg = newParams[0];
|
||||
|
||||
assert((!isUndefined(arg) && !isNull(arg)) || meta.type.asMap.kind.isLinkedMap, `${meta.name} expects one argument`);
|
||||
}
|
||||
|
||||
return [
|
||||
apiSection[method],
|
||||
newParams,
|
||||
method.startsWith('subscribe') ? 'subscribe' : area
|
||||
];
|
||||
}
|
||||
|
||||
private async subscribe ([isValid, newParams]: [boolean, unknown[]]): Promise<void> {
|
||||
if (!isValid || skipIf(this.props)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { api } = this.props;
|
||||
let info: ApiMethodInfo | undefined;
|
||||
|
||||
await api.isReady;
|
||||
|
||||
try {
|
||||
assert(at || !atProp, 'Unable to perform query on non-existent at hash');
|
||||
|
||||
info = this.getApiMethod(newParams);
|
||||
} catch (error) {
|
||||
// don't flood the console with the same errors each time, just do it once, then
|
||||
// ignore it going forward
|
||||
if (!errorred[(error as Error).message]) {
|
||||
console.warn(endpoint, '::', error);
|
||||
|
||||
errorred[(error as Error).message] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [apiMethod, params, area] = info;
|
||||
const updateCb = (value?: any): void =>
|
||||
this.triggerUpdate(this.props, value);
|
||||
|
||||
await this.unsubscribe();
|
||||
|
||||
try {
|
||||
if (['derive', 'subscribe'].includes(area) || (area === 'query' && (!at && !atProp))) {
|
||||
this.destroy = isMulti
|
||||
? await apiMethod.multi(params, updateCb)
|
||||
: await apiMethod(...params, updateCb);
|
||||
} else if (area === 'consts') {
|
||||
updateCb(apiMethod);
|
||||
} else {
|
||||
updateCb(
|
||||
at
|
||||
? await apiMethod.at(at, ...params)
|
||||
: await apiMethod(...params)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
private async unsubscribe (): Promise<void> {
|
||||
if (this.destroy) {
|
||||
this.destroy();
|
||||
this.destroy = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private triggerUpdate (props: any, value?: any): void {
|
||||
try {
|
||||
const callResult = (props.transform || transform)(value);
|
||||
|
||||
if (!this.isActive || isEqual(callResult, this.state.callResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerChange(callResult as OnChangeCb, callOnResult, props.callOnResult as OnChangeCb);
|
||||
|
||||
this.nextState({
|
||||
callResult,
|
||||
callUpdated: true,
|
||||
callUpdatedAt: Date.now()
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public override render (): React.ReactNode {
|
||||
const { callResult, callUpdated, callUpdatedAt } = this.state;
|
||||
const _props = {
|
||||
...this.props,
|
||||
callUpdated,
|
||||
callUpdatedAt
|
||||
};
|
||||
|
||||
if (!isUndefined(callResult)) {
|
||||
(_props as any)[propName || this.propName] = callResult;
|
||||
}
|
||||
|
||||
return (
|
||||
<Inner {..._props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return withApi(WithPromise);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BaseProps } from '../types.js';
|
||||
import type { DefaultProps, Options } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import withCall from './call.js';
|
||||
|
||||
interface Props<T> extends BaseProps<T> {
|
||||
callResult?: T;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default function withCallDiv<T> (endpoint: string, options: Options = {}) {
|
||||
return (render: (value?: T) => React.ReactNode, defaultProps: DefaultProps = {}): React.ComponentType<any> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
function Inner ({ callResult, callUpdated, children, className = defaultProps.className, label = '' }: any): React.ReactElement<Props<T>> {
|
||||
return (
|
||||
<div
|
||||
{...defaultProps}
|
||||
className={[className || '', callUpdated ? 'rx--updated' : undefined].join(' ')}
|
||||
>
|
||||
{label}{render(callResult as T)}{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return withCall(endpoint, { ...options, propName: 'callResult' })(Inner);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
import type { ApiProps, SubtractProps } from '../types.js';
|
||||
import type { Options } from './types.js';
|
||||
|
||||
import withCall from './call.js';
|
||||
|
||||
type Call = string | [string, Options];
|
||||
|
||||
export default function withCalls <P> (...calls: Call[]): (Component: React.ComponentType<P>) => React.ComponentType<SubtractProps<P, ApiProps>> {
|
||||
return (Component: React.ComponentType<P>): React.ComponentType<any> => {
|
||||
// NOTE: Order is reversed so it makes sense in the props, i.e. component
|
||||
// after something can use the value of the preceding version
|
||||
return calls
|
||||
.reverse()
|
||||
.reduce((Component, call): React.ComponentType<any> => {
|
||||
return Array.isArray(call)
|
||||
? withCall(...call)(Component as unknown as React.ComponentType<ApiProps>)
|
||||
: withCall(call)(Component as unknown as React.ComponentType<ApiProps>);
|
||||
}, Component);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { default as withApi } from './api.js';
|
||||
export { default as withCall } from './call.js';
|
||||
export { default as withCallDiv } from './callDiv.js';
|
||||
export { default as withCalls } from './calls.js';
|
||||
export { default as withMulti } from './multi.js';
|
||||
export { default as withObservable } from './observable.js';
|
||||
export * from './onlyOn.js';
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
type HOC = (Component: React.ComponentType<any>) => React.ComponentType<any>;
|
||||
|
||||
export default function withMulti<T> (Component: React.ComponentType<T>, ...hocs: HOC[]): React.ComponentType<any> {
|
||||
// NOTE: Order is reversed so it makes sense in the props, i.e. component
|
||||
// after something can use the value of the preceding version
|
||||
return hocs
|
||||
.reverse()
|
||||
.reduce((Component, hoc): React.ComponentType<any> =>
|
||||
hoc(Component), Component
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// TODO: Lots of duplicated code between this and withObservable, surely there is a better way of doing this?
|
||||
|
||||
import type { Observable, OperatorFunction } from 'rxjs';
|
||||
import type { CallState } from '../types.js';
|
||||
import type { DefaultProps, HOC, Options, RenderFn } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import { catchError, map, of } from 'rxjs';
|
||||
|
||||
import echoTransform from '../transform/echo.js';
|
||||
import { intervalObservable, isEqual, triggerChange } from '../util/index.js';
|
||||
|
||||
interface State extends CallState {
|
||||
subscriptions: { unsubscribe: () => void }[];
|
||||
}
|
||||
|
||||
export default function withObservable<T, P> (observable: Observable<P>, { callOnResult, propName = 'value', transform = echoTransform }: Options = {}): HOC {
|
||||
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps = {}, render?: RenderFn): React.ComponentType<any> => {
|
||||
class WithObservable extends React.Component<any, State> {
|
||||
private isActive = true;
|
||||
|
||||
public override state: State = {
|
||||
callResult: undefined,
|
||||
callUpdated: false,
|
||||
callUpdatedAt: 0,
|
||||
subscriptions: []
|
||||
};
|
||||
|
||||
public override componentDidMount (): void {
|
||||
this.setState({
|
||||
subscriptions: [
|
||||
observable
|
||||
.pipe(
|
||||
map(transform) as OperatorFunction<P, any>,
|
||||
catchError(() => of(undefined))
|
||||
)
|
||||
.subscribe((value) => this.triggerUpdate(this.props, value as T)),
|
||||
intervalObservable(this)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
public override componentWillUnmount (): void {
|
||||
this.isActive = false;
|
||||
this.state.subscriptions.forEach((subscription): void =>
|
||||
subscription.unsubscribe()
|
||||
);
|
||||
}
|
||||
|
||||
private triggerUpdate = (props: P, callResult?: T): void => {
|
||||
try {
|
||||
if (!this.isActive || isEqual(callResult, this.state.callResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerChange(callResult, callOnResult, (props as Options).callOnResult || defaultProps.callOnResult);
|
||||
|
||||
this.setState({
|
||||
callResult,
|
||||
callUpdated: true,
|
||||
callUpdatedAt: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(this.props, error);
|
||||
}
|
||||
};
|
||||
|
||||
public override render (): React.ReactNode {
|
||||
const { children } = this.props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { callResult, callUpdated, callUpdatedAt } = this.state;
|
||||
const _props = {
|
||||
...defaultProps,
|
||||
...this.props,
|
||||
callUpdated,
|
||||
callUpdatedAt,
|
||||
[propName]: callResult
|
||||
};
|
||||
|
||||
return (
|
||||
<Inner {..._props}>
|
||||
{render?.(callResult)}{children}
|
||||
</Inner>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithObservable;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import type { Environment } from '../types.js';
|
||||
|
||||
import { getEnvironment } from '../util/index.js';
|
||||
|
||||
const onlyOn = (environment: Environment) => <T extends ComponentType<any>>(component: T): T | (() => null) => {
|
||||
if (getEnvironment() === environment) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
return () => null;
|
||||
};
|
||||
|
||||
export const onlyOnWeb = onlyOn('web');
|
||||
export const onlyOnApp = onlyOn('app');
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
import type { OnChangeCb } from '../types.js';
|
||||
|
||||
export type Transform = (value: any, index: number) => any;
|
||||
|
||||
export interface DefaultProps {
|
||||
callOnResult?: OnChangeCb;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
at?: Uint8Array | string;
|
||||
atProp?: string;
|
||||
callOnResult?: OnChangeCb;
|
||||
fallbacks?: string[];
|
||||
isMulti?: boolean;
|
||||
params?: unknown[];
|
||||
paramName?: string;
|
||||
paramPick?: (props: any) => unknown;
|
||||
paramValid?: boolean;
|
||||
propName?: string;
|
||||
skipIf?: (props: any) => boolean;
|
||||
transform?: Transform;
|
||||
withIndicator?: boolean;
|
||||
}
|
||||
|
||||
export type RenderFn = (value?: any) => any;
|
||||
|
||||
export type StorageTransform = (input: any, index: number) => unknown;
|
||||
|
||||
export type HOC = (Component: React.ComponentType<unknown>, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType<unknown>;
|
||||
|
||||
export interface ApiMethod {
|
||||
name: string;
|
||||
section?: string;
|
||||
}
|
||||
|
||||
export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType<any>;
|
||||
|
||||
export type OmitProps<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type SubtractProps<T, K> = OmitProps<T, keyof K>;
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { ApiCtxRoot, DEFAULT_DECIMALS, DEFAULT_SS58 } from './Api.js';
|
||||
export { withApi, withCallDiv, withCalls, withMulti, withObservable } from './hoc/index.js';
|
||||
export { statics } from './statics.js';
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
|
||||
import { assert } from '@pezkuwi/util';
|
||||
|
||||
import { lightSpecs } from './light/index.js';
|
||||
|
||||
const srcRel = 'packages/react-api/src';
|
||||
const specDir = path.join(process.cwd(), srcRel);
|
||||
|
||||
describe('lightSpecs', (): void => {
|
||||
for (const [k, specs] of Object.entries(lightSpecs)) {
|
||||
describe(`${k}`, (): void => {
|
||||
for (const [k, info] of Object.entries(specs)) {
|
||||
it(`${k}`, (): void => {
|
||||
assert(
|
||||
fs.existsSync(path.join(specDir, info)),
|
||||
`${srcRel}/${info.slice(2)} does not exist`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WellKnownChain } from '@substrate/connect';
|
||||
|
||||
import { specs as kusama } from './kusama/index.js';
|
||||
import { specs as polkadot } from './polkadot/index.js';
|
||||
|
||||
export const lightSpecs: Record<string, Record<string, string>> =
|
||||
Object
|
||||
.entries({ kusama, polkadot })
|
||||
.reduce((all: Record<string, Record<string, string>>, [r, v]) => {
|
||||
all[r] = v.reduce((specs: Record<string, string>, k) => {
|
||||
specs[k] = `./light/${r}/${k}.json`;
|
||||
|
||||
return specs;
|
||||
}, {});
|
||||
|
||||
return all;
|
||||
}, {});
|
||||
|
||||
export const relaySpecs: Record<string, string> = {
|
||||
kusama: WellKnownChain.ksmcc3,
|
||||
polkadot: WellKnownChain.polkadot,
|
||||
rococo: WellKnownChain.rococo_v2_2,
|
||||
westend: WellKnownChain.westend2
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Add your imported spec here in alphabetical order.
|
||||
// The key here reflects the URL of the light client endpoint.
|
||||
// e.g. light://substrate-connect/kusama/gm
|
||||
export const specs: string[] = [
|
||||
'gm',
|
||||
'shiden',
|
||||
'tinkernet'
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Add your imported spec here in alphabetical order.
|
||||
// The key here reflects the URL of the light client endpoint.
|
||||
// e.g. light://substrate-connect/polkadot/astar
|
||||
export const specs: string[] = [
|
||||
'astar',
|
||||
'laos'
|
||||
];
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Laos Network",
|
||||
"id": "laos_network",
|
||||
"chainType": "Live",
|
||||
"bootNodes": [
|
||||
"/dns4/laos-boot-0.laosfoundation.io/tcp/30334/p2p/12D3KooWPwbNZK339oHX2BGrkp9UAkZ5XKaWkkejy4kj4ZU3aKM5",
|
||||
"/dns4/laos-boot-1.laosfoundation.io/tcp/30334/p2p/12D3KooWH9tUB68tBwUfP54NJkGxwx7cxKmuoLX5gpHkHiESLoeJ",
|
||||
"/dns4/laos-boot-2.laosfoundation.io/tcp/30334/p2p/12D3KooWEv926SQ6djXFEMMskZKKMuN3HwJYoCZKBHvymU8Dp5Qc",
|
||||
"/dns4/bootnode1.laos.gorengine.com/tcp/443/wss/p2p/12D3KooWQtyzyDVMFi5bctjrTtqQNdodRSwGE5hhhz8iWgYBdUvT",
|
||||
"/dns4/bootnode0.laos.gorengine.com/tcp/443/wss/p2p/12D3KooWPjWDdS8BNAsp2x5koLaFC4speG9J95eABWGU27ypfhAf"
|
||||
],
|
||||
"protocolId": "laos_network",
|
||||
"properties": {
|
||||
"ss58Format": 42,
|
||||
"tokenDecimals": 18,
|
||||
"tokenSymbol": "LAOS"
|
||||
},
|
||||
"relay_chain": "polkadot",
|
||||
"para_id": 3370,
|
||||
"codeSubstitutes": {},
|
||||
"genesis": {
|
||||
"stateRootHash": "0xd80e8c97286442f29e6bcb8a2d8e692099c15a1ab5fe3a337a7ddc8b4a62744f"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
|
||||
import { TypeRegistry } from '@pezkuwi/types/create';
|
||||
|
||||
interface Statics {
|
||||
api: ApiPromise;
|
||||
registry: TypeRegistry;
|
||||
}
|
||||
|
||||
// NOTE We are assuming that the Api class _will_ set it correctly
|
||||
export const statics = {
|
||||
api: undefined,
|
||||
registry: new TypeRegistry()
|
||||
} as unknown as Statics;
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export default function echoTransform <T> (x: T, _index: number): T {
|
||||
return x;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Blockchain } from '@acala-network/chopsticks-core';
|
||||
import type React from 'react';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/promise/types';
|
||||
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
|
||||
import type { InjectedExtension } from '@pezkuwi/extension-inject/types';
|
||||
import type { KeypairType } from '@pezkuwi/util-crypto/types';
|
||||
|
||||
// helpers for HOC props
|
||||
export type OmitProps<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type SubtractProps<T, K> = OmitProps<T, keyof K>;
|
||||
|
||||
export interface BareProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface InjectedAccountExt {
|
||||
address: string;
|
||||
meta: {
|
||||
name: string;
|
||||
source: string;
|
||||
whenCreated: number;
|
||||
};
|
||||
type: KeypairType;
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* The identity api used for retrieving identities from the people chain.
|
||||
*/
|
||||
apiIdentity: ApiPromise;
|
||||
/**
|
||||
* Used for checking if tx.identity.* should be used. Can be used for other scenarios as well.
|
||||
*/
|
||||
enableIdentity: boolean;
|
||||
apiCoretime: ApiPromise;
|
||||
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 OnChangeCbObs {
|
||||
next: (value?: any) => any;
|
||||
}
|
||||
|
||||
export type OnChangeCbFn = (value?: any) => any;
|
||||
export type OnChangeCb = OnChangeCbObs | OnChangeCbFn;
|
||||
|
||||
export interface ChangeProps {
|
||||
callOnResult?: OnChangeCb;
|
||||
}
|
||||
|
||||
export interface CallState {
|
||||
callResult?: unknown;
|
||||
callUpdated?: boolean;
|
||||
callUpdatedAt?: number;
|
||||
}
|
||||
|
||||
export type CallProps = ApiProps & CallState;
|
||||
|
||||
export interface BaseProps<T> extends BareProps, CallProps, ChangeProps {
|
||||
children?: React.ReactNode;
|
||||
label?: string;
|
||||
render?: (value?: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
export type Formatter = (value?: any) => string;
|
||||
|
||||
export type Environment = 'web' | 'app';
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017-2026 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { unzlibSync, zlibSync } from 'fflate';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { settings } from '@pezkuwi/ui-settings';
|
||||
import { assert, stringToU8a, u8aToString } from '@pezkuwi/util';
|
||||
import { base64Decode, base64Encode } from '@pezkuwi/util-crypto';
|
||||
|
||||
export function decodeUrlTypes (): Record<string, any> | null {
|
||||
const urlOptions = queryString.parse(location.href.split('?')[1]);
|
||||
|
||||
if (urlOptions.types) {
|
||||
try {
|
||||
assert(!Array.isArray(urlOptions.types), 'Expected a single type specification');
|
||||
|
||||
const parts = urlOptions.types.split('#');
|
||||
const compressed = base64Decode(decodeURIComponent(parts[0]));
|
||||
const uncompressed = unzlibSync(compressed);
|
||||
|
||||
return JSON.parse(u8aToString(uncompressed)) as Record<string, any>;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function encodeUrlTypes (types: Record<string, any>): string {
|
||||
const jsonU8a = stringToU8a(JSON.stringify(types));
|
||||
const compressed = zlibSync(jsonU8a, { level: 9 });
|
||||
const encoded = base64Encode(compressed);
|
||||
|
||||
return `${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(settings.apiUrl)}&types=${encodeURIComponent(encoded)}`;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Environment } from '../types.js';
|
||||
|
||||
// https://github.com/electron/electron/issues/2288
|
||||
function isElectron () {
|
||||
if (process?.versions?.electron) {
|
||||
return true;
|
||||
} else if ((window?.process as unknown as (Record<string, string> | undefined))?.type === 'renderer') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return navigator?.userAgent?.indexOf('Electron') >= 0;
|
||||
}
|
||||
|
||||
export function getEnvironment (): Environment {
|
||||
if (isElectron()) {
|
||||
return 'app';
|
||||
}
|
||||
|
||||
return 'web';
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { Codec } from '@pezkuwi/types/types';
|
||||
|
||||
type AtQuery <I extends unknown[]> = (hash: string | Uint8Array, ...params: I) => Promise<Codec>;
|
||||
|
||||
export async function getHistoric <T extends Codec, I extends unknown[] = unknown[]> (atQuery: AtQuery<I>, params: I, hashes: Hash[]): Promise<[Hash, T][]> {
|
||||
return Promise
|
||||
.all(hashes.map((hash): Promise<T> => atQuery(hash, ...params) as Promise<T>))
|
||||
.then((results): [Hash, T][] =>
|
||||
results.map((value, index): [Hash, T] => [hashes[index], value])
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { getEnvironment } from './getEnvironment.js';
|
||||
export { getHistoric } from './historic.js';
|
||||
export { intervalObservable } from './intervalObservable.js';
|
||||
export { isEqual } from './isEqual.js';
|
||||
export { triggerChange } from './triggerChange.js';
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import type { CallState } from '../types.js';
|
||||
|
||||
import { interval } from 'rxjs';
|
||||
|
||||
const interval$ = interval(500);
|
||||
|
||||
export function intervalObservable<Props, State extends CallState> (that: React.Component<Props, State>): Subscription {
|
||||
return interval$.subscribe((): void => {
|
||||
const elapsed = Date.now() - (that.state.callUpdatedAt || 0);
|
||||
const callUpdated = elapsed <= 1500;
|
||||
|
||||
if (callUpdated !== that.state.callUpdated) {
|
||||
that.setState({
|
||||
callUpdated
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
function flatten (_key: string | null, value?: unknown): unknown {
|
||||
return !value
|
||||
? value
|
||||
: (value as Record<string, unknown>).$$typeof
|
||||
? ''
|
||||
: Array.isArray(value)
|
||||
? value.map((item) => flatten(null, item))
|
||||
: value;
|
||||
}
|
||||
|
||||
export function isEqual <T> (a?: T, b?: T): boolean {
|
||||
return JSON.stringify({ test: a }, flatten) === JSON.stringify({ test: b }, flatten);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2017-2026 @pezkuwi/react-api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { OnChangeCb } from '../types.js';
|
||||
|
||||
import { isFunction, isObservable } from '@pezkuwi/util';
|
||||
|
||||
export function triggerChange (value?: unknown, ...callOnResult: (OnChangeCb | undefined)[]): void {
|
||||
if (!callOnResult?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
callOnResult.forEach((callOnResult): void => {
|
||||
if (isObservable(callOnResult)) {
|
||||
callOnResult.next(value);
|
||||
} else if (isFunction(callOnResult)) {
|
||||
callOnResult(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user