Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension - Add/Remove Chains (Smoldot v.0.3.x) #428

Merged
merged 17 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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