Provider Injection (#253)

* Add PostMessageProvider

* Remove coder

* Add provier in state

* Add msg handling

* Fix lint

* Fix little bugs

* Make send work

* Small tweaks

* Make subscriptions work

* Add readme

* Remove useless file

* Roder alphabet

* Fix lint

* List & start provider

* Add unsubscribe

* List and Use providers

* Update packages/extension/src/background/types.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension/src/background/handlers/State.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension/src/background/handlers/State.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension/src/background/handlers/State.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension/src/background/handlers/State.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension-dapp/src/index.ts

Co-Authored-By: Jaco Greeff <jacogr@gmail.com>

* Update README

* Remove circular dep

* Remove useless function

* Update README

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Amaury Martiny
2020-02-26 17:18:44 +01:00
committed by GitHub
co-authored by Jaco Greeff
parent 602dfdc026
commit 452ce41b71
8 changed files with 381 additions and 6 deletions
+9 -1
View File
@@ -13,7 +13,7 @@ A basic extractor that manipulates the `window.injectedWeb3` to retrieve all the
## Usage
```js
import { web3Accounts, web3Enable, web3FromAddress } from '@polkadot/extension-dapp';
import { web3Accounts, web3Enable, web3FromAddress, web3ListRpcProviders, web3UseRpcProvider } from '@polkadot/extension-dapp';
// returns an array of all the injected sources
// (this needs to be called first, before other requests)
@@ -35,4 +35,12 @@ api.setSigner(injector.signer);
api.tx.balances
.transfer('5C5555yEXUcmEJ5kkcCMvdZjUo7NGJiQJMS7vZXEeoMhj3VQ', 123456)
.signAndSend('5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE', (status) => { ... });
// retrieve all the RPC providers from a particular source. this returns a map
// of string->ProviderMeta, where the string represents an unique key to identify
// a provider, and ProviderMeta shows metadata about this provider
const allProviders = web3ListRpcProviders('polkadot-js');
// assuming one of the keys in `allProviders` is 'kusama-cc3', we can then use that provider
const { provider } = web3UseRpcProvider('polkadot-js', 'kusama-cc3');
const head = await provider.send('chain_getHeader', []); // shows latest header
```
+27 -1
View File
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedWindow, Unsubcall } from '@polkadot/extension-inject/types';
import { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedProviderWithMeta, InjectedWindow, Unsubcall, ProviderList } from '@polkadot/extension-inject/types';
// just a helper (otherwise we cast all-over, so shorter and more readable)
const win = window as Window & InjectedWindow;
@@ -176,3 +176,29 @@ export async function web3FromAddress (address: string): Promise<InjectedExtensi
return web3FromSource(found.meta.source);
}
// retrieve all providers exposed by one source
export async function web3ListRpcProviders (source: string): Promise<ProviderList | null> {
const { provider } = await web3FromSource(source);
if (!provider) {
console.warn(`Extension ${source} does not expose any provider`);
return null;
}
return provider.listProviders();
}
// retrieve all providers exposed by one source
export async function web3UseRpcProvider (source: string, key: string): Promise<InjectedProviderWithMeta> {
const { provider } = await web3FromSource(source);
if (!provider) {
throw new Error(`Extension ${source} does not expose any provider`);
}
const meta = await provider.startProvider(key);
return { provider, meta };
}
+27
View File
@@ -3,6 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { Signer as InjectedSigner } from '@polkadot/api/types';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
// eslint-disable-next-line no-undef
type This = typeof globalThis;
@@ -34,9 +35,35 @@ export interface InjectedExtensionInfo {
version: string;
}
// Metadata about a provider
export interface ProviderMeta {
// Network of the provider
network: string;
// Light or full node
node: 'full' | 'light';
// The extension source
source: string;
// Provider transport: 'WsProvider' etc.
transport: string;
}
export type ProviderList = Record<string, ProviderMeta>
export interface InjectedProvider extends ProviderInterface {
listProviders: () => Promise<ProviderList>;
startProvider: (key: string) => Promise<ProviderMeta>;
}
export interface InjectedProviderWithMeta {
// provider will actually always be a PostMessageProvider, which implements InjectedProvider
provider: InjectedProvider;
meta: ProviderMeta;
}
export interface Injected {
accounts: InjectedAccounts;
signer: InjectedSigner;
provider?: InjectedProvider;
}
export interface InjectedWindowProvider {
@@ -2,7 +2,10 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestSign, ResponseSigning, SigningRequest } from '../types';
import { ProviderMeta } from '@polkadot/extension-inject/types';
import { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
import { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest, RequestRpcUnsubscribe } from '../types';
import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs';
@@ -25,6 +28,15 @@ type AuthUrls = Record<string, {
url: string;
}>;
// List of providers passed into constructor. This is the list of providers
// exposed by the extension.
type Providers = Record<string, {
meta: ProviderMeta;
// The provider is not running at init, calling this will instantiate the
// provider.
start: () => ProviderInterface;
}>
interface SignRequest {
account: AccountJson;
id: string;
@@ -56,6 +68,12 @@ export default class State {
readonly #authRequests: Record<string, AuthRequest> = {};
// Map of providers currently injected in tabs
readonly #injectedProviders: Map<chrome.runtime.Port, ProviderInterface> = new Map();
// Map of all providers exposed by the extension, they are retrievable by key
readonly #providers: Providers;
readonly #signRequests: Record<string, SignRequest> = {};
#windows: number[] = [];
@@ -64,6 +82,10 @@ export default class State {
public readonly signSubject: BehaviorSubject<SigningRequest[]> = new BehaviorSubject([] as SigningRequest[]);
constructor (providers: Providers = {}) {
this.#providers = providers;
}
public get hasAuthRequests (): boolean {
return this.numAuthRequests === 0;
}
@@ -213,6 +235,69 @@ export default class State {
return this.#signRequests[id];
}
// List all providers the extension is exposing
public rpcListProviders (): Promise<ResponseRpcListProviders> {
return Promise.resolve(Object.keys(this.#providers).reduce((acc, key) => {
acc[key] = this.#providers[key].meta;
return acc;
}, {} as ResponseRpcListProviders));
}
public rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise<JsonRpcResponse> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.subscribe) before provider has been set');
return provider.send(request.method, request.params);
}
// Start a provider, return its meta
public rpcStartProvider (key: string, port: chrome.runtime.Port): Promise<ProviderMeta> {
assert(Object.keys(this.#providers).includes(key), `Provider ${key} is not exposed by extension`);
if (this.#injectedProviders.get(port)) {
return Promise.resolve(this.#providers[key].meta);
}
// Instantiate the provider
this.#injectedProviders.set(port, this.#providers[key].start());
// Close provider connection when page is closed
port.onDisconnect.addListener((): void => {
const provider = this.#injectedProviders.get(port);
if (provider) {
provider.disconnect();
}
this.#injectedProviders.delete(port);
});
return Promise.resolve(this.#providers[key].meta);
}
public rpcSubscribe (request: RequestRpcSubscribe, cb: ProviderInterfaceCallback, port: chrome.runtime.Port): Promise<number> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.subscribe) before provider has been set');
return provider.subscribe(
request.type,
request.method,
request.params,
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore WsProvider gives me (error, result)=>void, whereas (result)=>void is expected
(_error, res) => { cb(res); }
);
}
public rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise<boolean> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.unsubscribe) before provider has been set');
return provider.unsubscribe(request.type, request.method, request.subscriptionId);
}
public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> {
const id = getId();
@@ -2,11 +2,12 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { InjectedAccount, ProviderMeta } from '@polkadot/extension-inject/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { RequestAuthorizeTab, ResponseSigning, RequestTypes, ResponseTypes, MessageTypes } from '../types';
import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { RequestAuthorizeTab, ResponseSigning, RequestTypes, ResponseTypes, MessageTypes, ResponseRpcListProviders, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe } from '../types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
@@ -72,6 +73,34 @@ export default class Tabs {
return this.#state.sign(url, new RequestExtrinsicSign(request), { address, ...pair.meta });
}
private rpcListProviders (): Promise<ResponseRpcListProviders> {
return this.#state.rpcListProviders();
}
private rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise<JsonRpcResponse> {
return this.#state.rpcSend(request, port);
}
private rpcStartProvider (key: string, port: chrome.runtime.Port): Promise<ProviderMeta> {
return this.#state.rpcStartProvider(key, port);
}
private async rpcSubscribe (request: RequestRpcSubscribe, id: string, port: chrome.runtime.Port): Promise<boolean> {
const cb = createSubscription<'pub(rpc.subscribe)'>(id, port);
const subscriptionId = await this.#state.rpcSubscribe(request, cb, port);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
this.rpcUnsubscribe({ ...request, subscriptionId }, port);
});
return true;
}
private async rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise<boolean> {
return this.#state.rpcUnsubscribe(request, port);
}
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
if (type !== 'pub(authorize.tab)') {
this.#state.ensureUrlAuthorized(url);
@@ -93,6 +122,21 @@ export default class Tabs {
case 'pub(extrinsic.sign)':
return this.extrinsicSign(url, request as SignerPayloadJSON);
case 'pub(rpc.listProviders)':
return this.rpcListProviders();
case 'pub(rpc.send)':
return this.rpcSend(request as RequestRpcSend, port);
case 'pub(rpc.startProvider)':
return this.rpcStartProvider(request as string, port);
case 'pub(rpc.subscribe)':
return this.rpcSubscribe(request as RequestRpcSubscribe, id, port);
case 'pub(rpc.unsubscribe)':
return this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port);
default:
throw new Error(`Unable to handle message of type ${type}`);
}
+25 -1
View File
@@ -2,7 +2,8 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { InjectedAccount, ProviderList, ProviderMeta } from '@polkadot/extension-inject/types';
import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
import { KeypairType } from '@polkadot/util-crypto/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
@@ -67,6 +68,11 @@ export interface RequestSignatures {
'pub(authorize.tab)': [RequestAuthorizeTab, null];
'pub(bytes.sign)': [SignerPayloadRaw, ResponseSigning];
'pub(extrinsic.sign)': [SignerPayloadJSON, ResponseSigning];
'pub(rpc.listProviders)': [void, ResponseRpcListProviders];
'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse];
'pub(rpc.startProvider)': [string, ProviderMeta];
'pub(rpc.subscribe)': [RequestRpcSubscribe, number, JsonRpcResponse];
'pub(rpc.unsubscribe)': [RequestRpcUnsubscribe, boolean];
}
export type MessageTypes = keyof RequestSignatures;
@@ -133,6 +139,22 @@ export type RequestAccountList = null;
export type RequestAccountSubscribe = null;
export interface RequestRpcSend {
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any[];
}
export interface RequestRpcSubscribe extends RequestRpcSend {
type: string;
}
export interface RequestRpcUnsubscribe {
method: string;
subscriptionId: number;
type: string;
}
export interface RequestSigningApprovePassword {
id: string;
password: string;
@@ -204,6 +226,8 @@ export interface ResponseAccountExport {
exportedJson: string;
}
export type ResponseRpcListProviders = ProviderList;
// Subscriptions
export type SubscriptionMessageTypes = NoUndefinedValues<{
+4
View File
@@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import PostMessageProvider from './PostMessageProvider';
import { Injected } from '@polkadot/extension-inject/types';
import { SendRequest } from './types';
@@ -11,10 +12,13 @@ import Signer from './Signer';
export default class implements Injected {
public readonly accounts: Accounts;
public readonly provider: PostMessageProvider;
public readonly signer: Signer;
constructor (sendRequest: SendRequest) {
this.accounts = new Accounts(sendRequest);
this.provider = new PostMessageProvider(sendRequest);
this.signer = new Signer(sendRequest);
}
}
@@ -0,0 +1,157 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '@polkadot/rpc-provider/types';
import { AnyFunction } from '@polkadot/types/types';
import { isUndefined, logger } from '@polkadot/util';
import EventEmitter from 'eventemitter3';
import { SendRequest } from './types';
import { ProviderList, ProviderMeta, InjectedProvider } from '@polkadot/extension-inject/types';
const l = logger('PostMessageProvider');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CallbackHandler = (error?: null | Error, value?: any) => void;
// Same as https://github.com/polkadot-js/api/blob/57ca9a9c3204339e1e1f693fcacc33039868dc27/packages/rpc-provider/src/ws/Provider.ts#L17
interface SubscriptionHandler {
callback: CallbackHandler;
type: string;
}
/**
* @name PostMessageProvider
*
* @description Extension provider to be used by dapps
*/
export default class PostMessageProvider implements InjectedProvider {
readonly #eventemitter: EventEmitter;
readonly #sendRequest: SendRequest;
// Subscription IDs are (historically) not guaranteed to be globally unique;
// only unique for a given subscription method; which is why we identify
// the subscriptions based on subscription id + type
readonly #subscriptions: Record<string, AnyFunction> = {}; // {[(type,subscriptionId)]: callback}
/**
* @param {function} sendRequest The function to be called to send requests to the node
* @param {function} subscriptionNotificationHandler Channel for receiving subscription messages
*/
public constructor (sendRequest: SendRequest) {
this.#eventemitter = new EventEmitter();
this.#sendRequest = sendRequest;
// Give subscribers time to subscribe
setTimeout((): void => {
this.#eventemitter.emit('connected');
});
}
/**
* @description Returns a clone of the object
*/
public clone (): PostMessageProvider {
return new PostMessageProvider(this.#sendRequest);
}
/**
* @description Manually disconnect from the connection, clearing autoconnect logic
*/
public disconnect (): void {
// FIXME This should see if the extension's state's provider can disconnect
console.error('PostMessageProvider.disconnect() is not implemented.');
}
/**
* @summary `true` when this provider supports subscriptions
*/
public get hasSubscriptions (): boolean {
// FIXME This should see if the extension's state's provider has subscriptions
return true;
}
/**
* @summary Whether the node is connected or not.
* @return {boolean} true if connected
*/
public isConnected (): boolean {
// FIXME This should see if the extension's state's provider is connected
return true;
}
public listProviders (): Promise<ProviderList> {
return this.#sendRequest('pub(rpc.listProviders)', undefined);
}
/**
* @summary Listens on events after having subscribed using the [[subscribe]] function.
* @param {ProviderInterfaceEmitted} type Event
* @param {ProviderInterfaceEmitCb} sub Callback
* @return unsubscribe function
*/
public on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void {
this.#eventemitter.on(type, sub);
return (): void => {
this.#eventemitter.removeListener(type, sub);
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public send (method: string, params: any[], subscription?: SubscriptionHandler): Promise<any> {
if (subscription) {
const { callback, type } = subscription;
return this.#sendRequest(
'pub(rpc.subscribe)',
{ type, method, params },
(res) => {
subscription.callback(null, res);
}
).then((id): number => {
this.#subscriptions[`${type}::${id}`] = callback;
return id;
});
} else {
return this.#sendRequest('pub(rpc.send)', { method, params });
}
}
public startProvider (key: string): Promise<ProviderMeta> {
return this.#sendRequest('pub(rpc.startProvider)', key);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async subscribe (type: string, method: string, params: any[], callback: AnyFunction): Promise<number> {
const id = await this.send(method, params, { type, callback });
return id as number;
}
/**
* @summary Allows unsubscribing to subscriptions made with [[subscribe]].
*/
public async unsubscribe (type: string, method: string, id: number): Promise<boolean> {
const subscription = `${type}::${id}`;
// FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
// the assigned id now does not match what the API user originally received. It has
// a slight complication in solving - since we cannot rely on the send id, but rather
// need to find the actual subscription id to map it
if (isUndefined(this.#subscriptions[subscription])) {
l.debug((): string => `Unable to find active subscription=${subscription}`);
return false;
}
delete this.#subscriptions[subscription];
const result = await this.send(method, [id]);
return result as boolean;
}
}