From fa0d0f870606e9e8f44356a5790c60011bcc8f7d Mon Sep 17 00:00:00 2001 From: wirednkod Date: Fri, 23 Jul 2021 22:11:34 +0300 Subject: [PATCH 01/16] remove the eslint disablings --- .../ExtensionProvider/ExtensionProvider.ts | 8 +++++++- .../src/background/ConnectionManager.ts | 13 +++++------- projects/extension/src/background/index.ts | 20 +++++++++++++------ projects/extension/src/content/index.ts | 2 -- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 4194e35fc..54ab7f8e4 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -61,10 +61,16 @@ export class ExtensionProvider implements ProviderInterface { #appName: string; #chainName: string; + // TODO: NIK CHAINSPECS FOR EXTENSION + #chainSpecs: string | undefined; - public constructor(appName: string, chainName: string) { + + // TODO: NIK CHAINSPECS FOR EXTENSION + public constructor(appName: string, chainName: string, chainSpecs?: string) { this.#appName = appName; this.#chainName = chainName; + // TODO: NIK CHAINSPECS FOR EXTENSION + this.#chainSpecs = chainSpecs; } /** diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index eba9a0ab3..1ef4f5248 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -1,16 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import * as smoldot from 'smoldot'; import { AppMediator } from './AppMediator'; import { JsonRpcResponse, JsonRpcRequest, ConnectionManagerInterface } from './types'; import EventEmitter from 'eventemitter3'; import { StateEmitter, State } from './types'; import { Network } from '../types'; -import { logger } from '@polkadot/util'; +import { assert, logger } from '@polkadot/util'; const l = logger('Extension Connection Manager'); @@ -30,7 +24,7 @@ export interface ChainsInterface { * smoldot clients and to clean up all an app's subscriptions when disconnected. */ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) implements ConnectionManagerInterface { - // #isConnected = false; + #isConnected = false; #client: smoldot.SmoldotClient | undefined = undefined; #chains: ChainsInterface[] = []; readonly #networks: Network[] = []; @@ -219,6 +213,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) forbidWs: true, /* suppress console warnings about insecure connections */ maxLogLevel: this.smoldotLogLevel }); + this.#isConnected = true; } catch (err) { l.error(`Error while initializing smoldot: ${err}`); } @@ -239,6 +234,8 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) chainSpec: spec, jsonRpcCallback: (message: string) => { const parsed = JSON.parse(message) as JsonRpcResponse; + // SOMETHING IS WRONG HERE CONCERNING THE ROLLBACK + console.log('parsed', parsed); for (const app of this.#apps) { app.processSmoldotMessage(parsed); } diff --git a/projects/extension/src/background/index.ts b/projects/extension/src/background/index.ts index 299f16264..edd44ffe2 100644 --- a/projects/extension/src/background/index.ts +++ b/projects/extension/src/background/index.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ConnectionManager } from './ConnectionManager'; import westend from '../../public/assets/westend.json'; import kusama from '../../public/assets/kusama.json'; @@ -23,9 +21,9 @@ export interface RequestRpcSend { const init = async () => { try { await manager.initSmoldot(); - await manager.addChain('polkadot', JSON.stringify(polkadot)).catch(err => l.error('Error', err)); - await manager.addChain('kusama', JSON.stringify(kusama)).catch(err => l.error('Error', err)); - await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); + // await manager.addChain('polkadot', JSON.stringify(polkadot)).catch(err => l.error('Error', err)); + // await manager.addChain('kusama', JSON.stringify(kusama)).catch(err => l.error('Error', err)); + // await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); } catch (e) { l.error(`Error creating smoldot: ${e}`); } @@ -39,7 +37,17 @@ chrome.runtime.onStartup.addListener(() => { init().catch(console.error); }); -chrome.runtime.onConnect.addListener((port) => { +chrome.runtime.onConnect.addListener(async (port) => { + const chainName: string = port.name.split('::')[1]; + console.log('chainName - ', chainName, port); + + // Check if the incoming chain (name) exists + // if not then create it from the chain specs. + // If there are no chainspecs then break; + // add Chain should take place inside this one + await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); + + // If the chain name/spec exists then continue and addApp manager.addApp(port); }); diff --git a/projects/extension/src/content/index.ts b/projects/extension/src/content/index.ts index a27803868..b54b9a23d 100644 --- a/projects/extension/src/content/index.ts +++ b/projects/extension/src/content/index.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ExtensionMessageRouter } from './ExtensionMessageRouter'; import { ExtensionPageInjector } from './ExtensionPageInjector'; import { debug } from '../utils/debug'; From 2501930d58f2be07325343ad78e064c26ccc3335 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Mon, 26 Jul 2021 13:02:35 +0300 Subject: [PATCH 02/16] minor changes on Detector to accept chainspecs --- packages/connect/src/Detector.ts | 35 ++++++++++++++----- .../ExtensionProvider/ExtensionProvider.ts | 1 - .../src/background/ConnectionManager.ts | 6 ++-- projects/extension/src/background/index.ts | 15 +++----- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 4ea064c5a..30ed6b89d 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -121,19 +121,36 @@ export class Detector { */ public provider = (chainName: string, chainSpec?: string): ProviderInterface => { let provider: ProviderInterface = {} as ProviderInterface; - + let spec: string | undefined = undefined; if (Object.keys(this.#chainSpecs).includes(chainName)) { - if (this.#isExtension) { - provider = new ExtensionProvider(this.#name, chainName); - } else if (!this.#isExtension) { - const chainSpec = JSON.stringify(this.#chainSpecs[chainName]); - provider = new SmoldotProvider(chainSpec); - } + spec = JSON.stringify(this.#chainSpecs[chainName]); } else if (chainSpec) { - provider = new SmoldotProvider(chainSpec); - } else if (!chainSpec) { + spec = chainSpec; + } + + if (!spec) { throw new Error(`No known Chain was detected and no chainSpec was provided. Either give a known chain name ('${Object.keys(this.#chainSpecs).join('\', \'')}') or provide valid chainSpecs.`) } + + if (this.#isExtension) { + provider = new ExtensionProvider(this.#name, chainName, spec); + } else if (!this.#isExtension) { + provider = new SmoldotProvider(spec); + } + + // LOGIC must change to provide chainName and chainSpecs + // if (Object.keys(this.#chainSpecs).includes(chainName)) { + // if (this.#isExtension) { + // provider = new ExtensionProvider(this.#name, chainName); + // } else if (!this.#isExtension) { + // const chainSpec = JSON.stringify(this.#chainSpecs[chainName]); + // provider = new SmoldotProvider(chainSpec); + // } + // } else if (chainSpec) { + // provider = new SmoldotProvider(chainSpec); + // } else if (!chainSpec) { + // throw new Error(`No known Chain was detected and no chainSpec was provided. Either give a known chain name ('${Object.keys(this.#chainSpecs).join('\', \'')}') or provide valid chainSpecs.`) + // } return provider; } diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 54ab7f8e4..d27013d0c 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -64,7 +64,6 @@ export class ExtensionProvider implements ProviderInterface { // TODO: NIK CHAINSPECS FOR EXTENSION #chainSpecs: string | undefined; - // TODO: NIK CHAINSPECS FOR EXTENSION public constructor(appName: string, chainName: string, chainSpecs?: string) { this.#appName = appName; diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index 1ef4f5248..8b0d0044b 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -4,7 +4,7 @@ import { JsonRpcResponse, JsonRpcRequest, ConnectionManagerInterface } from './t import EventEmitter from 'eventemitter3'; import { StateEmitter, State } from './types'; import { Network } from '../types'; -import { assert, logger } from '@polkadot/util'; +import { logger } from '@polkadot/util'; const l = logger('Extension Connection Manager'); @@ -24,7 +24,7 @@ export interface ChainsInterface { * smoldot clients and to clean up all an app's subscriptions when disconnected. */ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) implements ConnectionManagerInterface { - #isConnected = false; + // #isConnected = false; #client: smoldot.SmoldotClient | undefined = undefined; #chains: ChainsInterface[] = []; readonly #networks: Network[] = []; @@ -213,7 +213,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) forbidWs: true, /* suppress console warnings about insecure connections */ maxLogLevel: this.smoldotLogLevel }); - this.#isConnected = true; + // this.#isConnected = true; } catch (err) { l.error(`Error while initializing smoldot: ${err}`); } diff --git a/projects/extension/src/background/index.ts b/projects/extension/src/background/index.ts index edd44ffe2..9199438bf 100644 --- a/projects/extension/src/background/index.ts +++ b/projects/extension/src/background/index.ts @@ -21,9 +21,9 @@ export interface RequestRpcSend { const init = async () => { try { await manager.initSmoldot(); - // await manager.addChain('polkadot', JSON.stringify(polkadot)).catch(err => l.error('Error', err)); - // await manager.addChain('kusama', JSON.stringify(kusama)).catch(err => l.error('Error', err)); - // await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); + await manager.addChain('polkadot', JSON.stringify(polkadot)).catch(err => l.error('Error', err)); + await manager.addChain('kusama', JSON.stringify(kusama)).catch(err => l.error('Error', err)); + await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); } catch (e) { l.error(`Error creating smoldot: ${e}`); } @@ -39,13 +39,8 @@ chrome.runtime.onStartup.addListener(() => { chrome.runtime.onConnect.addListener(async (port) => { const chainName: string = port.name.split('::')[1]; - console.log('chainName - ', chainName, port); - - // Check if the incoming chain (name) exists - // if not then create it from the chain specs. - // If there are no chainspecs then break; - // add Chain should take place inside this one - await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); + console.log('pooort', port, chainName); + // await manager.addChain(chainName, JSON.stringify(westend)).catch(err => l.error('Error', err)); // If the chain name/spec exists then continue and addApp manager.addApp(port); From ead62c260c34a00f880e156068f45a95c874a9d6 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Wed, 28 Jul 2021 00:54:25 +0300 Subject: [PATCH 03/16] Add implementation concerning adding chain of new app end to end --- .../connect-extension-protocol/src/index.ts | 2 +- packages/connect/src/Detector.ts | 14 --------- .../ExtensionProvider/ExtensionProvider.ts | 13 ++++----- .../extension/src/background/AppMediator.ts | 29 +++++++++++++++---- .../src/background/ConnectionManager.ts | 14 ++++++--- projects/extension/src/background/index.ts | 7 +---- projects/extension/src/background/types.ts | 1 + .../src/content/ExtensionMessageRouter.ts | 3 ++ 8 files changed, 44 insertions(+), 39 deletions(-) diff --git a/packages/connect-extension-protocol/src/index.ts b/packages/connect-extension-protocol/src/index.ts index 153838b07..4edd8d957 100644 --- a/packages/connect-extension-protocol/src/index.ts +++ b/packages/connect-extension-protocol/src/index.ts @@ -71,7 +71,7 @@ export interface ProviderMessageData { /** What action the `ExtensionMessageRouter` should take **/ action: 'forward' | 'connect' | 'disconnect'; /** The message the `ExtensionMessageRouter` should forward to the background **/ - message?: MessageToManager + message?: MessageToManager; } /** diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 30ed6b89d..44f2bfa37 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -137,20 +137,6 @@ export class Detector { } else if (!this.#isExtension) { provider = new SmoldotProvider(spec); } - - // LOGIC must change to provide chainName and chainSpecs - // if (Object.keys(this.#chainSpecs).includes(chainName)) { - // if (this.#isExtension) { - // provider = new ExtensionProvider(this.#name, chainName); - // } else if (!this.#isExtension) { - // const chainSpec = JSON.stringify(this.#chainSpecs[chainName]); - // provider = new SmoldotProvider(chainSpec); - // } - // } else if (chainSpec) { - // provider = new SmoldotProvider(chainSpec); - // } else if (!chainSpec) { - // throw new Error(`No known Chain was detected and no chainSpec was provided. Either give a known chain name ('${Object.keys(this.#chainSpecs).join('\', \'')}') or provide valid chainSpecs.`) - // } return provider; } diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index d27013d0c..0994a5775 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -61,14 +61,11 @@ export class ExtensionProvider implements ProviderInterface { #appName: string; #chainName: string; - // TODO: NIK CHAINSPECS FOR EXTENSION #chainSpecs: string | undefined; - // TODO: NIK CHAINSPECS FOR EXTENSION public constructor(appName: string, chainName: string, chainSpecs?: string) { this.#appName = appName; this.#chainName = chainName; - // TODO: NIK CHAINSPECS FOR EXTENSION this.#chainSpecs = chainSpecs; } @@ -215,9 +212,13 @@ export class ExtensionProvider implements ProviderInterface { origin: EXTENSION_PROVIDER_ORIGIN } provider.send(connectMsg); + + // Once connect is sent - send rpc to extension that will contain the chainSpecs + // for the extension to call addChain on smoldot + this.send('spec', [this.#chainSpecs]); + provider.listen(({data}: ExtensionMessage) => { if (data.origin && data.origin === CONTENT_SCRIPT_ORIGIN) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access this.#handleMessage(data); } }); @@ -231,7 +232,6 @@ export class ExtensionProvider implements ProviderInterface { * Manually "disconnect" - sends a message to the `ExtensionMessageRouter` * telling it to disconnect the port with the background manager. */ - // eslint-disable-next-line @typescript-eslint/require-await public async disconnect(): Promise { const disconnectMsg: ProviderMessageData = { appName: this.#appName, @@ -281,10 +281,8 @@ export class ExtensionProvider implements ProviderInterface { */ public async send( method: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any params: any[], subscription?: SubscriptionHandler - // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { return new Promise((resolve, reject): void => { const json = this.#coder.encodeJson(method, params); @@ -381,7 +379,6 @@ export class ExtensionProvider implements ProviderInterface { return await this.send(method, [id]) as Promise; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private emit(type: ProviderInterfaceEmitted, ...args: any[]): void { this.#eventemitter.emit(type, ...args); } diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index 746959b59..dd8737245 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ import EventEmitter from 'eventemitter3'; import { MessageToManager, @@ -31,6 +30,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { readonly #url: string | undefined; readonly #manager: ConnectionManagerInterface; #chainName: string | undefined = undefined; + #chainNo: number | undefined; #state: AppState = 'connected'; /** subscriptions is all the active message subscriptions this ap[ has */ readonly subscriptions: SubscriptionMapping[]; @@ -82,13 +82,20 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } /** - * chainName is the name of the smoldot client to talk to; this is the + * chainName is the name of the chain to talk to; this is the * name of the blockchain network. */ get chainName(): string { return this.#chainName || ''; } + /** + * chainNo is the id of the chain to talk to + */ + get chainNo(): number | undefined { + return this.#chainNo; + } + /** tabId is the tabId of the app in the browser */ get tabId(): number | undefined { return this.#tabId; @@ -216,6 +223,10 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { return true; } + #addChain = async (chainName: string, chainSpecs: string): Promise => { + this.#chainNo = await this.#manager.addChain(chainName, chainSpecs); + } + #handleRpcRequest = (msg: MessageToManager): void => { if (msg.type !== 'rpc') { console.warn(`Unrecognised message type ${msg.type} received from content script`); @@ -237,10 +248,16 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { }); } - // TODO: what about unsubscriptions requested by the UApp - we need to remove - // the subscription from our subscriptions state - const chainID = this.#manager.sendRpcMessageTo(this.#chainName as string, parsed); - this.requests.push({ appID, chainID }); + const chainName = this.#chainName as string; + + if (parsed.method === 'spec' && chainName) { + this.#addChain(chainName, parsed.params[0] as string); + } else { + // TODO: what about unsubscriptions requested by the UApp - we need to remove + // the subscription from our subscriptions state + const chainID = this.#manager.sendRpcMessageTo(chainName, parsed); + this.requests.push({ appID, chainID }); + } } /** diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index 8b0d0044b..544d39be9 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -193,6 +193,11 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) if (!this.#client) { throw new Error('Tried to unregister an app to smoldot client that does not exist.'); } + console.log('app', app, this); + if (!app.chainNo) { + throw new Error('Tried to remove an app from a chain that does not exist.'); + } + console.log('!!!!', this.#networks) const idx = this.#apps.findIndex(a => a.name === app.name); this.#apps.splice(idx, 1); this.emit('stateChanged', this.getState()); @@ -225,7 +230,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) * @param name - Name of the chain * @param spec - ChainSpec of chain to be added */ - async addChain (name: string, spec: string) { + async addChain (name: string, spec: string): Promise { try { if (!this.#client) { throw new Error('Smoldot client does not exist.'); @@ -234,16 +239,16 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) chainSpec: spec, jsonRpcCallback: (message: string) => { const parsed = JSON.parse(message) as JsonRpcResponse; - // SOMETHING IS WRONG HERE CONCERNING THE ROLLBACK - console.log('parsed', parsed); for (const app of this.#apps) { app.processSmoldotMessage(parsed); } } }); + const chainNo = ++this.#chainCounter; + this.#chains.push({ - idx: ++this.#chainCounter, + idx: chainNo, name, chain: addedChain }) @@ -255,6 +260,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) chainspecPath: `${name}.json` }); + return chainNo; } catch (err) { l.error(`Error while trying to connect to chain ${name}: ${err}`); } diff --git a/projects/extension/src/background/index.ts b/projects/extension/src/background/index.ts index 9199438bf..e0d0bd55d 100644 --- a/projects/extension/src/background/index.ts +++ b/projects/extension/src/background/index.ts @@ -37,12 +37,7 @@ chrome.runtime.onStartup.addListener(() => { init().catch(console.error); }); -chrome.runtime.onConnect.addListener(async (port) => { - const chainName: string = port.name.split('::')[1]; - console.log('pooort', port, chainName); - // await manager.addChain(chainName, JSON.stringify(westend)).catch(err => l.error('Error', err)); - - // If the chain name/spec exists then continue and addApp +chrome.runtime.onConnect.addListener(port => { manager.addApp(port); }); diff --git a/projects/extension/src/background/types.ts b/projects/extension/src/background/types.ts index 021aca595..423c0845b 100644 --- a/projects/extension/src/background/types.ts +++ b/projects/extension/src/background/types.ts @@ -48,6 +48,7 @@ export interface ConnectionManagerInterface { sendRpcMessageTo: (name: string, message: JsonRpcRequest) => number; registerApp: (app: AppMediator, name: string) => void; unregisterApp: (app: AppMediator, name: string) => void; + addChain: (name: string, spec: string) => Promise; } export interface JsonRpcObject { diff --git a/projects/extension/src/content/ExtensionMessageRouter.ts b/projects/extension/src/content/ExtensionMessageRouter.ts index 7185bd924..9ed628102 100644 --- a/projects/extension/src/content/ExtensionMessageRouter.ts +++ b/projects/extension/src/content/ExtensionMessageRouter.ts @@ -90,6 +90,9 @@ export class ExtensionMessageRouter { return; } + // Call background to "remove()" the chain + port.postMessage(data.message); + port.disconnect(); debug(`DISCONNECTED ${data.chainName} PORT`, port); delete this.#ports[data.chainName]; From abc331e783110f6f98b4bff2493738d041ca4c9e Mon Sep 17 00:00:00 2001 From: wirednkod Date: Wed, 28 Jul 2021 00:55:02 +0300 Subject: [PATCH 04/16] Fix some tests --- .../connect/src/ExtensionProvider/ExtensionProvider.test.ts | 6 +++--- projects/extension/src/mocks.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts index b5df6915d..bc8f77f06 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts @@ -48,7 +48,7 @@ test('connect sends connect message and emits connected', async () => { action: 'connect', origin: 'extension-provider' }; - expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledTimes(2); const { data } = handler.mock.calls[0][0] as ProviderMessage; expect(data).toEqual(expectedMessage); expect(ep.isConnected).toBe(true); @@ -70,8 +70,8 @@ test('disconnect sends disconnect message and emits disconnected', async () => { action: 'disconnect', origin: 'extension-provider' }; - expect(handler).toHaveBeenCalledTimes(2); - const { data } = handler.mock.calls[1][0] as ProviderMessage; + expect(handler).toHaveBeenCalledTimes(3); + const { data } = handler.mock.calls[2][0] as ProviderMessage; expect(data).toEqual(expectedMessage); expect(ep.isConnected).toBe(false); expect(emitted).toHaveBeenCalledTimes(1); diff --git a/projects/extension/src/mocks.ts b/projects/extension/src/mocks.ts index 5a9b9069e..36c0cce62 100644 --- a/projects/extension/src/mocks.ts +++ b/projects/extension/src/mocks.ts @@ -60,6 +60,10 @@ export class MockConnectionManager implements ConnectionManagerInterface { this.#willFindClient = willFindClient; } + addChain (name: string, spec: string): Promise { + return Promise.resolve(1); + } + registerApp(): void { return; } From 0e6f3925fac33db19755bb157a7c42ecd1807580 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Wed, 28 Jul 2021 01:20:24 +0300 Subject: [PATCH 05/16] add code for removing app's chain from smoldot upon disconnection --- projects/extension/src/background/ConnectionManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index f7bd0ae15..893a202f5 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -169,6 +169,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) /** * unregisterApp is used after an app has finished processing any unsubscribe * messages and disconnected to fully unregister itself. + * It also retrieves the chain that app was connected to and calls smoldot for removal * * @param app - The app */ @@ -176,11 +177,11 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) if (!this.#client) { throw new Error('Tried to unregister an app to smoldot client that does not exist.'); } - console.log('app', app, this); - if (!app.chainNo) { + const client = this.#networks.find(a => a.idx === app.chainNo); + if (!app.chainNo || !client) { throw new Error('Tried to remove an app from a chain that does not exist.'); } - console.log('!!!!', this.#networks) + client?.chain?.remove(); const idx = this.#apps.findIndex(a => a.name === app.name); this.#apps.splice(idx, 1); this.emit('stateChanged', this.getState()); From 3932910bfc85ea29fa4952048c1ea9fa82c8b3ac Mon Sep 17 00:00:00 2001 From: wirednkod Date: Wed, 28 Jul 2021 01:56:41 +0300 Subject: [PATCH 06/16] remove stray code --- projects/extension/src/content/ExtensionMessageRouter.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/projects/extension/src/content/ExtensionMessageRouter.ts b/projects/extension/src/content/ExtensionMessageRouter.ts index 9ed628102..7185bd924 100644 --- a/projects/extension/src/content/ExtensionMessageRouter.ts +++ b/projects/extension/src/content/ExtensionMessageRouter.ts @@ -90,9 +90,6 @@ export class ExtensionMessageRouter { return; } - // Call background to "remove()" the chain - port.postMessage(data.message); - port.disconnect(); debug(`DISCONNECTED ${data.chainName} PORT`, port); delete this.#ports[data.chainName]; From 14fd3f5e71083550e7bfef9dc3c582d9a7e55b04 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Thu, 29 Jul 2021 00:12:40 +0300 Subject: [PATCH 07/16] remove indexing from Appmediator and related chain with it --- packages/connect/src/Detector.ts | 11 +++------ .../extension/src/background/AppMediator.ts | 23 +++++++++++++------ .../src/background/ConnectionManager.ts | 14 ++++------- projects/extension/src/background/index.ts | 14 ++++++++--- projects/extension/src/background/types.ts | 3 ++- projects/extension/src/types/index.ts | 1 - 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 44f2bfa37..1eea66021 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -121,20 +121,15 @@ export class Detector { */ public provider = (chainName: string, chainSpec?: string): ProviderInterface => { let provider: ProviderInterface = {} as ProviderInterface; - let spec: string | undefined = undefined; - if (Object.keys(this.#chainSpecs).includes(chainName)) { - spec = JSON.stringify(this.#chainSpecs[chainName]); - } else if (chainSpec) { - spec = chainSpec; - } - if (!spec) { + if (!chainSpec && !Object.keys(this.#chainSpecs).includes(chainName)) { throw new Error(`No known Chain was detected and no chainSpec was provided. Either give a known chain name ('${Object.keys(this.#chainSpecs).join('\', \'')}') or provide valid chainSpecs.`) } if (this.#isExtension) { - provider = new ExtensionProvider(this.#name, chainName, spec); + provider = new ExtensionProvider(this.#name, chainName, chainSpec); } else if (!this.#isExtension) { + const spec = JSON.stringify(this.#chainSpecs[chainName]); provider = new SmoldotProvider(spec); } return provider; diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index dd8737245..5e295905d 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -13,7 +13,8 @@ import { StateEmitter, SubscriptionMapping } from './types'; - +import { SmoldotChain } from 'smoldot'; +import { relayChains } from './index'; /** * AppMediator is the class that represents and manages an app's connection to * a blockchain network. N.B. an app that connects to multiple nblockchain @@ -30,7 +31,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { readonly #url: string | undefined; readonly #manager: ConnectionManagerInterface; #chainName: string | undefined = undefined; - #chainNo: number | undefined; + #chain: SmoldotChain | undefined; #state: AppState = 'connected'; /** subscriptions is all the active message subscriptions this ap[ has */ readonly subscriptions: SubscriptionMapping[]; @@ -90,10 +91,10 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } /** - * chainNo is the id of the chain to talk to + * returns the chain that the app is connected to */ - get chainNo(): number | undefined { - return this.#chainNo; + get chain(): SmoldotChain | undefined { + return this.#chain; } /** tabId is the tabId of the app in the browser */ @@ -224,7 +225,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } #addChain = async (chainName: string, chainSpecs: string): Promise => { - this.#chainNo = await this.#manager.addChain(chainName, chainSpecs); + this.#chain = await this.#manager.addChain(chainName, chainSpecs); } #handleRpcRequest = (msg: MessageToManager): void => { @@ -251,7 +252,15 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { const chainName = this.#chainName as string; if (parsed.method === 'spec' && chainName) { - this.#addChain(chainName, parsed.params[0] as string); + // When params[0] (chainSpecs in the current case is empty) then this is a + // known relay chain and specs should be retrieved from inside the extension + let chainSpec: string; + if (Object.keys(relayChains).includes(chainName)) { + chainSpec = relayChains[chainName]; + } else { + chainSpec = parsed.params[0] as string; + } + this.#addChain(chainName, chainSpec); } else { // TODO: what about unsubscriptions requested by the UApp - we need to remove // the subscription from our subscriptions state diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index 893a202f5..0d64c9752 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -22,7 +22,6 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) readonly #networks: Network[] = []; readonly #apps: AppMediator[] = []; smoldotLogLevel = 3; - #chainCounter = 0; #id = 0; /** registeredApps @@ -177,11 +176,11 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) if (!this.#client) { throw new Error('Tried to unregister an app to smoldot client that does not exist.'); } - const client = this.#networks.find(a => a.idx === app.chainNo); - if (!app.chainNo || !client) { + if (!app.chain) { throw new Error('Tried to remove an app from a chain that does not exist.'); } - client?.chain?.remove(); + console.log('app', app); + app.chain?.remove(); const idx = this.#apps.findIndex(a => a.name === app.name); this.#apps.splice(idx, 1); this.emit('stateChanged', this.getState()); @@ -214,7 +213,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) * @param name - Name of the chain * @param spec - ChainSpec of chain to be added */ - async addChain (name: string, spec: string): Promise { + async addChain (name: string, spec: string): Promise { try { if (!this.#client) { throw new Error('Smoldot client does not exist.'); @@ -229,10 +228,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) } }); - const chainNo = ++this.#chainCounter; - this.#networks.push({ - idx: chainNo, name: name, chain: addedChain, status: 'connected', @@ -240,7 +236,7 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) chainspecPath: `${name}.json` }); - return chainNo; + return addedChain; } catch (err) { l.error(`Error while trying to connect to chain ${name}: ${err}`); } diff --git a/projects/extension/src/background/index.ts b/projects/extension/src/background/index.ts index e0d0bd55d..743f2fb74 100644 --- a/projects/extension/src/background/index.ts +++ b/projects/extension/src/background/index.ts @@ -12,6 +12,14 @@ declare let window: Background; const manager = window.manager = new ConnectionManager(); +type RelayType = Record + +export const relayChains: RelayType = { + "polkadot": JSON.stringify(polkadot), + "kusama": JSON.stringify(kusama), + "westend": JSON.stringify(westend) +} + const l = logger('Extension'); export interface RequestRpcSend { method: string; @@ -21,9 +29,9 @@ export interface RequestRpcSend { const init = async () => { try { await manager.initSmoldot(); - await manager.addChain('polkadot', JSON.stringify(polkadot)).catch(err => l.error('Error', err)); - await manager.addChain('kusama', JSON.stringify(kusama)).catch(err => l.error('Error', err)); - await manager.addChain('westend', JSON.stringify(westend)).catch(err => l.error('Error', err)); + for(const [key, value] of Object.entries(relayChains)) { + await manager.addChain(key, value).catch(err => l.error('Error', err)); + } } catch (e) { l.error(`Error creating smoldot: ${e}`); } diff --git a/projects/extension/src/background/types.ts b/projects/extension/src/background/types.ts index 423c0845b..e465c81e2 100644 --- a/projects/extension/src/background/types.ts +++ b/projects/extension/src/background/types.ts @@ -1,3 +1,4 @@ +import * as smoldot from 'smoldot'; import { AppMediator } from './AppMediator'; import EventEmitter from 'eventemitter3'; import StrictEventEmitter from 'strict-event-emitter-types'; @@ -48,7 +49,7 @@ export interface ConnectionManagerInterface { sendRpcMessageTo: (name: string, message: JsonRpcRequest) => number; registerApp: (app: AppMediator, name: string) => void; unregisterApp: (app: AppMediator, name: string) => void; - addChain: (name: string, spec: string) => Promise; + addChain: (name: string, spec: string) => Promise; } export interface JsonRpcObject { diff --git a/projects/extension/src/types/index.ts b/projects/extension/src/types/index.ts index 4756231f5..c2d2ed712 100644 --- a/projects/extension/src/types/index.ts +++ b/projects/extension/src/types/index.ts @@ -25,7 +25,6 @@ interface ChainSpec { chainspecPath: string; } export interface Network extends ChainSpec { - idx: number; chain: smoldot.SmoldotChain | undefined; parachains?: Parachain[]; } From ad633d035297d8e4f1ee100c7da792114038b9c7 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Thu, 29 Jul 2021 00:18:06 +0300 Subject: [PATCH 08/16] add more exclusions for IDE's sanity --- packages/connect-extension-protocol/src/index.ts | 2 +- packages/connect-extension-protocol/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/connect-extension-protocol/src/index.ts b/packages/connect-extension-protocol/src/index.ts index e6ab9fc1a..3f21fa06b 100644 --- a/packages/connect-extension-protocol/src/index.ts +++ b/packages/connect-extension-protocol/src/index.ts @@ -24,7 +24,7 @@ export interface ExtensionMessageData { /** message is telling the `ExtensionProvider` the port has been closed **/ disconnect?: boolean; /** message is the message from the manager to be forwarded to the app **/ - message?: MessageFromManager + message?: MessageFromManager; } /** diff --git a/packages/connect-extension-protocol/tsconfig.json b/packages/connect-extension-protocol/tsconfig.json index 01afde1aa..09fdade21 100644 --- a/packages/connect-extension-protocol/tsconfig.json +++ b/packages/connect-extension-protocol/tsconfig.json @@ -12,5 +12,6 @@ "skipLibCheck": true, "strict": true, "module": "ES2020" - } + }, + "exclude": ["../../node_modules*", "./node_modules*"] } From 3c6359986b84c51b78ad32ba369822f6c997c740 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Thu, 29 Jul 2021 00:19:54 +0300 Subject: [PATCH 09/16] correct some test cases and remove stray log --- .../src/background/ConnectionManager.test.ts | 12 +++++------- .../extension/src/background/ConnectionManager.ts | 1 - projects/extension/src/mocks.ts | 5 +++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/projects/extension/src/background/ConnectionManager.test.ts b/projects/extension/src/background/ConnectionManager.test.ts index a72891ef2..ce5c056d2 100644 --- a/projects/extension/src/background/ConnectionManager.test.ts +++ b/projects/extension/src/background/ConnectionManager.test.ts @@ -177,22 +177,20 @@ describe('Unit tests', () => { }); test('Get networks/chains', () => { - const tmpChains: unknown[] = []; // With this look the "chain" is removed intentionally as "chain" // object cannot be compared with jest - manager.networks.forEach(n => { - tmpChains.push({ - idx: n.idx, + const tmpChains = manager.networks.map(n => ( + { name: n.name, status: n.status, chainspecPath: n.chainspecPath, isKnown: n.isKnown }) - }) + ) expect(tmpChains).toEqual([ - { idx: 1, name: 'westend', status: "connected", chainspecPath: "westend.json", isKnown: true }, - { idx: 2, name: 'kusama', status: "connected", chainspecPath: "kusama.json", isKnown: true } + { name: 'westend', status: "connected", chainspecPath: "westend.json", isKnown: true }, + { name: 'kusama', status: "connected", chainspecPath: "kusama.json", isKnown: true } ]); expect(manager.networks).toHaveLength(2); diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index 0d64c9752..014c3e1ff 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -179,7 +179,6 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) if (!app.chain) { throw new Error('Tried to remove an app from a chain that does not exist.'); } - console.log('app', app); app.chain?.remove(); const idx = this.#apps.findIndex(a => a.name === app.name); this.#apps.splice(idx, 1); diff --git a/projects/extension/src/mocks.ts b/projects/extension/src/mocks.ts index 36c0cce62..75453c849 100644 --- a/projects/extension/src/mocks.ts +++ b/projects/extension/src/mocks.ts @@ -8,6 +8,7 @@ import { MessageToManager, MessageFromManager } from '@substrate/connect-extension-protocol'; +import { SmoldotChain } from 'smoldot'; export class MockPort implements chrome.runtime.Port { sender: any; @@ -60,8 +61,8 @@ export class MockConnectionManager implements ConnectionManagerInterface { this.#willFindClient = willFindClient; } - addChain (name: string, spec: string): Promise { - return Promise.resolve(1); + addChain (name: string, spec: string): Promise { + return Promise.resolve({} as SmoldotChain); } registerApp(): void { From db61c88ce93830fe106ceb8684f896e92bdea30c Mon Sep 17 00:00:00 2001 From: wirednkod Date: Thu, 29 Jul 2021 14:28:18 +0300 Subject: [PATCH 10/16] fix lint errors and tests --- packages/connect/src/Detector.test.ts | 10 +++++----- packages/connect/src/Detector.ts | 6 +++--- .../src/ExtensionProvider/ExtensionProvider.test.ts | 2 +- .../src/ExtensionProvider/ExtensionProvider.ts | 11 ++++++----- projects/extension/jest.config.ts | 1 - projects/extension/src/background/AppMediator.ts | 2 +- .../src/background/ConnectionManager.test.ts | 2 +- .../extension/src/background/ConnectionManager.ts | 7 +++++-- projects/extension/src/mocks.ts | 2 +- 9 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/connect/src/Detector.test.ts b/packages/connect/src/Detector.test.ts index 4af02786d..ba9155a1a 100644 --- a/packages/connect/src/Detector.test.ts +++ b/packages/connect/src/Detector.test.ts @@ -15,14 +15,14 @@ describe('Initialize Detector without extension', () => { detect = new Detector('test-uapp'); const api = await detect.connect('westend'); expect(api).toBeTruthy(); - await detect.disconnect('westend'); + detect.disconnect('westend'); }, timeout); test('Should connect with known chain "polkadot".', async () => { detect = new Detector('test-uapp'); const api = await detect.connect('polkadot'); expect(api).toBeTruthy(); - await detect.disconnect('polkadot'); + detect.disconnect('polkadot'); }, extTimeout); test('Should connect with known chain westend, no chainSpecs and options', async () => { @@ -31,14 +31,14 @@ describe('Initialize Detector without extension', () => { const options = {} as ApiOptions; const api = await detect.connect(chainName, undefined, options); expect(api).toBeTruthy(); - await detect.disconnect('westend'); + detect.disconnect('westend'); }, extTimeout); test('Should connect with known chain "kusama".', async () => { detect = new Detector('test-uapp'); const api = await detect.connect('kusama'); expect(api).toBeTruthy(); - await detect.disconnect('kusama'); + detect.disconnect('kusama'); }, extTimeout); test('Should connect with unknown chain westend2 and chainSpecs.', async () => { @@ -47,7 +47,7 @@ describe('Initialize Detector without extension', () => { const detect = new Detector('test-uapp'); const api = await detect.connect(chainName, chainSpec); expect(api).toBeTruthy(); - await detect.disconnect(chainName); + detect.disconnect(chainName); }, extTimeout); test('Should NOT connect with unknown chain westend2 and without chainSpecs.', () => { diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 1eea66021..4770b2fb4 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -127,7 +127,7 @@ export class Detector { } if (this.#isExtension) { - provider = new ExtensionProvider(this.#name, chainName, chainSpec); + provider = new ExtensionProvider(this.#name, chainName, chainSpec) as ProviderInterface; } else if (!this.#isExtension) { const spec = JSON.stringify(this.#chainSpecs[chainName]); provider = new SmoldotProvider(spec); @@ -140,8 +140,8 @@ export class Detector { * * @param chainName - the name of the blockchain network to disconnect from */ - public disconnect = async (chainName: string): Promise => { - await this.#providers[chainName].disconnect(); + public disconnect = (chainName: string): void => { + this.#providers[chainName].disconnect(); delete this.#providers[chainName]; }; } diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts index bc8f77f06..21da2452b 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts @@ -61,7 +61,7 @@ test('disconnect sends disconnect message and emits disconnected', async () => { await ep.connect(); ep.on('disconnected', emitted); - await ep.disconnect(); + ep.disconnect(); await waitForMessageToBePosted(); const expectedMessage: ProviderMessageData = { diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 0994a5775..430f32975 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import {RpcCoder} from '@polkadot/rpc-provider/coder'; import { JsonRpcResponse, @@ -107,7 +108,7 @@ export class ExtensionProvider implements ProviderInterface { * @remarks This method is not supported * @throws {@link Error} */ - public clone(): ExtensionProvider { + public clone(): ProviderInterface { throw new Error('clone() is not supported.'); } @@ -215,7 +216,7 @@ export class ExtensionProvider implements ProviderInterface { // Once connect is sent - send rpc to extension that will contain the chainSpecs // for the extension to call addChain on smoldot - this.send('spec', [this.#chainSpecs]); + this.send('spec', [this.#chainSpecs]).catch(console.error); provider.listen(({data}: ExtensionMessage) => { if (data.origin && data.origin === CONTENT_SCRIPT_ORIGIN) { @@ -232,7 +233,7 @@ export class ExtensionProvider implements ProviderInterface { * Manually "disconnect" - sends a message to the `ExtensionMessageRouter` * telling it to disconnect the port with the background manager. */ - public async disconnect(): Promise { + public disconnect(): void { const disconnectMsg: ProviderMessageData = { appName: this.#appName, chainName: this.#chainName, @@ -350,7 +351,7 @@ export class ExtensionProvider implements ProviderInterface { callback: ProviderInterfaceCallback ): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await this.send(method, params, { callback, type }); + return await this.send(method, params, { callback, type }) as Promise; } /** @@ -379,7 +380,7 @@ export class ExtensionProvider implements ProviderInterface { return await this.send(method, [id]) as Promise; } - private emit(type: ProviderInterfaceEmitted, ...args: any[]): void { + private emit(type: ProviderInterfaceEmitted, ...args: unknown[]): void { this.#eventemitter.emit(type, ...args); } } diff --git a/projects/extension/jest.config.ts b/projects/extension/jest.config.ts index 53698a097..0bc6575a4 100644 --- a/projects/extension/jest.config.ts +++ b/projects/extension/jest.config.ts @@ -1,7 +1,6 @@ export default { coverageReporters: ["text-summary"], roots: ['/src'], - reporters: ["jest-silent-reporter"], testEnvironment: 'jsdom', setupFilesAfterEnv: ['./jest-setup.js'], verbose: true, diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index 5e295905d..46e9d2511 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -260,7 +260,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } else { chainSpec = parsed.params[0] as string; } - this.#addChain(chainName, chainSpec); + this.#addChain(chainName, chainSpec).catch(console.error); } else { // TODO: what about unsubscriptions requested by the UApp - we need to remove // the subscription from our subscriptions state diff --git a/projects/extension/src/background/ConnectionManager.test.ts b/projects/extension/src/background/ConnectionManager.test.ts index ce5c056d2..c5ddd167d 100644 --- a/projects/extension/src/background/ConnectionManager.test.ts +++ b/projects/extension/src/background/ConnectionManager.test.ts @@ -14,7 +14,7 @@ const connectApp = (manager: ConnectionManager, tabId: number, name: string, ne } test('adding and removing apps changes state', async () => { - //setup conenection manager with 2 chains + //setup connection manager with 2 chains const manager = new ConnectionManager(); manager.smoldotLogLevel = 1; await manager.initSmoldot(); diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index 014c3e1ff..fe240a56c 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as smoldot from 'smoldot'; import { AppMediator } from './AppMediator'; import { JsonRpcResponse, JsonRpcRequest, ConnectionManagerInterface } from './types'; @@ -194,13 +198,12 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) /** * initSmoldot initializes the smoldot client. */ - async initSmoldot () { + async initSmoldot(): Promise { try { this.#client = await (smoldot as any).start({ forbidWs: true, /* suppress console warnings about insecure connections */ maxLogLevel: this.smoldotLogLevel }); - // this.#isConnected = true; } catch (err) { l.error(`Error while initializing smoldot: ${err}`); } diff --git a/projects/extension/src/mocks.ts b/projects/extension/src/mocks.ts index 75453c849..e9fea481a 100644 --- a/projects/extension/src/mocks.ts +++ b/projects/extension/src/mocks.ts @@ -61,7 +61,7 @@ export class MockConnectionManager implements ConnectionManagerInterface { this.#willFindClient = willFindClient; } - addChain (name: string, spec: string): Promise { + addChain (): Promise { return Promise.resolve({} as SmoldotChain); } From 2a8853d76b8fa74959e01be81b2a191d883a2c34 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Fri, 30 Jul 2021 16:25:12 +0300 Subject: [PATCH 11/16] Fix circular dependencies issue that was breaking the tests --- packages/connect-extension-protocol/src/index.ts | 1 - .../connect-extension-protocol/tsconfig.json | 3 +-- packages/connect/src/Detector.ts | 4 ++-- .../src/ExtensionProvider/ExtensionProvider.ts | 5 +++-- projects/extension/{ .babelrc => .babelrc} | 7 +------ projects/extension/jest.config.ts | 1 + projects/extension/src/background/AppMediator.ts | 16 ++++++++++++++-- .../src/background/ConnectionManager.test.ts | 1 - .../src/background/ConnectionManager.ts | 7 ++----- 9 files changed, 24 insertions(+), 21 deletions(-) rename projects/extension/{ .babelrc => .babelrc} (73%) diff --git a/packages/connect-extension-protocol/src/index.ts b/packages/connect-extension-protocol/src/index.ts index 3f21fa06b..3048ffd5d 100644 --- a/packages/connect-extension-protocol/src/index.ts +++ b/packages/connect-extension-protocol/src/index.ts @@ -53,7 +53,6 @@ export const extension = { } }; - /** * ProviderMessage represents messages sent via `window.postMessage` from * `ExtensionProvider` to `ExtensionMessageRouter` as received by the extension. diff --git a/packages/connect-extension-protocol/tsconfig.json b/packages/connect-extension-protocol/tsconfig.json index 09fdade21..01afde1aa 100644 --- a/packages/connect-extension-protocol/tsconfig.json +++ b/packages/connect-extension-protocol/tsconfig.json @@ -12,6 +12,5 @@ "skipLibCheck": true, "strict": true, "module": "ES2020" - }, - "exclude": ["../../node_modules*", "./node_modules*"] + } } diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 4770b2fb4..4965a1671 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -121,11 +121,11 @@ export class Detector { */ public provider = (chainName: string, chainSpec?: string): ProviderInterface => { let provider: ProviderInterface = {} as ProviderInterface; - + if (!chainSpec && !Object.keys(this.#chainSpecs).includes(chainName)) { throw new Error(`No known Chain was detected and no chainSpec was provided. Either give a known chain name ('${Object.keys(this.#chainSpecs).join('\', \'')}') or provide valid chainSpecs.`) } - + if (this.#isExtension) { provider = new ExtensionProvider(this.#name, chainName, chainSpec) as ProviderInterface; } else if (!this.#isExtension) { diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 430f32975..92a70d2aa 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -217,7 +217,7 @@ export class ExtensionProvider implements ProviderInterface { // Once connect is sent - send rpc to extension that will contain the chainSpecs // for the extension to call addChain on smoldot this.send('spec', [this.#chainSpecs]).catch(console.error); - + provider.listen(({data}: ExtensionMessage) => { if (data.origin && data.origin === CONTENT_SCRIPT_ORIGIN) { this.#handleMessage(data); @@ -233,7 +233,7 @@ export class ExtensionProvider implements ProviderInterface { * Manually "disconnect" - sends a message to the `ExtensionMessageRouter` * telling it to disconnect the port with the background manager. */ - public disconnect(): void { + public disconnect(): Promise { const disconnectMsg: ProviderMessageData = { appName: this.#appName, chainName: this.#chainName, @@ -244,6 +244,7 @@ export class ExtensionProvider implements ProviderInterface { provider.send(disconnectMsg); this.#isConnected = false; this.emit('disconnected'); + return Promise.resolve(); } /** diff --git a/projects/extension/ .babelrc b/projects/extension/.babelrc similarity index 73% rename from projects/extension/ .babelrc rename to projects/extension/.babelrc index 6c2eeee11..0247759ee 100644 --- a/projects/extension/ .babelrc +++ b/projects/extension/.babelrc @@ -12,10 +12,5 @@ "babel-plugin-styled-components", "react-hot-loader/babel", "@babel/plugin-proposal-private-methods" - ], - "env": { - "test": { - "plugins": ["transform-es2015-modules-commonjs"] - } - } + ] } diff --git a/projects/extension/jest.config.ts b/projects/extension/jest.config.ts index 0bc6575a4..53698a097 100644 --- a/projects/extension/jest.config.ts +++ b/projects/extension/jest.config.ts @@ -1,6 +1,7 @@ export default { coverageReporters: ["text-summary"], roots: ['/src'], + reporters: ["jest-silent-reporter"], testEnvironment: 'jsdom', setupFilesAfterEnv: ['./jest-setup.js'], verbose: true, diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index 46e9d2511..eb2dae06c 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -14,7 +14,18 @@ import { SubscriptionMapping } from './types'; import { SmoldotChain } from 'smoldot'; -import { relayChains } from './index'; +import westend from '../../public/assets/westend.json'; +import kusama from '../../public/assets/kusama.json'; +import polkadot from '../../public/assets/polkadot.json'; + +type RelayType = Record + +export const relayChains: RelayType = { + "polkadot": JSON.stringify(polkadot), + "kusama": JSON.stringify(kusama), + "westend": JSON.stringify(westend) +} + /** * AppMediator is the class that represents and manages an app's connection to * a blockchain network. N.B. an app that connects to multiple nblockchain @@ -248,9 +259,10 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { method: parsed.method }); } - const chainName = this.#chainName as string; + console.log('parsed.method', parsed.method); + if (parsed.method === 'spec' && chainName) { // When params[0] (chainSpecs in the current case is empty) then this is a // known relay chain and specs should be retrieved from inside the extension diff --git a/projects/extension/src/background/ConnectionManager.test.ts b/projects/extension/src/background/ConnectionManager.test.ts index c5ddd167d..1a5429491 100644 --- a/projects/extension/src/background/ConnectionManager.test.ts +++ b/projects/extension/src/background/ConnectionManager.test.ts @@ -84,7 +84,6 @@ test('adding and removing apps changes state', async () => { ] }); - handler.mockClear(); manager.disconnectTab(42); expect(handler).toHaveBeenCalledTimes(2); diff --git a/projects/extension/src/background/ConnectionManager.ts b/projects/extension/src/background/ConnectionManager.ts index fe240a56c..c5fada5fd 100644 --- a/projects/extension/src/background/ConnectionManager.ts +++ b/projects/extension/src/background/ConnectionManager.ts @@ -173,17 +173,14 @@ export class ConnectionManager extends (EventEmitter as { new(): StateEmitter }) * unregisterApp is used after an app has finished processing any unsubscribe * messages and disconnected to fully unregister itself. * It also retrieves the chain that app was connected to and calls smoldot for removal - * + * * @param app - The app */ unregisterApp(app: AppMediator): void { if (!this.#client) { throw new Error('Tried to unregister an app to smoldot client that does not exist.'); } - if (!app.chain) { - throw new Error('Tried to remove an app from a chain that does not exist.'); - } - app.chain?.remove(); + app.chain && app.chain?.remove(); const idx = this.#apps.findIndex(a => a.name === app.name); this.#apps.splice(idx, 1); this.emit('stateChanged', this.getState()); From 8713be109a6305917f200c4c887392cfe46dbe3d Mon Sep 17 00:00:00 2001 From: wirednkod Date: Fri, 30 Jul 2021 16:32:52 +0300 Subject: [PATCH 12/16] fix lint issues --- packages/connect/src/Detector.ts | 2 +- .../connect/src/ExtensionProvider/ExtensionProvider.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect/src/Detector.ts b/packages/connect/src/Detector.ts index 4965a1671..218e583b1 100644 --- a/packages/connect/src/Detector.ts +++ b/packages/connect/src/Detector.ts @@ -141,7 +141,7 @@ export class Detector { * @param chainName - the name of the blockchain network to disconnect from */ public disconnect = (chainName: string): void => { - this.#providers[chainName].disconnect(); + void this.#providers[chainName].disconnect(); delete this.#providers[chainName]; }; } diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts index 21da2452b..046fe93ca 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.test.ts @@ -61,7 +61,7 @@ test('disconnect sends disconnect message and emits disconnected', async () => { await ep.connect(); ep.on('disconnected', emitted); - ep.disconnect(); + void ep.disconnect(); await waitForMessageToBePosted(); const expectedMessage: ProviderMessageData = { From 3ed218fc7537ac506e79b05e75c92259f9f953db Mon Sep 17 00:00:00 2001 From: wirednkod Date: Fri, 30 Jul 2021 16:35:52 +0300 Subject: [PATCH 13/16] remove stray console log --- projects/extension/src/background/AppMediator.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index eb2dae06c..f08d3387d 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -261,8 +261,6 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } const chainName = this.#chainName as string; - console.log('parsed.method', parsed.method); - if (parsed.method === 'spec' && chainName) { // When params[0] (chainSpecs in the current case is empty) then this is a // known relay chain and specs should be retrieved from inside the extension From dd1666830c73207d30b8d6f72e20630489490dfc Mon Sep 17 00:00:00 2001 From: wirednkod Date: Mon, 2 Aug 2021 19:00:40 +0300 Subject: [PATCH 14/16] Fix PR comments --- .../connect-extension-protocol/src/index.ts | 4 ++-- .../src/ExtensionProvider/ExtensionProvider.ts | 15 +++++++++++++-- .../src/content/ExtensionMessageRouter.ts | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/connect-extension-protocol/src/index.ts b/packages/connect-extension-protocol/src/index.ts index 3048ffd5d..19f44ba19 100644 --- a/packages/connect-extension-protocol/src/index.ts +++ b/packages/connect-extension-protocol/src/index.ts @@ -68,7 +68,7 @@ export interface ProviderMessageData { /** The name of the blockchain network the app is talking to **/ chainName: string; /** What action the `ExtensionMessageRouter` should take **/ - action: 'forward' | 'connect' | 'disconnect'; + action: 'forward' | 'connect' | 'disconnect' | 'spec'; /** The message the `ExtensionMessageRouter` should forward to the background **/ message?: MessageToManager; } @@ -79,7 +79,7 @@ export interface ProviderMessageData { */ export interface MessageToManager { /** Type of the message. Defines how to interpret the {@link payload} */ - type: 'rpc'; + type: 'rpc' | 'spec'; /** Payload of the message - a JSON encoded RPC request **/ payload: string; /** whether an RPC message is a subscription or not **/ diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 92a70d2aa..5a4d92ead 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -216,8 +216,19 @@ export class ExtensionProvider implements ProviderInterface { // Once connect is sent - send rpc to extension that will contain the chainSpecs // for the extension to call addChain on smoldot - this.send('spec', [this.#chainSpecs]).catch(console.error); - + const someMsg: ProviderMessageData = { + appName: this.#appName, + chainName: this.#chainName, + action: 'spec', + origin: EXTENSION_PROVIDER_ORIGIN, + message: { + type: 'spec', + payload: this.#chainSpecs || '' + } + } + + provider.send(someMsg); + provider.listen(({data}: ExtensionMessage) => { if (data.origin && data.origin === CONTENT_SCRIPT_ORIGIN) { this.#handleMessage(data); diff --git a/projects/extension/src/content/ExtensionMessageRouter.ts b/projects/extension/src/content/ExtensionMessageRouter.ts index 7185bd924..a4129b12a 100644 --- a/projects/extension/src/content/ExtensionMessageRouter.ts +++ b/projects/extension/src/content/ExtensionMessageRouter.ts @@ -45,6 +45,18 @@ export class ExtensionMessageRouter { window.removeEventListener('message', this.#handleMessage); } + #sendSpecs = (message: ProviderMessage): void => { + const {data: { chainName }} = message; + const port = this.#ports[chainName]; + if (!port) { + // this is probably someone trying to abuse the extension. + console.warn(`App requested to send message to ${chainName} - no port found`); + return; + } + debug(`SENDING SPECS TO ${chainName} PORT`, message); + port.postMessage(message); + } + #establishNewConnection = (message: ProviderMessage): void => { const data = message.data; const port = chrome.runtime.connect({ name: `${data.appName}::${data.chainName}` }); @@ -112,6 +124,12 @@ export class ExtensionMessageRouter { return this.#establishNewConnection(message); } + if (data.action === 'spec') { + console.log('data action: ', data.action); + console.log('message', message.data) + return this.#sendSpecs(message); + } + if (data.action === 'disconnect') { return this.#disconnectPort(message); } From dc27cb18d4826ced963141337aa8f123612d3276 Mon Sep 17 00:00:00 2001 From: wirednkod Date: Mon, 2 Aug 2021 23:08:22 +0300 Subject: [PATCH 15/16] Keep action without spec; Alter message to include 'spec' and use 'forward' method for propagating it to background --- .../connect-extension-protocol/src/index.ts | 2 +- .../ExtensionProvider/ExtensionProvider.ts | 2 +- .../extension/src/background/AppMediator.ts | 6 +++--- .../src/content/ExtensionMessageRouter.ts | 20 +------------------ 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/connect-extension-protocol/src/index.ts b/packages/connect-extension-protocol/src/index.ts index 19f44ba19..61f51b90f 100644 --- a/packages/connect-extension-protocol/src/index.ts +++ b/packages/connect-extension-protocol/src/index.ts @@ -68,7 +68,7 @@ export interface ProviderMessageData { /** The name of the blockchain network the app is talking to **/ chainName: string; /** What action the `ExtensionMessageRouter` should take **/ - action: 'forward' | 'connect' | 'disconnect' | 'spec'; + action: 'forward' | 'connect' | 'disconnect'; /** The message the `ExtensionMessageRouter` should forward to the background **/ message?: MessageToManager; } diff --git a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts index 5a4d92ead..eb5d690a4 100644 --- a/packages/connect/src/ExtensionProvider/ExtensionProvider.ts +++ b/packages/connect/src/ExtensionProvider/ExtensionProvider.ts @@ -219,7 +219,7 @@ export class ExtensionProvider implements ProviderInterface { const someMsg: ProviderMessageData = { appName: this.#appName, chainName: this.#chainName, - action: 'spec', + action: 'forward', origin: EXTENSION_PROVIDER_ORIGIN, message: { type: 'spec', diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index f08d3387d..793c83e91 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -240,7 +240,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } #handleRpcRequest = (msg: MessageToManager): void => { - if (msg.type !== 'rpc') { + if (msg.type !== 'rpc' && msg.type !== 'spec') { console.warn(`Unrecognised message type ${msg.type} received from content script`); return; } @@ -261,14 +261,14 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { } const chainName = this.#chainName as string; - if (parsed.method === 'spec' && chainName) { + if (msg.type === 'spec' && chainName) { // When params[0] (chainSpecs in the current case is empty) then this is a // known relay chain and specs should be retrieved from inside the extension let chainSpec: string; if (Object.keys(relayChains).includes(chainName)) { chainSpec = relayChains[chainName]; } else { - chainSpec = parsed.params[0] as string; + chainSpec = msg.payload; } this.#addChain(chainName, chainSpec).catch(console.error); } else { diff --git a/projects/extension/src/content/ExtensionMessageRouter.ts b/projects/extension/src/content/ExtensionMessageRouter.ts index a4129b12a..5a74ef62d 100644 --- a/projects/extension/src/content/ExtensionMessageRouter.ts +++ b/projects/extension/src/content/ExtensionMessageRouter.ts @@ -45,18 +45,6 @@ export class ExtensionMessageRouter { window.removeEventListener('message', this.#handleMessage); } - #sendSpecs = (message: ProviderMessage): void => { - const {data: { chainName }} = message; - const port = this.#ports[chainName]; - if (!port) { - // this is probably someone trying to abuse the extension. - console.warn(`App requested to send message to ${chainName} - no port found`); - return; - } - debug(`SENDING SPECS TO ${chainName} PORT`, message); - port.postMessage(message); - } - #establishNewConnection = (message: ProviderMessage): void => { const data = message.data; const port = chrome.runtime.connect({ name: `${data.appName}::${data.chainName}` }); @@ -124,12 +112,6 @@ export class ExtensionMessageRouter { return this.#establishNewConnection(message); } - if (data.action === 'spec') { - console.log('data action: ', data.action); - console.log('message', message.data) - return this.#sendSpecs(message); - } - if (data.action === 'disconnect') { return this.#disconnectPort(message); } @@ -142,7 +124,7 @@ export class ExtensionMessageRouter { return; } - if (innerMessage.type === 'rpc') { + if (innerMessage.type === 'rpc' || innerMessage.type === 'spec') { return this.#forwardRpcMessage(message); } From 137d153ca7372383410f236eb3986beb0d516c5f Mon Sep 17 00:00:00 2001 From: wirednkod Date: Tue, 3 Aug 2021 13:04:43 +0300 Subject: [PATCH 16/16] fix PR comments --- .../extension/src/background/AppMediator.ts | 24 +++++++------------ projects/extension/src/background/index.ts | 14 +++++------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/projects/extension/src/background/AppMediator.ts b/projects/extension/src/background/AppMediator.ts index 793c83e91..7ba6e0102 100644 --- a/projects/extension/src/background/AppMediator.ts +++ b/projects/extension/src/background/AppMediator.ts @@ -18,14 +18,13 @@ import westend from '../../public/assets/westend.json'; import kusama from '../../public/assets/kusama.json'; import polkadot from '../../public/assets/polkadot.json'; -type RelayType = Record - -export const relayChains: RelayType = { - "polkadot": JSON.stringify(polkadot), - "kusama": JSON.stringify(kusama), - "westend": JSON.stringify(westend) -} +type RelayType = Map +export const relayChains: RelayType = new Map([ + ["polkadot", JSON.stringify(polkadot)], + ["kusama", JSON.stringify(kusama)], + ["westend", JSON.stringify(westend)] +]) /** * AppMediator is the class that represents and manages an app's connection to * a blockchain network. N.B. an app that connects to multiple nblockchain @@ -262,14 +261,9 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) { const chainName = this.#chainName as string; if (msg.type === 'spec' && chainName) { - // When params[0] (chainSpecs in the current case is empty) then this is a - // known relay chain and specs should be retrieved from inside the extension - let chainSpec: string; - if (Object.keys(relayChains).includes(chainName)) { - chainSpec = relayChains[chainName]; - } else { - chainSpec = msg.payload; - } + const chainSpec: string = relayChains.has(chainName) ? + (relayChains.get(chainName) || '') : + msg.payload this.#addChain(chainName, chainSpec).catch(console.error); } else { // TODO: what about unsubscriptions requested by the UApp - we need to remove diff --git a/projects/extension/src/background/index.ts b/projects/extension/src/background/index.ts index 743f2fb74..a2281e1b7 100644 --- a/projects/extension/src/background/index.ts +++ b/projects/extension/src/background/index.ts @@ -12,13 +12,13 @@ declare let window: Background; const manager = window.manager = new ConnectionManager(); -type RelayType = Record +type RelayType = Map -export const relayChains: RelayType = { - "polkadot": JSON.stringify(polkadot), - "kusama": JSON.stringify(kusama), - "westend": JSON.stringify(westend) -} +export const relayChains: RelayType = new Map([ + ["polkadot", JSON.stringify(polkadot)], + ["kusama", JSON.stringify(kusama)], + ["westend", JSON.stringify(westend)] +]) const l = logger('Extension'); export interface RequestRpcSend { @@ -29,7 +29,7 @@ export interface RequestRpcSend { const init = async () => { try { await manager.initSmoldot(); - for(const [key, value] of Object.entries(relayChains)) { + for(const [key, value] of relayChains.entries()) { await manager.addChain(key, value).catch(err => l.error('Error', err)); } } catch (e) {