Skip to content

Commit

Permalink
Integrate substrate connect with Apps (#5644)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
wirednkod and jacogr committed Jul 12, 2021
1 parent 23d65bc commit e202dda
Show file tree
Hide file tree
Showing 19 changed files with 172 additions and 41 deletions.
3 changes: 2 additions & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)']
};
28 changes: 26 additions & 2 deletions packages/apps-config/src/endpoints/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = '';
Expand Down
4 changes: 3 additions & 1 deletion packages/apps-config/src/endpoints/productionRelayKusama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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: [
Expand Down
4 changes: 3 additions & 1 deletion packages/apps-config/src/endpoints/testingRelayWestend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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
},
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-config/src/endpoints/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import type { Option } from '../settings/types';

import { Endpoint } from '@polkadot/ui-settings/types';

export interface EndpointOption {
dnslink?: string;
genesisHash?: string;
Expand All @@ -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;
Expand All @@ -27,6 +29,7 @@ export interface LinkOption extends Option {
homepage?: string;
isChild?: boolean;
isDevelopment?: boolean;
isLightClient?: boolean;
isRelay?: boolean;
isUnreachable?: boolean;
isSpaced?: boolean;
Expand Down
14 changes: 12 additions & 2 deletions packages/apps-config/src/endpoints/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 };
}
13 changes: 13 additions & 0 deletions packages/apps-config/src/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/apps/public/locales/en/apps-config.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 5 additions & 1 deletion packages/apps/src/Endpoints/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
);

Expand Down
36 changes: 30 additions & 6 deletions packages/apps/src/Endpoints/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
/>
Expand All @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/apps/src/Endpoints/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from 'react';
export interface Network {
icon?: string;
isChild?: boolean;
isLightClient?: boolean;
isUnreachable?: boolean;
name: string;
providers: {
Expand Down
2 changes: 1 addition & 1 deletion packages/apps/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
38 changes: 28 additions & 10 deletions packages/apps/src/initSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
Expand All @@ -35,24 +52,25 @@ function getApiUrl (): string {
const option = endpoints.find(({ dnslink }) => dnslink === ipnsChain);

if (option) {
return option.value;
return { param: option.value, type: 'json-rpc' };
}
}

const stored = store.get('settings') as Record<string, unknown> || {};
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);
4 changes: 2 additions & 2 deletions packages/apps/src/overlays/Connecting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:');

Expand Down

0 comments on commit e202dda

Please sign in to comment.