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

Expose subscribe on Accounts interface #42

Merged
merged 5 commits into from
May 31, 2019
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
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ window.injectedWeb3 = {
}
```

When there is more than one extension, each will populate an entry above, so from an extension implementation perspective, the structure should not be overridded. The injected interface, when returned via `enable`, contains the following -
When there is more than one extension, each will populate an entry above, so from an extension implementation perspective, the structure should not be overridded. The `Injected` interface, as returned via `enable`, contains the following -

```js
interface Injected {
Expand All @@ -57,12 +57,17 @@ interface Injected {
// readonly provider: Provider
}

interface Account = {
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
};

// exposes accounts
interface Accounts {
get (): Promise<Array<{
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
}>>;
// retrieves the list of accounts for right now
get (): Promise<Array<Account>>;
// subscribe to all accounts, updating as they change
subscribe (cb: (accounts: Array<Account>) => any): () => void
}

// a signer that communicates with the extension via sendMessage
Expand Down
13 changes: 7 additions & 6 deletions packages/extension-dapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

A basic extractor that manipulates the `window.injectedWeb3` to retrieve all the providers added to the page. It has a number of utilities -

- `web3Enable(dappName: string)` - to be called before anything else, retrieves the list of all injected extensions/providers
- `web3Accounts()` - returns a list of all the injected accounts, accross all extensions (source in meta)
- `web3FromAddress(address: string)` - Retrieves a provider for a specific address
- `web3FromSource(name: string)` - Retriebes a provider identified by the name
- `isWeb3Injected` - boolean to indicate if `injectedWeb3` was found on the page
- `web3EnablePromise` - `null` or the value of the last call to `web3Enable`
- `web3Enable(dappName: string): Promise<Array<InjectedExtension>>` - to be called before anything else, retrieves the list of all injected extensions/providers
- `web3Accounts(): Promise<Array<InjectedAccountWithMeta>>` - returns a list of all the injected accounts, accross all extensions (source in meta)
- `web3AccountsSubscribe(cb: (accounts: Array<InjectedAccountWithMeta>) => any): Promise<Unsubcall>` - subscribes to the accounts accross all extensions, returning a full list as changes are made
- `web3FromAddress(address: string): Promise<InjectedExtension>` - Retrieves a provider for a specific address
- `web3FromSource(name: string): Promise<InjectedExtension>` - Retrieves a provider identified by the name
- `isWeb3Injected: boolean` - boolean to indicate if `injectedWeb3` was found on the page
- `web3EnablePromise: Promise<Array<InjectedExtension>> | null` - `null` or the promise as a result of the last call to `web3Enable`

## Usage

Expand Down
1 change: 1 addition & 0 deletions packages/extension-dapp/src/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import singleSource from './singleSource';

// initialize all the compatibility engines
export default function initCompat (): Promise<boolean> {
return Promise.all([
singleSource()
Expand Down
33 changes: 25 additions & 8 deletions packages/extension-dapp/src/compat/singleSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import { Signer } from '@polkadot/api/types';
import { InjectedAccount, InjectedWindow } from '../types';

// RxJs interface, only what we need here
// RxJs interface, only the bare-bones of what we need here
type Subscriber<T> = {
subscribe: (cb: (value: Array<T>) => void) => void
subscribe: (cb: (value: T) => void) => {
unsubscribe (): any
}
};

type SingleSourceAccount = {
Expand All @@ -17,7 +19,7 @@ type SingleSourceAccount = {
};

type SingleSource = {
accounts$: Subscriber<SingleSourceAccount>,
accounts$: Subscriber<Array<SingleSourceAccount>>,
environment$: Subscriber<string>,
signer: Signer
};
Expand All @@ -26,24 +28,39 @@ type SingleWindow = Window & InjectedWindow & {
SingleSource: SingleSource
};

// transfor the SingleSource accounts into a simple address/name array
function transformAccounts (accounts: Array<SingleSourceAccount>): Array<InjectedAccount> {
return accounts.map(({ address, name }) => ({
address,
name
}));
}

// add a compat interface of SingleSource to window.injectedWeb3
function injectSingleSource (win: SingleWindow): void {
let accounts: Array<InjectedAccount> = [];

// we don't yet have an accounts subscribe on the interface, simply get the
// accounts and store them, any get will resolve the last found values
win.SingleSource.accounts$.subscribe((_accounts) => {
accounts = _accounts.map(({ address, name }) => ({
address,
name
}));
accounts = transformAccounts(_accounts);
});

// decorate the compat interface
win.injectedWeb3['SingleSource'] = {
enable: async (origin: string) => ({
accounts: {
get: async () => accounts
get: async () =>
accounts,
subscribe: (cb: (accounts: Array<InjectedAccount>) => any) => {
const sub = win.SingleSource.accounts$.subscribe((accounts) =>
cb(transformAccounts(accounts))
);

return (): void => {
sub.unsubscribe();
};
}
},
signer: win.SingleSource.signer
}),
Expand Down
52 changes: 44 additions & 8 deletions packages/extension-dapp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedWindow } from './types';
import { InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedWindow, Unsubcall } from './types';

// our extension adaptor for other kinds of extensions
import compatInjector from './compat';
Expand All @@ -18,6 +18,19 @@ function web3IsInjected (): boolean {
return Object.keys(win.injectedWeb3).length !== 0;
}

// helper to throw a consistent error when not enabled
function throwError (method: string): never {
throw new Error(`${method}: web3Enable(originName) needs to be called before ${method}`);
}

// internal helper to map from Array<InjectedAccount> -> Array<InjectedAccountWithMeta>
function mapAccounts (source: string, list: Array<InjectedAccount>): Array<InjectedAccountWithMeta> {
return list.map(({ address, name }) => ({
address,
meta: { name, source }
}));
}

// have we found a properly constructed window.injectedWeb3
let isWeb3Injected = web3IsInjected();

Expand Down Expand Up @@ -56,7 +69,7 @@ export function web3Enable (originName: string): Promise<Array<InjectedExtension
// retrieve all the accounts accross all providers
export async function web3Accounts (): Promise<Array<InjectedAccountWithMeta>> {
if (!web3EnablePromise) {
throw new Error(`web3Accounts: web3Enable(originName) needs to be called before web3Accounts`);
return throwError('web3Accounts');
}

const accounts: Array<InjectedAccountWithMeta> = [];
Expand All @@ -66,10 +79,7 @@ export async function web3Accounts (): Promise<Array<InjectedAccountWithMeta>> {
try {
const list = await accounts.get();

return list.map(({ address, name }) => ({
address,
meta: { name, source }
}));
return mapAccounts(source, list);
} catch (error) {
// cannot handle this one
return [];
Expand All @@ -86,10 +96,36 @@ export async function web3Accounts (): Promise<Array<InjectedAccountWithMeta>> {
return accounts;
}

export async function web3AccountsSubscribe (cb: (accounts: Array<InjectedAccountWithMeta>) => any): Promise<Unsubcall> {
if (!web3EnablePromise) {
return throwError('web3AccountsSubscribe');
}

const accounts: { [source: string]: Array<InjectedAccount> } = {};
const triggerUpdate = (): void => {
cb(Object.entries(accounts).reduce((result, [source, list]) => {
result.push(...mapAccounts(source, list));

return result;
}, [] as Array<InjectedAccountWithMeta>));
};

const unsubs = (await web3EnablePromise).map(({ accounts: { subscribe }, name: source }) =>
subscribe((result) => {
accounts[source] = result;
triggerUpdate();
})
);

return (): void => {
unsubs.forEach((unsub) => unsub());
};
}

// find a specific provider based on an address
export async function web3FromAddress (address: string): Promise<InjectedExtension> {
if (!web3EnablePromise) {
throw new Error(`web3FromAddress: web3Enable(originName) needs to be called before web3FromAddress`);
return throwError('web3FromAddress');
}

const accounts = await web3Accounts();
Expand All @@ -105,7 +141,7 @@ export async function web3FromAddress (address: string): Promise<InjectedExtensi
// find a specific provider based on the name
export async function web3FromSource (source: string): Promise<InjectedExtension> {
if (!web3EnablePromise) {
throw new Error(`web3FromSource: web3Enable(originName) needs to be called before web3FromSource`);
return throwError('web3FromSource');
}

const sources = await web3EnablePromise;
Expand Down
3 changes: 3 additions & 0 deletions packages/extension-dapp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import { Signer } from '@polkadot/api/types';

export type Unsubcall = () => void;

export interface InjectedAccount {
address: string;
name: string;
Expand All @@ -19,6 +21,7 @@ export interface InjectedAccountWithMeta {

export interface InjectedAccounts {
get: () => Promise<Array<InjectedAccount>>;
subscribe: (cb: (accounts: Array<InjectedAccount>) => any) => Unsubcall;
}

export interface InjectedSigner extends Signer {}
Expand Down
39 changes: 31 additions & 8 deletions packages/extension/src/background/handlers/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { Unsubcall } from '@polkadot/extension-dapp/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { MessageTypes, MessageAuthorize, MessageExtrinsicSign, MessageExtrinsicSign$Response } from '../types';

import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert } from '@polkadot/util';
import { assert, isFunction } from '@polkadot/util';

import State from './State';

type Accounts = Array<{ address: string, name?: string }>;

function transformAccounts (accounts: SubjectInfo): Accounts {
return Object.values(accounts).map(({ json: { address, meta: { name } } }) => ({
address, name
}));
}

export default class Tabs {
private _state: State;

Expand All @@ -21,12 +31,22 @@ export default class Tabs {
return this._state.authorizeUrl(url, request);
}

private accountsList (url: string): Array<{ address: string, name?: string }> {
return Object
.values(accountsObservable.subject.getValue())
.map(({ json: { address, meta: { name } } }) => ({
address, name
}));
private accountsList (url: string): Accounts {
return transformAccounts(accountsObservable.subject.getValue());
}

private accountsSubscribe (url: string, cb?: (accounts: Accounts) => void): Unsubcall {
if (!isFunction(cb)) {
throw new Error('Expected accountsSubscribe to be passed a subscriber');
}

const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo) =>
cb(transformAccounts(accounts))
);

return (): void => {
subscription.unsubscribe();
};
}

private extrinsicSign (url: string, request: MessageExtrinsicSign): Promise<MessageExtrinsicSign$Response> {
Expand All @@ -38,14 +58,17 @@ export default class Tabs {
return this._state.signQueue(url, request);
}

async handle (type: MessageTypes, request: any, url: string = 'unknown'): Promise<any> {
async handle (type: MessageTypes, request: any, url: string, subscriber?: (data: any) => void): Promise<any> {
switch (type) {
case 'authorize.tab':
return this.authorize(url, request);

case 'accounts.list':
return this.accountsList(url);

case 'accounts.subscribe':
return this.accountsSubscribe(url, subscriber);

case 'extrinsic.sign':
return this.extrinsicSign(url, request);

Expand Down
36 changes: 26 additions & 10 deletions packages/extension/src/background/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,54 @@ import Extension from './Extension';
import State from './State';
import Tabs from './Tabs';

const FALLBACK_URL = '<unknown>';

const state = new State();
const extension = new Extension(state);
const tabs = new Tabs(state);

export default function handler ({ id, message, request }: MessageRequest, port: chrome.runtime.Port): boolean {
function createSubscription (id: number, port: chrome.runtime.Port): (data: any) => void {
// FIXME We are not handling actual unsubscribes yet, this is an issue
return (subscription: any) => {
port.postMessage({ id, subscription });
};
}

export default function handler ({ id, message, request }: MessageRequest, port: chrome.runtime.Port): void {
const isPopup = port.name === PORT_POPUP;
const sender = port.sender as chrome.runtime.MessageSender;
const from = isPopup
? 'popup'
: sender.tab
? sender.tab.url
: '<unknown>';
const source = `${from}: ${id}: ${message}`;
: sender.tab && sender.tab.url;
const source = `${from || FALLBACK_URL}: ${id}: ${message}`;

console.log(` [in] ${source}`); // :: ${JSON.stringify(request)}`);

// This is not great - basically, based on the name (since there is only 1 atm),
// we create a subscription handler. Basically these handlers will continute stream
// results as they become available
const subscription = message.indexOf('.subscribe') !== -1
? createSubscription(id, port)
: undefined;
const promise = isPopup
? extension.handle(message, request)
: tabs.handle(message, request, from);
: tabs.handle(message, request, from || FALLBACK_URL, subscription);

promise
.then((response) => {
console.log(`[out] ${source}`); // :: ${JSON.stringify(response)}`);

port.postMessage({ id, response });
if (subscription) {
// TODO See unsub handling above, here we need to store the
// actual unsubscribe and handle appropriately
port.postMessage({ id, response: true });
} else {
port.postMessage({ id, response });
}
})
.catch((error) => {
console.log(`[err] ${source}:: ${error.message}`);

port.postMessage({ id, error: error.message });
});

// return true to indicate we are sending the response async
return true;
}
5 changes: 3 additions & 2 deletions packages/extension/src/background/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { KeypairType } from '@polkadot/util-crypto/types';

export type MessageTypes = 'authorize.approve' | 'authorize.reject' | 'authorize.requests' | 'authorize.tab' | 'accounts.create' | 'accounts.edit' | 'accounts.forget' | 'accounts.list' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests';
export type MessageTypes = 'authorize.approve' | 'authorize.reject' | 'authorize.requests' | 'authorize.tab' | 'accounts.create' | 'accounts.edit' | 'accounts.forget' | 'accounts.list' | 'accounts.subscribe' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests';

export type AuthorizeRequest = [number, MessageAuthorize, string];

Expand All @@ -31,7 +31,8 @@ export type MessageRequest = {
export type MessageResponse = {
error?: string,
id: number,
response?: any
response?: any,
subscription?: any
};

export type MessageAccountCreate = {
Expand Down
Loading