ghost-portal/packages/react-api/src/Api.tsx
2020-09-17 16:46:46 +02:00

219 lines
7.5 KiB
TypeScript

// Copyright 2017-2020 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { InjectedExtension } from '@polkadot/extension-inject/types';
import { ChainProperties, ChainType } from '@polkadot/types/interfaces';
import { ApiProps, ApiState } from './types';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import store from 'store';
import ApiPromise from '@polkadot/api/promise';
import { setDeriveCache, deriveMapCache } from '@polkadot/api-derive/util';
import { typesChain, typesSpec } from '@polkadot/apps-config/api';
import { POLKADOT_DENOM_BLOCK, POLKADOT_GENESIS } from '@polkadot/apps-config/api/constants';
import { web3Accounts, web3Enable } from '@polkadot/extension-dapp';
import { WsProvider } from '@polkadot/rpc-provider';
import { StatusContext } from '@polkadot/react-components/Status';
import { TokenUnit } from '@polkadot/react-components/InputNumber';
import keyring from '@polkadot/ui-keyring';
import { KeyringStore } from '@polkadot/ui-keyring/types';
import uiSettings from '@polkadot/ui-settings';
import ApiSigner from '@polkadot/react-signer/signers/ApiSigner';
import { formatBalance, isTestChain } from '@polkadot/util';
import { setSS58Format } from '@polkadot/util-crypto';
import addressDefaults from '@polkadot/util-crypto/address/defaults';
import ApiContext from './ApiContext';
import registry from './typeRegistry';
interface Props {
children: React.ReactNode;
url?: string;
store?: KeyringStore;
}
interface InjectedAccountExt {
address: string;
meta: {
name: string;
source: string;
whenCreated: number;
};
}
interface ChainData {
injectedAccounts: InjectedAccountExt[];
properties: ChainProperties;
systemChain: string;
systemChainType: ChainType;
systemName: string;
systemVersion: string;
}
const DEFAULT_DECIMALS = registry.createType('u32', 15);
const DEFAULT_SS58 = registry.createType('u32', addressDefaults.prefix);
const injectedPromise = web3Enable('polkadot-js/apps');
let api: ApiPromise;
export { api };
function getDevTypes (): Record<string, Record<string, string>> {
const types = 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 retrieve (api: ApiPromise): Promise<ChainData> {
const [bestHeader, chainProperties, systemChain, systemChainType, systemName, systemVersion, injectedAccounts] = await Promise.all([
api.rpc.chain.getHeader(),
api.rpc.system.properties(),
api.rpc.system.chain(),
api.rpc.system.chainType
? api.rpc.system.chainType()
: Promise.resolve(registry.createType('ChainType', 'Live')),
api.rpc.system.name(),
api.rpc.system.version(),
injectedPromise
.then(() => web3Accounts())
.then((accounts) => accounts.map(({ address, meta }, whenCreated): InjectedAccountExt => ({
address,
meta: {
...meta,
name: `${meta.name || 'unknown'} (${meta.source === 'polkadot-js' ? 'extension' : meta.source})`,
whenCreated
}
})))
.catch((error): InjectedAccountExt[] => {
console.error('web3Enable', error);
return [];
})
]);
// HACK Horrible hack to try and give some window to the DOT denomination
const properties = api.genesisHash.eq(POLKADOT_GENESIS)
? bestHeader.number.toBn().gte(POLKADOT_DENOM_BLOCK)
? registry.createType('ChainProperties', { ...chainProperties, tokenDecimals: 10, tokenSymbol: 'DOT' })
: registry.createType('ChainProperties', { ...chainProperties, tokenDecimals: 12, tokenSymbol: 'DOT (old)' })
: chainProperties;
return {
injectedAccounts,
properties,
systemChain: (systemChain || '<unknown>').toString(),
systemChainType,
systemName: systemName.toString(),
systemVersion: systemVersion.toString()
};
}
async function loadOnReady (api: ApiPromise, store: KeyringStore | undefined, types: Record<string, Record<string, string>>): Promise<ApiState> {
registry.register(types);
const { injectedAccounts, properties, systemChain, systemChainType, systemName, systemVersion } = await retrieve(api);
const ss58Format = uiSettings.prefix === -1
? properties.ss58Format.unwrapOr(DEFAULT_SS58).toNumber()
: uiSettings.prefix;
const tokenSymbol = properties.tokenSymbol.unwrapOr(undefined)?.toString();
const tokenDecimals = properties.tokenDecimals.unwrapOr(DEFAULT_DECIMALS).toNumber();
const isDevelopment = systemChainType.isDevelopment || systemChainType.isLocal || isTestChain(systemChain);
console.log(`chain: ${systemChain} (${systemChainType.toString()}), ${JSON.stringify(properties)}`);
// explicitly override the ss58Format as specified
registry.setChainProperties(registry.createType('ChainProperties', { ...properties, ss58Format }));
// FIXME This should be removed (however we have some hanging bits, e.g. vanity)
setSS58Format(ss58Format);
// first setup the UI helpers
formatBalance.setDefaults({
decimals: tokenDecimals,
unit: tokenSymbol
});
TokenUnit.setAbbr(tokenSymbol);
// finally load the keyring
keyring.loadAll({
genesisHash: api.genesisHash,
isDevelopment,
ss58Format,
store,
type: 'ed25519'
}, 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 && api.tx.system.setCode) || apiDefaultTx;
const isSubstrateV2 = !!Object.keys(api.consts).length;
setDeriveCache(api.genesisHash.toHex(), deriveMapCache);
return {
apiDefaultTx,
apiDefaultTxSudo,
hasInjectedAccounts: injectedAccounts.length !== 0,
isApiReady: true,
isDevelopment,
isSubstrateV2,
systemChain,
systemName,
systemVersion
};
}
function Api ({ children, store, url }: Props): React.ReactElement<Props> | null {
const { queuePayload, queueSetTxStatus } = useContext(StatusContext);
const [state, setState] = useState<ApiState>({ hasInjectedAccounts: false, isApiReady: false } as unknown as ApiState);
const [isApiConnected, setIsApiConnected] = useState(false);
const [isApiInitialized, setIsApiInitialized] = useState(false);
const [extensions, setExtensions] = useState<InjectedExtension[] | undefined>();
const value = useMemo<ApiProps>(
() => ({ ...state, api, extensions, isApiConnected, isApiInitialized, isWaitingInjected: !extensions }),
[extensions, isApiConnected, isApiInitialized, state]
);
// initial initialization
useEffect((): void => {
const provider = new WsProvider(url);
const signer = new ApiSigner(queuePayload, queueSetTxStatus);
const types = getDevTypes();
api = new ApiPromise({ provider, registry, signer, types, typesChain, typesSpec });
api.on('connected', () => setIsApiConnected(true));
api.on('disconnected', () => setIsApiConnected(false));
api.on('ready', async (): Promise<void> => {
try {
setState(await loadOnReady(api, store, types));
} catch (error) {
console.error('Unable to load chain', error);
}
});
injectedPromise
.then(setExtensions)
.catch((error: Error) => console.error(error));
setIsApiInitialized(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!value.isApiInitialized) {
return null;
}
return (
<ApiContext.Provider value={value}>
{children}
</ApiContext.Provider>
);
}
export default React.memo(Api);