Integrate substrate connect with Apps (#5644)

* integrate apps with smoldot init

* Add a new category for substrate connect light clients to be grouped under. Add translations; Alter Api.tsx in order to choose between light clients and/or url addresses

* remove supportedLightClient array that was introduced. SubstrateConnect already has the information included and will return error if no supported network is found

* rollback 'hacky' changes in order to proceed with better implementation

* alter configuration of testing and production networks in order to introduce light client type

* upgrade substrate connect to 0.3.7

* implementation for integrating substrate-connect

* merge from latest master

* Add substrate/connect version to 0.3.10

* fix merge conflicts

* introduce function for correctly creating the provider URLs and update URLs

* Fix mistaken typos

* fix linter issue about rpc-provider@npm:^4.10.1 missing

* move substrate-connect for react-api to dependencies

* Fix lint issues

* Add tests for substrate-connect light clients urls and fix for ws

* Update packages/react-api/src/Api.tsx

Co-authored-by: Jaco <jacogr@gmail.com>

* Remove duplicated code

* add exceptions in jest config (transformIgnorePatterns)

* minor fixes

* fix typo errors

* minor yarn update

* Alter urls to string | union type (revert urls for json-rpc)

* fix erroneously changed url

* fix yarn.lock

* Stub mock for tests of substrate connect

* fix yarn

* Update yarn.lock

* Update yarn.lock

* Update packages/apps/src/Endpoints/index.tsx

Co-authored-by: Jaco <jacogr@gmail.com>

* Update packages/apps-config/src/endpoints/util.ts

Co-authored-by: Jaco <jacogr@gmail.com>

* Fix Pr comments

* Fix function that was recreated in every rerender

* Update packages/apps/src/Endpoints/index.tsx

Co-authored-by: Jaco <jacogr@gmail.com>

* Fix 2nd bundle of PR comments

* Fix function on PR comment

* filter light clients out of auto-selected sidebar networks

* Update package.json

Co-authored-by: Jaco <jacogr@gmail.com>

* Update packages/react-api/package.json

Co-authored-by: Jaco <jacogr@gmail.com>

* run install

* update yarn.lock from master

* sync yarn.lock from master

* Alter implementation to avoid white screen and make sure that API returned from substrate-connect works

* Update packages/page-accounts/src/CreateAccount.slow.spec.tsx

* Update packages/page-bounties/src/Bounties.slow.spec.tsx

Co-authored-by: Jaco <jacogr@gmail.com>
This commit is contained in:
Nikos Kontakis 2021-07-12 07:51:21 +03:00 committed by GitHub
parent 23d65bc9b6
commit e202dda584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 172 additions and 41 deletions

View File

@ -22,8 +22,9 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
'\\.(md)$': '<rootDir>/jest/mocks/empty.js'
},
modulePathIgnorePatterns: ['<rootDir>/packages/apps-config/build'],
setupFilesAfterEnv: ['<rootDir>/jest/jest-setup.ts'],
testEnvironment: 'jsdom',
testTimeout: 90000,
transformIgnorePatterns: ['/node_modules/(?!@polkadot|@babel/runtime/helpers/esm/)']
transformIgnorePatterns: ['/node_modules/(?!@polkadot|@babel/runtime/helpers/esm/|@substrate|smoldot)']
};

View File

@ -10,14 +10,20 @@ interface Endpoint {
ws: string;
}
interface LightClientEndpoint {
name: string;
param: string;
}
const allEndpoints = createWsEndpoints((k: string, v?: string) => v || k);
describe('urls are all valid', (): void => {
describe('WS urls are all valid', (): void => {
allEndpoints
.filter(({ value }) =>
value &&
isString(value) &&
!value.includes('127.0.0.1')
!value.includes('127.0.0.1') &&
!value.includes('substrate-connect')
)
.map(({ text, value }): Endpoint => ({
name: text as string,
@ -30,6 +36,24 @@ describe('urls are all valid', (): void => {
);
});
describe('light client urls are all valid', (): void => {
allEndpoints
.filter(({ value }) =>
value &&
isString(value) &&
value.includes('substrate-connect')
)
.map(({ text, value }): LightClientEndpoint => ({
name: text as string,
param: value
}))
.forEach(({ name, param }) =>
it(`${name} @ ${param}`, (): void => {
assert(param.substr(param.indexOf('-')) === '-substrate-connect', `${name} @ ${param} should end with '-substrate-connect'`);
})
);
});
describe('urls are sorted', (): void => {
let hasDevelopment = false;
let lastHeader = '';

View File

@ -5,6 +5,7 @@ import type { TFunction } from 'i18next';
import type { EndpointOption } from './types';
import { KUSAMA_GENESIS } from '../api/constants';
import { createProviderUrl } from './util';
/* eslint-disable sort-keys */
@ -22,7 +23,8 @@ export function createKusama (t: TFunction): EndpointOption {
providers: {
Parity: 'wss://kusama-rpc.polkadot.io',
OnFinality: 'wss://kusama.api.onfinality.io/public-ws',
'Patract Elara': 'wss://kusama.elara.patract.io'
'Patract Elara': 'wss://kusama.elara.patract.io',
'light client': createProviderUrl('kusama-substrate-connect', 'substrate-connect')
// Pinknode: 'wss://rpc.pinknode.io/kusama/explorer' // https://github.com/polkadot-js/apps/issues/5721
},
teleport: [1000],

View File

@ -5,6 +5,7 @@ import type { TFunction } from 'i18next';
import type { EndpointOption } from './types';
import { POLKADOT_GENESIS } from '../api/constants';
import { createProviderUrl } from './util';
/* eslint-disable sort-keys */
@ -22,7 +23,8 @@ export function createPolkadot (t: TFunction): EndpointOption {
providers: {
Parity: 'wss://rpc.polkadot.io',
OnFinality: 'wss://polkadot.api.onfinality.io/public-ws',
'Patract Elara': 'wss://polkadot.elara.patract.io'
'Patract Elara': 'wss://polkadot.elara.patract.io',
'light client': createProviderUrl('polkadot-substrate-connect', 'substrate-connect')
// Pinknode: 'wss://rpc.pinknode.io/polkadot/explorer' // https://github.com/polkadot-js/apps/issues/5721
},
linked: [

View File

@ -5,6 +5,7 @@ import type { TFunction } from 'i18next';
import type { EndpointOption } from './types';
import { WESTEND_GENESIS } from '../api/constants';
import { createProviderUrl } from './util';
/* eslint-disable sort-keys */
@ -24,7 +25,8 @@ export function createWestend (t: TFunction): EndpointOption {
providers: {
Parity: 'wss://westend-rpc.polkadot.io',
'Patract Elara': 'wss://westend.elara.patract.io',
OnFinality: 'wss://westend.api.onfinality.io/public-ws'
OnFinality: 'wss://westend.api.onfinality.io/public-ws',
'light client': createProviderUrl('westend-substrate-connect', 'substrate-connect')
// 'NodeFactory(Vedran)': 'wss://westend.vedran.nodefactory.io/ws', // https://github.com/polkadot-js/apps/issues/5580
// Pinknode: 'wss://rpc.pinknode.io/westend/explorer' // https://github.com/polkadot-js/apps/issues/5721
},

View File

@ -3,6 +3,8 @@
import type { Option } from '../settings/types';
import { Endpoint } from '@polkadot/ui-settings/types';
export interface EndpointOption {
dnslink?: string;
genesisHash?: string;
@ -14,7 +16,7 @@ export interface EndpointOption {
linked?: EndpointOption[];
info?: string;
paraId?: number;
providers: Record<string, string>;
providers: Record<string, string | Endpoint>;
summary?: string;
teleport?: number[];
text: React.ReactNode;
@ -27,6 +29,7 @@ export interface LinkOption extends Option {
homepage?: string;
isChild?: boolean;
isDevelopment?: boolean;
isLightClient?: boolean;
isRelay?: boolean;
isUnreachable?: boolean;
isSpaced?: boolean;

View File

@ -4,6 +4,9 @@
import type { TFunction } from 'i18next';
import type { EndpointOption, LinkOption } from './types';
import { Endpoint, EndpointType } from '@polkadot/ui-settings/types';
import { isString } from '@polkadot/util';
interface SortOption {
isUnreachable?: boolean;
}
@ -52,9 +55,12 @@ export function expandEndpoint (t: TFunction, { dnslink, genesisHash, homepage,
.map(([host, value], index): LinkOption => ({
...base,
dnslink: index === 0 ? dnslink : undefined,
isLightClient: isString(value) ? false : value.type === 'substrate-connect',
isRelay: false,
textBy: t('rpc.hosted.by', 'via {{host}}', { ns: 'apps-config', replace: { host } }),
value
textBy: isString(value)
? t('rpc.hosted.by', 'hosted by {{host}}', { ns: 'apps-config', replace: { host } })
: t('lightclient.experimental', 'light client (experimental)', { ns: 'apps-config' }),
value: isString(value) ? value : value.param
}));
if (linked) {
@ -78,3 +84,7 @@ export function expandEndpoint (t: TFunction, { dnslink, genesisHash, homepage,
export function expandEndpoints (t: TFunction, input: EndpointOption[], firstOnly?: boolean): LinkOption[] {
return input.sort(sortLinks).reduce((result: LinkOption[], input) => result.concat(expandEndpoint(t, input, firstOnly)), []);
}
export function createProviderUrl (param: string, type: EndpointType): Endpoint {
return { param, type };
}

View File

@ -8,3 +8,16 @@ export interface Option {
text: React.ReactNode;
value: string | number;
}
export interface LinkOption extends Option {
dnslink?: string;
genesisHash?: string;
genesisHashRelay?: string;
isChild?: boolean;
isDevelopment?: boolean;
isSpaced?: boolean;
isLightClient?: boolean;
linked?: LinkOption[];
paraId?: number;
textBy: string;
}

View File

@ -1,4 +1,5 @@
{
"lightclient.experimental": "light client (experimental)",
"lng.detect": "Default browser language (auto-detect)",
"rpc.KlugDossier": "Klug Dossier",
"rpc.dev.custom": "Custom environment",

View File

@ -25,7 +25,11 @@ function NetworkDisplay ({ apiUrl, className = '', setApiUrl, value: { icon, isC
);
const _selectUrl = useCallback(
() => setApiUrl(name, providers[Math.floor(Math.random() * providers.length)].url),
() => {
const filteredProviders = providers.filter((p) => !p.name.startsWith('light client'));
return setApiUrl(name, filteredProviders[Math.floor(Math.random() * filteredProviders.length)].url);
},
[name, providers, setApiUrl]
);

View File

@ -14,6 +14,7 @@ import styled from 'styled-components';
import { createWsEndpoints, CUSTOM_ENDPOINT_KEY } from '@polkadot/apps-config';
import { Button, Input, Sidebar } from '@polkadot/react-components';
import { settings } from '@polkadot/ui-settings';
import { Endpoint, EndpointType } from '@polkadot/ui-settings/types';
import { isAscii } from '@polkadot/util';
import { useTranslation } from '../translate';
@ -43,13 +44,21 @@ function isValidUrl (url: string): boolean {
);
}
function getApiType (param: string): Endpoint {
if (param.includes('-substrate-connect')) {
return { param, type: 'substrate-connect' };
}
return { param, type: 'json-rpc' };
}
function combineEndpoints (endpoints: LinkOption[]): Group[] {
return endpoints.reduce((result: Group[], e): Group[] => {
if (e.isHeader) {
result.push({ header: e.text, isDevelopment: e.isDevelopment, isSpaced: e.isSpaced, networks: [] });
} else {
const prev = result[result.length - 1];
const prov = { name: e.textBy, url: e.value };
const prov = { isLightClient: e.isLightClient, name: e.textBy, url: e.value };
if (prev.networks[prev.networks.length - 1] && e.text === prev.networks[prev.networks.length - 1].name) {
prev.networks[prev.networks.length - 1].providers.push(prov);
@ -118,6 +127,18 @@ function loadAffinities (groups: Group[]): Record<string, string> {
}), {});
}
function isSwitchDisabled (hasUrlChanged: boolean, apiType: EndpointType, isUrlValid: boolean): boolean {
if (!hasUrlChanged) {
return true;
} else if (apiType === 'substrate-connect') {
return false;
} else if (isUrlValid) {
return false;
}
return true;
}
function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const linkOptions = createWsEndpoints(t);
@ -216,21 +237,24 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
const _onApply = useCallback(
(): void => {
settings.set({ ...(settings.get()), apiUrl });
window.location.assign(`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(apiUrl)}${window.location.hash}`);
window.location.assign(`${window.location.origin}${window.location.pathname}${getApiType(apiUrl).type === 'substrate-connect' ? '?sc=' : '?rpc='}${encodeURIComponent(apiUrl)}${window.location.hash}`);
// window.location.reload();
onClose();
},
[apiUrl, onClose]
);
const canSwitch = useMemo(
() => isSwitchDisabled(hasUrlChanged, getApiType(apiUrl).type, isUrlValid),
[hasUrlChanged, apiUrl, isUrlValid]
);
return (
<Sidebar
button={
<Button
icon='sync'
isDisabled={!(hasUrlChanged && isUrlValid)}
isDisabled={canSwitch}
label={t<string>('Switch')}
onClick={_onApply}
/>
@ -251,7 +275,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
setGroup={_changeGroup}
value={group}
>
{group.isDevelopment && (
{group.isDevelopment && getApiType(apiUrl).type === 'json-rpc' && (
<div className='endpointCustomWrapper'>
<Input
className='endpointCustom'

View File

@ -6,6 +6,7 @@ import React from 'react';
export interface Network {
icon?: string;
isChild?: boolean;
isLightClient?: boolean;
isUnreachable?: boolean;
name: string;
providers: {

View File

@ -44,8 +44,8 @@ function Root ({ store }: Props): React.ReactElement<Props> {
<ThemeProvider theme={theme}>
<Queue>
<Api
apiType={settings.apiType}
store={store}
url={settings.apiUrl}
>
<BlockAuthors>
<Events>

View File

@ -7,9 +7,18 @@ import store from 'store';
import { createWsEndpoints } from '@polkadot/apps-config';
import { extractIpfsDetails } from '@polkadot/react-hooks/useIpfs';
import { settings } from '@polkadot/ui-settings';
import { Endpoint } from '@polkadot/ui-settings/types';
import { assert } from '@polkadot/util';
function getApiUrl (): string {
function networkOrUrl (apiType: Endpoint): void {
if (apiType.type === 'json-rpc') {
console.log('WS endpoint=', apiType.param);
} else if (apiType.type === 'substrate-connect') {
console.log('Chain of light client is =', apiType.param);
}
}
function getApiType (): Endpoint {
// we split here so that both these forms are allowed
// - http://localhost:3000/?rpc=wss://substrate-rpc.parity.io/#/explorer
// - http://localhost:3000/#/explorer?rpc=wss://substrate-rpc.parity.io
@ -24,7 +33,15 @@ function getApiUrl (): string {
assert(url.startsWith('ws://') || url.startsWith('wss://'), 'Non-prefixed ws/wss url');
return url;
return { param: url, type: 'json-rpc' };
} else if (urlOptions.sc) {
assert(!Array.isArray(urlOptions.sc), 'Invalid network specified');
// https://polkadot.js.org/apps/?sc=kusama#/explorer;
const network = decodeURIComponent(urlOptions.sc.split('#')[0]);
const chain = network.split('-')[0];
return { param: chain, type: 'substrate-connect' };
}
const endpoints = createWsEndpoints(<T = string>(): T => ('' as unknown as T));
@ -35,7 +52,7 @@ function getApiUrl (): string {
const option = endpoints.find(({ dnslink }) => dnslink === ipnsChain);
if (option) {
return option.value;
return { param: option.value, type: 'json-rpc' };
}
}
@ -43,16 +60,17 @@ function getApiUrl (): string {
const fallbackUrl = endpoints.find(({ value }) => !!value);
// via settings, or the default chain
return [stored.apiUrl, process.env.WS_URL].includes(settings.apiUrl)
? settings.apiUrl // keep as-is
return [stored.apiType, process.env.WS_URL].includes(settings.apiType)
? settings.apiType // keep as-is
: fallbackUrl
? fallbackUrl.value // grab the fallback
: 'ws://127.0.0.1:9944'; // nothing found, go local
? { param: fallbackUrl.value, type: 'json-rpc' } // grab the fallback
: { param: 'ws://127.0.0.1:9944', type: 'json-rpc' }; // nothing found, go local
}
const apiUrl = getApiUrl();
// There cannot be a Substrate Connect light client default (expect only jrpc EndpointType)
const apiType = getApiType();
// set the default as retrieved here
settings.set({ apiUrl });
settings.set({ apiType });
console.log('WS endpoint=', apiUrl);
networkOrUrl(apiType);

View File

@ -9,8 +9,8 @@ import { settings } from '@polkadot/ui-settings';
import { useTranslation } from '../translate';
import BaseOverlay from './Base';
const wsUrl = settings.apiUrl;
const isWs = typeof wsUrl === 'string' && wsUrl.startsWith('ws://');
const wsUrl = settings.apiType.param;
const isWs = settings.apiType.type === 'json-rpc' && typeof wsUrl === 'string' && wsUrl.startsWith('ws://');
const isWsLocal = typeof wsUrl === 'string' && wsUrl.includes('127.0.0.1');
const isHttps = window.location.protocol.startsWith('https:');

View File

@ -14,15 +14,19 @@ import { Api } from '@polkadot/react-api';
import { MemoryStore } from '@polkadot/test-support/keyring';
import { WaitForApi } from '@polkadot/test-support/react';
import { SUBSTRATE_PORT } from '@polkadot/test-support/substrate';
import { Endpoint } from '@polkadot/ui-settings/types';
const renderAccounts = () => {
const memoryStore = new MemoryStore();
const apiType: Endpoint = { param: `ws://127.0.0.1:${SUBSTRATE_PORT}`, type: 'json-rpc' };
return render(
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<Api store={memoryStore}
url={`ws://127.0.0.1:${SUBSTRATE_PORT}`}>
<Api
apiType={apiType}
store={memoryStore}
>
<WaitForApi>
<div>
<AccountsApp basePath='/accounts'

View File

@ -17,18 +17,23 @@ import { MemoryStore } from '@polkadot/test-support/keyring';
import { aliceSigner } from '@polkadot/test-support/keyring/signers';
import { WaitForApi } from '@polkadot/test-support/react';
import { execute } from '@polkadot/test-support/transaction/execute';
import { Endpoint } from '@polkadot/ui-settings/types';
const SUBSTRATE_PORT = Number.parseInt(process.env.TEST_SUBSTRATE_PORT || '30333');
const renderBounties = () => {
const memoryStore = new MemoryStore();
const apiType: Endpoint = { param: `ws://127.0.0.1:${SUBSTRATE_PORT}`, type: 'json-rpc' };
return render(
<Suspense fallback='...'>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<Api store={memoryStore}
url={`ws://127.0.0.1:${SUBSTRATE_PORT}`}>
<Api
apiType={apiType}
store={memoryStore}
>
<WaitForApi>
<div>
<BountiesApp basePath='/bounties' />

View File

@ -0,0 +1,4 @@
// Copyright 2019-2020 @polkadot/extension authors & contributors
// SPDX-License-Identifier: Apache-2.0
module.exports = '';

View File

@ -7,9 +7,11 @@ import type { ChainProperties, ChainType } from '@polkadot/types/interfaces';
import type { KeyringStore } from '@polkadot/ui-keyring/types';
import type { ApiProps, ApiState } from './types';
import { Detector } from '@substrate/connect';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import store from 'store';
import { WsProvider } from '@polkadot/api';
import { ApiPromise } from '@polkadot/api/promise';
import { deriveMapCache, setDeriveCache } from '@polkadot/api-derive/util';
import { ethereumChains, typesBundle, typesChain } from '@polkadot/apps-config';
@ -17,9 +19,10 @@ import { web3Accounts, web3Enable } from '@polkadot/extension-dapp';
import { TokenUnit } from '@polkadot/react-components/InputNumber';
import { StatusContext } from '@polkadot/react-components/Status';
import ApiSigner from '@polkadot/react-signer/signers/ApiSigner';
import { WsProvider } from '@polkadot/rpc-provider';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { keyring } from '@polkadot/ui-keyring';
import { settings } from '@polkadot/ui-settings';
import { Endpoint } from '@polkadot/ui-settings/types';
import { formatBalance, isTestChain } from '@polkadot/util';
import { defaults as addressDefaults } from '@polkadot/util-crypto/address/defaults';
@ -29,7 +32,7 @@ import { decodeUrlTypes } from './urlTypes';
interface Props {
children: React.ReactNode;
url?: string;
apiType: Endpoint;
store?: KeyringStore;
}
@ -177,7 +180,7 @@ async function loadOnReady (api: ApiPromise, injectedPromise: Promise<InjectedEx
};
}
function Api ({ children, store, url }: Props): React.ReactElement<Props> | null {
function Api ({ apiType, children, store }: 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);
@ -186,13 +189,24 @@ function Api ({ children, store, url }: Props): React.ReactElement<Props> | null
const [extensions, setExtensions] = useState<InjectedExtension[] | undefined>();
const value = useMemo<ApiProps>(
() => ({ ...state, api, apiError, apiUrl: url, extensions, isApiConnected, isApiInitialized, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, state, url]
() => ({ ...state, api, apiError, apiUrl: apiType.param, extensions, isApiConnected, isApiInitialized, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, state, apiType]
);
// initial initialization
useEffect((): void => {
const provider = new WsProvider(url);
let provider;
if (apiType.type === 'substrate-connect') {
const detect: Detector = new Detector('PolkadotJS apps');
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
provider = detect.provider(apiType.param) as ProviderInterface;
provider.connect().catch(console.error);
} else if (apiType.type === 'json-rpc') {
provider = new WsProvider(apiType.param);
}
const signer = new ApiSigner(registry, queuePayload, queueSetTxStatus);
const types = getDevTypes();
@ -218,8 +232,7 @@ function Api ({ children, store, url }: Props): React.ReactElement<Props> | null
});
setIsApiInitialized(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [apiType, queuePayload, queueSetTxStatus, store]);
if (!value.isApiInitialized) {
return null;