Add typings to messages (#130)

* Add typings to messages

* Fix eslint (after merge)

* Fix TypeScript error

* Lint

* Consistent naming

* Better promise error handling

* Constraint handle typings as much as tsc will allow

* Revert to empty Props interfaces

* SeedLengths type

* Add type to transport subscription message

* nit

* Typecheck passing callback to sendMessage when none expected

* Type subscription cb and replace KeyringJson w/ InjectedAccount

Use InjectedAccount for `accounts.list`, `accounts.subscribe`
This commit is contained in:
Axel Chalon 2019-09-05 16:11:01 +02:00 committed by Jaco Greeff
parent 25026b8ec4
commit 251417b50e
19 changed files with 266 additions and 169 deletions

View File

@ -66,12 +66,12 @@ When there is more than one extension, each will populate an entry via the injec
```js
interface Injected {
// the interface for Accounts, as detailed below
read-only accounts: Accounts;
readonly accounts: Accounts;
// the standard Signer interface for the API, as detailed below
read-only signer: Signer;
readonly signer: Signer;
// not injected as of yet, subscribable provider for polkadot-js API injection,
// this can be passed to the API itself upon construction in the dapp
// read-only provider: Provider
// readonly provider?: Provider
}
interface Account = {

View File

@ -11,13 +11,13 @@ export type Unsubcall = () => void;
export interface InjectedAccount {
address: string;
name: string;
name?: string;
}
export interface InjectedAccountWithMeta {
address: string;
meta: {
name: string;
name?: string;
source: string;
};
}
@ -27,7 +27,6 @@ export interface InjectedAccounts {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe: (cb: (accounts: InjectedAccount[]) => any) => Unsubcall;
}
export interface InjectedExtensionInfo {
name: string;
version: string;

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 { MessageAuthorize } from '@polkadot/extension/background/types';
import { RequestAuthorizeTab } from '@polkadot/extension/background/types';
import React, { useContext } from 'react';
import styled from 'styled-components';
@ -14,7 +14,7 @@ interface Props {
authId: string;
className?: string;
isFirst: boolean;
request: MessageAuthorize;
request: RequestAuthorizeTab;
url: string;
}

View File

@ -8,7 +8,7 @@ import { ActionContext, Address, Button, Header, Loading, TextArea } from '../co
import { createAccount, createSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {};
type Props = {}
export default function Create (): React.ReactElement<Props> {
const onAction = useContext(ActionContext);

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 { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { RequestExtrinsicSign } from '@polkadot/extension/background/types';
import React, { useContext } from 'react';
import { createType } from '@polkadot/types';
@ -14,7 +14,7 @@ import Unlock from './Unlock';
interface Props {
isFirst: boolean;
request: MessageExtrinsicSign;
request: RequestExtrinsicSign;
signId: string;
url: string;
}

View File

@ -3,7 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AuthorizeRequest, SigningRequest } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { Prefix } from '@polkadot/util-crypto/address/types';
import React, { useEffect, useState } from 'react';
@ -31,7 +31,7 @@ const { prefix } = settings.get();
setAddressPrefix((prefix === -1 ? 42 : prefix) as Prefix);
export default function Popup (): React.ReactElement<{}> {
const [accounts, setAccounts] = useState<null | KeyringJson[]>(null);
const [accounts, setAccounts] = useState<null | InjectedAccount[]>(null);
const [authRequests, setAuthRequests] = useState<null | AuthorizeRequest[]>(null);
const [signRequests, setSignRequests] = useState<null | SigningRequest[]>(null);
const [isWelcomeDone, setWelcomeDone] = useState(false);

View File

@ -2,17 +2,17 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { Prefix } from '@polkadot/util-crypto/address/types';
import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import Identicon from '@polkadot/react-identicon';
import settings from '@polkadot/ui-settings';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import IconBox from './IconBox';
import { AccountContext } from './contexts';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
interface Props {
address?: string | null;
@ -24,7 +24,7 @@ interface Props {
function Address ({ address, children, className, name, theme = 'polkadot' }: Props): React.ReactElement<Props> {
const accounts = useContext(AccountContext);
const [account, setAccount] = useState<KeyringJson | null>(null);
const [account, setAccount] = useState<InjectedAccount | null>(null);
const [formatted, setFormatted] = useState<string | null>(null);
useEffect((): void => {
@ -58,7 +58,7 @@ function Address ({ address, children, className, name, theme = 'polkadot' }: Pr
}
intro={
<>
<div className='name'>{name || (account && account.meta.name) || '<unknown>'}</div>
<div className='name'>{name || (account && account.name) || '<unknown>'}</div>
<div className='address'>{formatted || '<unknown>'}</div>
</>
}

View File

@ -3,12 +3,12 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AuthorizeRequest, SigningRequest } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { InjectedAccount } from '@polkadot/extension-inject/types';
export type OmitProps<T, K> = Pick<T, Exclude<keyof T, K>>;
export type SubtractProps<T, K> = OmitProps<T, keyof K>;
export type AccountsFromCtx = KeyringJson[];
export type AccountsFromCtx = InjectedAccount[];
export type OnActionFromCtx = (to?: string) => void;
export type AuthRequestsFromCtx = AuthorizeRequest[];
export type SignRequestsFromCtx = SigningRequest[];

View File

@ -2,12 +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 { AuthorizeRequest, MessageTypes, SigningRequest } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '@polkadot/extension/background/types';
import { KeypairType } from '@polkadot/util-crypto/types';
import extension from 'extensionizer';
import { PORT_POPUP } from '@polkadot/extension/defaults';
import { InjectedAccount } from '@polkadot/extension-inject/types';
interface Handler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -45,14 +45,17 @@ port.onMessage.addListener((data): void => {
}
});
function sendMessage<TMessageType extends MessageTypesWithNullRequest>(message: TMessageType): Promise<ResponseTypes[TMessageType]>;
function sendMessage<TMessageType extends MessageTypesWithNoSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType]): Promise<ResponseTypes[TMessageType]>;
function sendMessage<TMessageType extends MessageTypesWithSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise<ResponseTypes[TMessageType]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function sendMessage (message: MessageTypes, request: any = {}, subscriber?: (data: any) => void): Promise<any> {
function sendMessage<TMessageType extends MessageTypes> (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: any) => void): Promise<ResponseTypes[TMessageType]> {
return new Promise((resolve, reject): void => {
const id = `${Date.now()}.${++idCounter}`;
handlers[id] = { resolve, reject, subscriber };
port.postMessage({ id, message, request });
port.postMessage({ id, message, request: request || {} });
});
}
@ -64,7 +67,7 @@ export async function forgetAccount (address: string): Promise<boolean> {
return sendMessage('accounts.forget', { address });
}
export async function getAccounts (): Promise<KeyringJson[]> {
export async function getAccounts (): Promise<InjectedAccount[]> {
return sendMessage('accounts.list');
}
@ -96,20 +99,20 @@ export async function createAccount (name: string, password: string, suri: strin
return sendMessage('accounts.create', { name, password, suri, type });
}
export async function createSeed (length?: number, type?: KeypairType): Promise<{ address: string; seed: string }> {
export async function createSeed (length?: SeedLengths, type?: KeypairType): Promise<{ address: string; seed: string }> {
return sendMessage('seed.create', { length, type });
}
export async function subscribeAccounts (cb: (accounts: KeyringJson[]) => void): Promise<boolean> {
return sendMessage('accounts.subscribe', {}, cb);
export async function subscribeAccounts (cb: (accounts: InjectedAccount[]) => void): Promise<boolean> {
return sendMessage('accounts.subscribe', null, cb);
}
export async function subscribeAuthorize (cb: (accounts: AuthorizeRequest[]) => void): Promise<boolean> {
return sendMessage('authorize.subscribe', {}, cb);
return sendMessage('authorize.subscribe', null, cb);
}
export async function subscribeSigning (cb: (accounts: SigningRequest[]) => void): Promise<boolean> {
return sendMessage('signing.subscribe', {}, cb);
return sendMessage('signing.subscribe', null, cb);
}
export async function validateSeed (suri: string, type?: KeypairType): Promise<{ address: string; suri: string }> {

View File

@ -22,7 +22,7 @@ export default function Name ({ address, className, defaultValue, isFocussed, la
const accounts = useContext(AccountContext);
const [name, setName] = useState('');
const account = accounts.find((account): boolean => account.address === address);
const startValue = (account && account.meta.name) || defaultValue;
const startValue = (account && account.name) || defaultValue;
const isError = !name && startValue
? false
: (name.length < MIN_LENGTH);

View File

@ -3,8 +3,8 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { AuthorizeRequest, MessageTypes, MessageAccountCreate, MessageAccountEdit, MessageAuthorizeApprove, MessageAuthorizeReject, MessageExtrinsicSignApprove, MessageExtrinsicSignCancel, MessageSeedCreate, MessageSeedCreateResponse, MessageSeedValidate, MessageSeedValidateResponse, MessageAccountForget, SigningRequest } from '../types';
import { AuthorizeRequest, RequestAccountCreate, RequestAccountEdit, RequestAuthorizeApprove, RequestAuthorizeReject, RequestSigningApprove, RequestSigningCancel, RequestSeedCreate, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, RequestAccountForget, SigningRequest, RequestTypes, ResponseTypes, MessageTypes } from '../types';
import { InjectedAccount } from '@polkadot/extension-inject/types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
@ -18,8 +18,8 @@ import { createSubscription, unsubscribe } from './subscriptions';
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 24];
function transformAccounts (accounts: SubjectInfo): KeyringJson[] {
return Object.values(accounts).map(({ json }): KeyringJson => json);
function transformAccounts (accounts: SubjectInfo): InjectedAccount[] {
return Object.values(accounts).map(({ json }): InjectedAccount => ({ ...json, name: json.meta && json.meta.name }));
}
export default class Extension {
@ -29,13 +29,13 @@ export default class Extension {
this.state = state;
}
private accountsCreate ({ name, password, suri, type }: MessageAccountCreate): boolean {
private accountsCreate ({ name, password, suri, type }: RequestAccountCreate): boolean {
keyring.addUri(suri, password, { name }, type);
return true;
}
private accountsEdit ({ address, name }: MessageAccountEdit): boolean {
private accountsEdit ({ address, name }: RequestAccountEdit): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
@ -45,19 +45,19 @@ export default class Extension {
return true;
}
private accountsForget ({ address }: MessageAccountForget): boolean {
private accountsForget ({ address }: RequestAccountForget): boolean {
keyring.forgetAccount(address);
return true;
}
private accountsList (): KeyringJson[] {
private accountsList (): InjectedAccount[] {
return transformAccounts(accountsObservable.subject.getValue());
}
// FIXME This looks very much like what we have in Tabs
private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const cb = createSubscription<'accounts.subscribe'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void =>
cb(transformAccounts(accounts))
);
@ -70,7 +70,7 @@ export default class Extension {
return true;
}
private authorizeApprove ({ id }: MessageAuthorizeApprove): boolean {
private authorizeApprove ({ id }: RequestAuthorizeApprove): boolean {
const queued = this.state.getAuthRequest(id);
assert(queued, 'Unable to find request');
@ -82,7 +82,7 @@ export default class Extension {
return true;
}
private authorizeReject ({ id }: MessageAuthorizeReject): boolean {
private authorizeReject ({ id }: RequestAuthorizeReject): boolean {
const queued = this.state.getAuthRequest(id);
assert(queued, 'Unable to find request');
@ -100,7 +100,7 @@ export default class Extension {
// FIXME This looks very much like what we have in accounts
private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const cb = createSubscription<'authorize.subscribe'>(id, port);
const subscription = this.state.authSubject.subscribe((requests: AuthorizeRequest[]): void =>
cb(requests)
);
@ -113,7 +113,7 @@ export default class Extension {
return true;
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: MessageSeedCreate): MessageSeedCreateResponse {
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate {
const seed = mnemonicGenerate(length);
return {
@ -122,7 +122,7 @@ export default class Extension {
};
}
private seedValidate ({ suri, type }: MessageSeedValidate): MessageSeedValidateResponse {
private seedValidate ({ suri, type }: RequestSeedValidate): ResponseSeedValidate {
const { phrase } = keyExtractSuri(suri);
assert(SEED_LENGTHS.includes(phrase.split(' ').length), `Mnemonic needs to contain ${SEED_LENGTHS.join(', ')} words`);
@ -134,7 +134,7 @@ export default class Extension {
};
}
private signingApprove ({ id, password }: MessageExtrinsicSignApprove): boolean {
private signingApprove ({ id, password }: RequestSigningApprove): boolean {
const queued = this.state.getSignRequest(id);
assert(queued, 'Unable to find request');
@ -163,7 +163,7 @@ export default class Extension {
return true;
}
private signingCancel ({ id }: MessageExtrinsicSignCancel): boolean {
private signingCancel ({ id }: RequestSigningCancel): boolean {
const queued = this.state.getSignRequest(id);
assert(queued, 'Unable to find request');
@ -181,7 +181,7 @@ export default class Extension {
// FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const cb = createSubscription<'signing.subscribe'>(id, port);
const subscription = this.state.signSubject.subscribe((requests: SigningRequest[]): void =>
cb(requests)
);
@ -194,56 +194,57 @@ export default class Extension {
return true;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/require-await
public async handle (id: string, type: MessageTypes, request: any, port: chrome.runtime.Port): Promise<any> {
switch (type) {
case 'authorize.approve':
return this.authorizeApprove(request);
public handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
return new Promise((resolve, reject): void => {
switch (type) {
case 'authorize.approve':
return resolve(this.authorizeApprove(request as RequestAuthorizeApprove));
case 'authorize.reject':
return this.authorizeReject(request);
case 'authorize.reject':
return resolve(this.authorizeReject(request as RequestAuthorizeApprove));
case 'authorize.requests':
return this.authorizeRequests();
case 'authorize.requests':
return resolve(this.authorizeRequests());
case 'authorize.subscribe':
return this.authorizeSubscribe(id, port);
case 'authorize.subscribe':
return resolve(this.authorizeSubscribe(id, port));
case 'accounts.create':
return this.accountsCreate(request);
case 'accounts.create':
return resolve(this.accountsCreate(request as RequestAccountCreate));
case 'accounts.forget':
return this.accountsForget(request);
case 'accounts.forget':
return resolve(this.accountsForget(request as RequestAccountForget));
case 'accounts.edit':
return this.accountsEdit(request);
case 'accounts.edit':
return resolve(this.accountsEdit(request as RequestAccountEdit));
case 'accounts.list':
return this.accountsList();
case 'accounts.list':
return resolve(this.accountsList());
case 'accounts.subscribe':
return this.accountsSubscribe(id, port);
case 'accounts.subscribe':
return resolve(this.accountsSubscribe(id, port));
case 'seed.create':
return this.seedCreate(request);
case 'seed.create':
return resolve(this.seedCreate(request as RequestSeedCreate));
case 'seed.validate':
return this.seedValidate(request);
case 'seed.validate':
return resolve(this.seedValidate(request as RequestSeedValidate));
case 'signing.approve':
return this.signingApprove(request);
case 'signing.approve':
return resolve(this.signingApprove(request as RequestSigningApprove));
case 'signing.cancel':
return this.signingCancel(request);
case 'signing.cancel':
return resolve(this.signingCancel(request as RequestSigningCancel));
case 'signing.requests':
return this.signingRequests();
case 'signing.requests':
return resolve(this.signingRequests());
case 'signing.subscribe':
return this.signingSubscribe(id, port);
case 'signing.subscribe':
return resolve(this.signingSubscribe(id, port));
default:
throw new Error(`Unable to handle message of type ${type}`);
}
default:
return reject(new Error(`Unable to handle message of type ${type}`));
}
});
}
}

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 { AuthorizeRequest, MessageAuthorize, MessageExtrinsicSign, MessageExtrinsicSignResponse, SigningRequest } from '../types';
import { AuthorizeRequest, RequestAuthorizeTab, RequestExtrinsicSign, ResponseExtrinsicSign, SigningRequest } from '../types';
import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs';
@ -11,7 +11,7 @@ import { assert } from '@polkadot/util';
interface AuthRequest {
id: string;
idStr: string;
request: MessageAuthorize;
request: RequestAuthorizeTab;
resolve: (result: boolean) => void;
reject: (error: Error) => void;
url: string;
@ -27,8 +27,8 @@ type AuthUrls = Record<string, {
interface SignRequest {
id: string;
request: MessageExtrinsicSign;
resolve: (result: MessageExtrinsicSignResponse) => void;
request: RequestExtrinsicSign;
resolve: (result: ResponseExtrinsicSign) => void;
reject: (error: Error) => void;
url: string;
}
@ -124,8 +124,8 @@ export default class State {
};
}
private signComplete = (id: string, fn: Function): (result: MessageExtrinsicSignResponse | Error) => void => {
return (result: MessageExtrinsicSignResponse | Error): void => {
private signComplete = (id: string, fn: Function): (result: ResponseExtrinsicSign | Error) => void => {
return (result: ResponseExtrinsicSign | Error): void => {
delete this._signRequests[id];
this.updateIconSign(true);
@ -167,7 +167,7 @@ export default class State {
this.updateIcon(shouldClose);
}
public async authorizeUrl (url: string, request: MessageAuthorize): Promise<boolean> {
public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise<boolean> {
const idStr = this.stripUrl(url);
if (this._authUrls[idStr]) {
@ -210,7 +210,7 @@ export default class State {
return this._signRequests[id];
}
public signQueue (url: string, request: MessageExtrinsicSign): Promise<MessageExtrinsicSignResponse> {
public signQueue (url: string, request: RequestExtrinsicSign): Promise<ResponseExtrinsicSign> {
const id = getId();
return new Promise((resolve, reject): void => {

View File

@ -2,8 +2,9 @@
// 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 { RequestAuthorizeTab, RequestExtrinsicSign, ResponseExtrinsicSign, RequestTypes, ResponseTypes, MessageTypes } from '../types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { MessageTypes, MessageAuthorize, MessageExtrinsicSign, MessageExtrinsicSignResponse } from '../types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
@ -12,14 +13,8 @@ import { assert } from '@polkadot/util';
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
interface Account {
address: string;
name?: string;
}
type Accounts = Account[];
function transformAccounts (accounts: SubjectInfo): Accounts {
return Object.values(accounts).map(({ json: { address, meta: { name } } }): Account => ({
function transformAccounts (accounts: SubjectInfo): InjectedAccount[] {
return Object.values(accounts).map(({ json: { address, meta: { name } } }): InjectedAccount => ({
address, name
}));
}
@ -31,18 +26,18 @@ export default class Tabs {
this.state = state;
}
private authorize (url: string, request: MessageAuthorize): Promise<boolean> {
private authorize (url: string, request: RequestAuthorizeTab): Promise<boolean> {
return this.state.authorizeUrl(url, request);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private accountsList (url: string): Accounts {
private accountsList (url: string): InjectedAccount[] {
return transformAccounts(accountsObservable.subject.getValue());
}
// FIXME This looks very much like what we have in Extension
private accountsSubscribe (url: string, id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const cb = createSubscription<'accounts.subscribe'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void =>
cb(transformAccounts(accounts))
);
@ -55,7 +50,7 @@ export default class Tabs {
return true;
}
private extrinsicSign (url: string, request: MessageExtrinsicSign): Promise<MessageExtrinsicSignResponse> {
private extrinsicSign (url: string, request: RequestExtrinsicSign): Promise<ResponseExtrinsicSign> {
const { address } = request;
const pair = keyring.getPair(address);
@ -64,15 +59,14 @@ export default class Tabs {
return this.state.signQueue(url, request);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async handle (id: string, type: MessageTypes, request: any, url: string, port: chrome.runtime.Port): Promise<any> {
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 !== 'authorize.tab') {
this.state.ensureUrlAuthorized(url);
}
switch (type) {
case 'authorize.tab':
return this.authorize(url, request);
return this.authorize(url, request as RequestAuthorizeTab);
case 'accounts.list':
return this.accountsList(url);
@ -81,7 +75,7 @@ export default class Tabs {
return this.accountsSubscribe(url, id, port);
case 'extrinsic.sign':
return this.extrinsicSign(url, request);
return this.extrinsicSign(url, request as RequestExtrinsicSign);
default:
throw new Error(`Unable to handle message of type ${type}`);

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 { MessageRequest } from '../types';
import { TransportRequestMessage, MessageTypes } from '../types';
import { PORT_POPUP } from '../../defaults';
import Extension from './Extension';
@ -13,7 +13,7 @@ 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): void {
export default function handler<TMessageType extends MessageTypes> ({ id, message, request }: TransportRequestMessage<TMessageType>, port: chrome.runtime.Port): void {
const isPopup = port.name === PORT_POPUP;
const sender = port.sender as chrome.runtime.MessageSender;
const from = isPopup

View File

@ -2,13 +2,14 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubscriptionMessageTypes, MessageTypesWithSubscriptions } from '../types';
type Subscriptions = Record<string, chrome.runtime.Port>;
const subscriptions: Subscriptions = {};
// return a subscription callback, that will send the data to the caller via the port
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createSubscription (id: string, port: chrome.runtime.Port): (data: any) => void {
export function createSubscription<TMessageType extends MessageTypesWithSubscriptions> (id: string, port: chrome.runtime.Port): (data: SubscriptionMessageTypes[TMessageType]) => void {
subscriptions[id] = port;
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -2,91 +2,171 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '@polkadot/util-crypto/types';
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { SignerPayloadJSON } from '@polkadot/types/types';
import { KeypairType } from '@polkadot/util-crypto/types';
export type MessageTypes = 'authorize.approve' | 'authorize.reject' | 'authorize.requests' | 'authorize.subscribe' | 'authorize.tab' | 'accounts.create' | 'accounts.edit' | 'accounts.forget' | 'accounts.list' | 'accounts.subscribe' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests' | 'signing.subscribe';
type KeysWithDefinedValues<T> = {
[K in keyof T]: T[K] extends undefined ? never : K
}[keyof T];
export type AuthorizeRequest = [string, MessageAuthorize, string];
type NoUndefinedValues<T> = {
[K in KeysWithDefinedValues<T>]: T[K]
};
export type SigningRequest = [string, MessageExtrinsicSign, string];
type IsNull<T, K extends keyof T> = { [K1 in Exclude<keyof T, K>]: T[K1] } & T[K] extends null ? K : never
type NullKeys<T> = { [K in keyof T]: IsNull<T, K> }[keyof T]
export interface MessageAuthorize {
export type SeedLengths = 12 | 24;
export type AuthorizeRequest = [string, RequestAuthorizeTab, string];
export type SigningRequest = [string, RequestExtrinsicSign, string];
// [MessageType]: [RequestType, ResponseType, SubscriptionMessageType?]
export interface RequestSignatures {
'accounts.create': [RequestAccountCreate, boolean];
'accounts.edit': [RequestAccountEdit, boolean];
'accounts.forget': [RequestAccountForget, boolean];
'accounts.list': [RequestAccountList, InjectedAccount[]];
'accounts.subscribe': [RequestAccountSubscribe, boolean, InjectedAccount[]];
'authorize.tab': [RequestAuthorizeTab, null];
'authorize.approve': [RequestAuthorizeApprove, boolean];
'authorize.reject': [RequestAuthorizeReject, boolean];
'authorize.requests': [RequestAuthorizeRequests, AuthorizeRequest[]];
'authorize.subscribe': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
'extrinsic.sign': [RequestExtrinsicSign, ResponseExtrinsicSign];
'seed.create': [RequestSeedCreate, ResponseSeedCreate];
'seed.validate': [RequestSeedValidate, ResponseSeedValidate];
'signing.approve': [RequestSigningApprove, boolean];
'signing.cancel': [RequestSigningCancel, boolean];
'signing.requests': [RequestSigningRequests, SigningRequest[]];
'signing.subscribe': [RequestSigningSubscribe, boolean, SigningRequest[]];
}
export type MessageTypes = keyof RequestSignatures;
// Requests
export type RequestTypes = {
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][0]
};
export type MessageTypesWithNullRequest = NullKeys<RequestTypes>
export interface TransportRequestMessage<TMessageType extends MessageTypes> {
id: string;
message: TMessageType;
origin: 'page' | 'popup';
request: RequestTypes[TMessageType];
}
export interface RequestAuthorizeTab {
origin: string;
}
export interface MessageAuthorizeApprove {
export interface RequestAuthorizeApprove {
id: string;
}
export interface MessageAuthorizeReject {
export interface RequestAuthorizeReject {
id: string;
}
export interface MessageRequest {
id: string;
message: MessageTypes;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request: any;
}
export type RequestAuthorizeRequests = null;
export interface MessageResponse {
error?: string;
id: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscription?: any;
}
export type RequestAuthorizeSubscribe = null;
export interface MessageAccountCreate {
export interface RequestAccountCreate {
name: string;
password: string;
suri: string;
type?: KeypairType;
}
export interface MessageAccountEdit {
export interface RequestAccountEdit {
address: string;
name: string;
}
export interface MessageAccountForget {
export interface RequestAccountForget {
address: string;
}
export interface MessageExtrinsicSignApprove {
export type RequestAccountList = null;
export type RequestAccountSubscribe = null;
export type RequestExtrinsicSign = SignerPayloadJSON;
export interface RequestSigningApprove {
id: string;
password: string;
}
export interface MessageExtrinsicSignCancel {
export interface RequestSigningCancel {
id: string;
}
export type MessageExtrinsicSign = SignerPayloadJSON;
export type RequestSigningRequests = null;
export interface MessageExtrinsicSignResponse {
export type RequestSigningSubscribe = null;
export interface RequestSeedCreate {
length?: SeedLengths;
type?: KeypairType;
}
export interface RequestSeedValidate {
suri: string;
type?: KeypairType;
}
// Responses
export type ResponseTypes = {
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][1]
};
interface TransportResponseMessageSub<TMessageType extends MessageTypesWithSubscriptions> {
error?: string;
id: string;
response?: ResponseTypes[TMessageType];
subscription?: SubscriptionMessageTypes[TMessageType];
}
interface TransportResponseMessageNoSub<TMessageType extends MessageTypesWithNoSubscriptions> {
error?: string;
id: string;
response?: ResponseTypes[TMessageType];
}
export type TransportResponseMessage<TMessageType extends MessageTypes> =
TMessageType extends MessageTypesWithNoSubscriptions
? TransportResponseMessageNoSub<TMessageType>
: TMessageType extends MessageTypesWithSubscriptions
? TransportResponseMessageSub<TMessageType>
: never;
export interface ResponseExtrinsicSign {
id: string;
signature: string;
}
export interface MessageSeedCreateResponse {
export interface ResponseSeedCreate {
address: string;
seed: string;
}
export interface MessageSeedCreate {
length?: 12 | 24;
type?: KeypairType;
}
export interface MessageSeedValidate {
suri: string;
type?: KeypairType;
}
export interface MessageSeedValidateResponse {
export interface ResponseSeedValidate {
address: string;
suri: string;
}
// Subscriptions
export type SubscriptionMessageTypes = NoUndefinedValues<{
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][2]
}>;
export type MessageTypesWithSubscriptions = keyof SubscriptionMessageTypes;
export type MessageTypesWithNoSubscriptions = Exclude<MessageTypes, keyof SubscriptionMessageTypes>

View File

@ -2,13 +2,13 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Injected as InjectedInjected } from '@polkadot/extension-inject/types';
import { Injected } from '@polkadot/extension-inject/types';
import { SendRequest } from './types';
import Accounts from './Accounts';
import Signer from './Signer';
export default class Injected implements InjectedInjected {
export default class implements Injected {
public readonly accounts: Accounts;
public readonly signer: Signer;

View File

@ -2,11 +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 { MessageTypes } from '../background/types';
import { injectExtension } from '@polkadot/extension-inject';
import Injected from './Injected';
import { ResponseTypes, TransportRequestMessage, TransportResponseMessage, MessageTypes, RequestTypes, MessageTypesWithNullRequest, SubscriptionMessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '../background/types';
// when sending a message from the injector to the extension, we
// - create an event - this we send to the loader
@ -30,14 +29,24 @@ let idCounter = 0;
// a generic message sender that creates an event, returning a promise that will
// resolve once the event is resolved (by the response listener just below this)
function sendMessage<TMessageType extends MessageTypesWithNullRequest>(message: TMessageType): Promise<ResponseTypes[TMessageType]>;
function sendMessage<TMessageType extends MessageTypesWithNoSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType]): Promise<ResponseTypes[TMessageType]>;
function sendMessage<TMessageType extends MessageTypesWithSubscriptions> (message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise<ResponseTypes[TMessageType]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function sendMessage (message: MessageTypes, request: any = null, subscriber?: (data: any) => void): Promise<any> {
function sendMessage<TMessageType extends MessageTypes> (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: any) => void): Promise<ResponseTypes[TMessageType]> {
return new Promise((resolve, reject): void => {
const id = `${Date.now()}.${++idCounter}`;
handlers[id] = { resolve, reject, subscriber };
window.postMessage({ id, message, origin: 'page', request }, '*');
const transportRequestMessage: TransportRequestMessage<TMessageType> = {
id,
message,
origin: 'page',
request: request || null as RequestTypes[TMessageType]
};
window.postMessage(transportRequestMessage, '*');
});
}
@ -48,17 +57,12 @@ async function enable (origin: string): Promise<Injected> {
return new Injected(sendMessage);
}
// setup a response listener (events created by the loader for extension responses)
window.addEventListener('message', ({ data, source }): void => {
// only allow messages from our window, by the loader
if (source !== window || data.origin !== 'content') {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleResponse<TMessageType extends MessageTypes> (data: TransportResponseMessage<TMessageType> & {subscription?: any}): void {
const handler = handlers[data.id];
if (!handler) {
console.error(`Uknown response: ${JSON.stringify(data)}`);
console.error(`Unknown response: ${JSON.stringify(data)}`);
return;
}
@ -73,6 +77,20 @@ window.addEventListener('message', ({ data, source }): void => {
} else {
handler.resolve(data.response);
}
}
// setup a response listener (events created by the loader for extension responses)
window.addEventListener('message', ({ data, source }): void => {
// only allow messages from our window, by the loader
if (source !== window || data.origin !== 'content') {
return;
}
if (data.id) {
handleResponse(data);
} else {
console.error('Missing id for response.');
}
});
injectExtension(enable, {

View File

@ -2,9 +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 { MessageTypes } from '../background/types';
import { RequestTypes, ResponseTypes, SubscriptionMessageTypes, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '../background/types';
export interface SendRequest {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: MessageTypes, request?: any, subscriber?: (data: any) => any): Promise<any>;
<TMessageType extends MessageTypesWithNullRequest>(message: TMessageType): Promise<ResponseTypes[TMessageType]>;
<TMessageType extends MessageTypesWithNoSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType]): Promise<ResponseTypes[TMessageType]>;
<TMessageType extends MessageTypesWithSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise<ResponseTypes[TMessageType]>;
}