Skip to content

Commit

Permalink
Extension - Add/Remove Chains (Smoldot v.0.3.x) (#428)
Browse files Browse the repository at this point in the history
* minor changes on Detector to accept chainspecs
* Add implementation concerning adding chain of new app end to end
* add code for removing app's chain from smoldot upon disconnection
* remove indexing from Appmediator and related chain with it
* add more exclusions for IDE's sanity
* correct some test cases and remove stray log
* Fix circular dependencies issue that was breaking the tests
* Keep action without spec; Alter message to include 'spec' and use 'forward' method for propagating it to background
  • Loading branch information
wirednkod committed Aug 3, 2021
1 parent 11eb3bb commit 3493be4
Show file tree
Hide file tree
Showing 15 changed files with 121 additions and 78 deletions.
7 changes: 3 additions & 4 deletions packages/connect-extension-protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -53,7 +53,6 @@ export const extension = {
}
};


/**
* ProviderMessage represents messages sent via `window.postMessage` from
* `ExtensionProvider` to `ExtensionMessageRouter` as received by the extension.
Expand All @@ -71,7 +70,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;
}

/**
Expand All @@ -80,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 **/
Expand Down
10 changes: 5 additions & 5 deletions packages/connect/src/Detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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.', () => {
Expand Down
22 changes: 10 additions & 12 deletions packages/connect/src/Detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,16 @@ export class Detector {
public provider = (chainName: string, chainSpec?: string): ProviderInterface => {
let provider: ProviderInterface = {} as ProviderInterface;

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) {
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) {
const spec = JSON.stringify(this.#chainSpecs[chainName]);
provider = new SmoldotProvider(spec);
}
return provider;
}

Expand All @@ -142,8 +140,8 @@ export class Detector {
*
* @param chainName - the name of the blockchain network to disconnect from
*/
public disconnect = async (chainName: string): Promise<void> => {
await this.#providers[chainName].disconnect();
public disconnect = (chainName: string): void => {
void this.#providers[chainName].disconnect();
delete this.#providers[chainName];
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -61,7 +61,7 @@ test('disconnect sends disconnect message and emits disconnected', async () => {
await ep.connect();

ep.on('disconnected', emitted);
await ep.disconnect();
void ep.disconnect();
await waitForMessageToBePosted();

const expectedMessage: ProviderMessageData = {
Expand All @@ -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);
Expand Down
35 changes: 25 additions & 10 deletions packages/connect/src/ExtensionProvider/ExtensionProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {RpcCoder} from '@polkadot/rpc-provider/coder';
import {
JsonRpcResponse,
Expand Down Expand Up @@ -61,10 +62,12 @@ export class ExtensionProvider implements ProviderInterface {

#appName: string;
#chainName: string;
#chainSpecs: string | undefined;

public constructor(appName: string, chainName: string) {
public constructor(appName: string, chainName: string, chainSpecs?: string) {
this.#appName = appName;
this.#chainName = chainName;
this.#chainSpecs = chainSpecs;
}

/**
Expand Down Expand Up @@ -105,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.');
}

Expand Down Expand Up @@ -210,9 +213,24 @@ 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
const someMsg: ProviderMessageData = {
appName: this.#appName,
chainName: this.#chainName,
action: 'forward',
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) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.#handleMessage(data);
}
});
Expand All @@ -226,8 +244,7 @@ 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<void> {
public disconnect(): Promise<void> {
const disconnectMsg: ProviderMessageData = {
appName: this.#appName,
chainName: this.#chainName,
Expand All @@ -238,6 +255,7 @@ export class ExtensionProvider implements ProviderInterface {
provider.send(disconnectMsg);
this.#isConnected = false;
this.emit('disconnected');
return Promise.resolve();
}

/**
Expand Down Expand Up @@ -276,10 +294,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<any> {
return new Promise((resolve, reject): void => {
const json = this.#coder.encodeJson(method, params);
Expand Down Expand Up @@ -347,7 +363,7 @@ export class ExtensionProvider implements ProviderInterface {
callback: ProviderInterfaceCallback
): Promise<number | string> {
// 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<number | string>;
}

/**
Expand Down Expand Up @@ -376,8 +392,7 @@ export class ExtensionProvider implements ProviderInterface {
return await this.send(method, [id]) as Promise<boolean>;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private emit(type: ProviderInterfaceEmitted, ...args: any[]): void {
private emit(type: ProviderInterfaceEmitted, ...args: unknown[]): void {
this.#eventemitter.emit(type, ...args);
}
}
7 changes: 1 addition & 6 deletions projects/extension/ .babelrc → projects/extension/.babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,5 @@
"babel-plugin-styled-components",
"react-hot-loader/babel",
"@babel/plugin-proposal-private-methods"
],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
]
}
48 changes: 39 additions & 9 deletions projects/extension/src/background/AppMediator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import EventEmitter from 'eventemitter3';
import {
MessageToManager,
Expand All @@ -14,7 +13,18 @@ import {
StateEmitter,
SubscriptionMapping
} from './types';

import { SmoldotChain } from 'smoldot';
import westend from '../../public/assets/westend.json';
import kusama from '../../public/assets/kusama.json';
import polkadot from '../../public/assets/polkadot.json';

type RelayType = Map<string, string>

export const relayChains: RelayType = new Map<string, string>([
["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
Expand All @@ -31,6 +41,7 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) {
readonly #url: string | undefined;
readonly #manager: ConnectionManagerInterface;
#chainName: string | undefined = undefined;
#chain: SmoldotChain | undefined;
#state: AppState = 'connected';
/** subscriptions is all the active message subscriptions this ap[ has */
readonly subscriptions: SubscriptionMapping[];
Expand Down Expand Up @@ -82,13 +93,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 || '';
}

/**
* returns the chain that the app is connected to
*/
get chain(): SmoldotChain | undefined {
return this.#chain;
}

/** tabId is the tabId of the app in the browser */
get tabId(): number | undefined {
return this.#tabId;
Expand Down Expand Up @@ -216,8 +234,12 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) {
return true;
}

#addChain = async (chainName: string, chainSpecs: string): Promise<void> => {
this.#chain = await this.#manager.addChain(chainName, chainSpecs);
}

#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;
}
Expand All @@ -236,11 +258,19 @@ export class AppMediator extends (EventEmitter as { new(): StateEmitter }) {
method: parsed.method
});
}

// 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 (msg.type === 'spec' && chainName) {
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
// the subscription from our subscriptions state
const chainID = this.#manager.sendRpcMessageTo(chainName, parsed);
this.requests.push({ appID, chainID });
}
}

/**
Expand Down
15 changes: 6 additions & 9 deletions projects/extension/src/background/ConnectionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -84,7 +84,6 @@ test('adding and removing apps changes state', async () => {
]
});


handler.mockClear();
manager.disconnectTab(42);
expect(handler).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -177,22 +176,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);
Expand Down
Loading

0 comments on commit 3493be4

Please sign in to comment.