diff --git a/README.md b/README.md index 59be4ddb..1811d322 100644 --- a/README.md +++ b/README.md @@ -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 = { diff --git a/packages/extension-inject/src/types.ts b/packages/extension-inject/src/types.ts index 4ce5b3ef..c54ccc17 100644 --- a/packages/extension-inject/src/types.ts +++ b/packages/extension-inject/src/types.ts @@ -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; diff --git a/packages/extension-ui/src/Popup/Authorize/Request.tsx b/packages/extension-ui/src/Popup/Authorize/Request.tsx index 94786d08..669d3fa4 100644 --- a/packages/extension-ui/src/Popup/Authorize/Request.tsx +++ b/packages/extension-ui/src/Popup/Authorize/Request.tsx @@ -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; } diff --git a/packages/extension-ui/src/Popup/Create.tsx b/packages/extension-ui/src/Popup/Create.tsx index e0180744..8bd947f3 100644 --- a/packages/extension-ui/src/Popup/Create.tsx +++ b/packages/extension-ui/src/Popup/Create.tsx @@ -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 { const onAction = useContext(ActionContext); diff --git a/packages/extension-ui/src/Popup/Signing/Request.tsx b/packages/extension-ui/src/Popup/Signing/Request.tsx index dfdd1a94..2fef2514 100644 --- a/packages/extension-ui/src/Popup/Signing/Request.tsx +++ b/packages/extension-ui/src/Popup/Signing/Request.tsx @@ -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; } diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index 6994afa0..491eedd3 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -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); + const [accounts, setAccounts] = useState(null); const [authRequests, setAuthRequests] = useState(null); const [signRequests, setSignRequests] = useState(null); const [isWelcomeDone, setWelcomeDone] = useState(false); diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index 368db0c8..2cbe5ff0 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -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 { const accounts = useContext(AccountContext); - const [account, setAccount] = useState(null); + const [account, setAccount] = useState(null); const [formatted, setFormatted] = useState(null); useEffect((): void => { @@ -58,7 +58,7 @@ function Address ({ address, children, className, name, theme = 'polkadot' }: Pr } intro={ <> -
{name || (account && account.meta.name) || ''}
+
{name || (account && account.name) || ''}
{formatted || ''}
} diff --git a/packages/extension-ui/src/components/types.ts b/packages/extension-ui/src/components/types.ts index 699d4d4d..4cceff98 100644 --- a/packages/extension-ui/src/components/types.ts +++ b/packages/extension-ui/src/components/types.ts @@ -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 = Pick>; export type SubtractProps = OmitProps; -export type AccountsFromCtx = KeyringJson[]; +export type AccountsFromCtx = InjectedAccount[]; export type OnActionFromCtx = (to?: string) => void; export type AuthRequestsFromCtx = AuthorizeRequest[]; export type SignRequestsFromCtx = SigningRequest[]; diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index 40104702..81f2141e 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -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(message: TMessageType): Promise; +function sendMessage(message: TMessageType, request: RequestTypes[TMessageType]): Promise; +function sendMessage(message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any -function sendMessage (message: MessageTypes, request: any = {}, subscriber?: (data: any) => void): Promise { +function sendMessage (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: any) => void): Promise { 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 { return sendMessage('accounts.forget', { address }); } -export async function getAccounts (): Promise { +export async function getAccounts (): Promise { 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 { - return sendMessage('accounts.subscribe', {}, cb); +export async function subscribeAccounts (cb: (accounts: InjectedAccount[]) => void): Promise { + return sendMessage('accounts.subscribe', null, cb); } export async function subscribeAuthorize (cb: (accounts: AuthorizeRequest[]) => void): Promise { - return sendMessage('authorize.subscribe', {}, cb); + return sendMessage('authorize.subscribe', null, cb); } export async function subscribeSigning (cb: (accounts: SigningRequest[]) => void): Promise { - return sendMessage('signing.subscribe', {}, cb); + return sendMessage('signing.subscribe', null, cb); } export async function validateSeed (suri: string, type?: KeypairType): Promise<{ address: string; suri: string }> { diff --git a/packages/extension-ui/src/partials/Name.tsx b/packages/extension-ui/src/partials/Name.tsx index 5ea74983..3f9ec41b 100644 --- a/packages/extension-ui/src/partials/Name.tsx +++ b/packages/extension-ui/src/partials/Name.tsx @@ -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); diff --git a/packages/extension/src/background/handlers/Extension.ts b/packages/extension/src/background/handlers/Extension.ts index fb067906..5c91eacb 100644 --- a/packages/extension/src/background/handlers/Extension.ts +++ b/packages/extension/src/background/handlers/Extension.ts @@ -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 { - switch (type) { - case 'authorize.approve': - return this.authorizeApprove(request); + public handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise { + 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}`)); + } + }); } } diff --git a/packages/extension/src/background/handlers/State.ts b/packages/extension/src/background/handlers/State.ts index 9db77474..cdf0af83 100644 --- a/packages/extension/src/background/handlers/State.ts +++ b/packages/extension/src/background/handlers/State.ts @@ -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 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 { + public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise { 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 { + public signQueue (url: string, request: RequestExtrinsicSign): Promise { const id = getId(); return new Promise((resolve, reject): void => { diff --git a/packages/extension/src/background/handlers/Tabs.ts b/packages/extension/src/background/handlers/Tabs.ts index 103d6316..0098012a 100644 --- a/packages/extension/src/background/handlers/Tabs.ts +++ b/packages/extension/src/background/handlers/Tabs.ts @@ -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 { + private authorize (url: string, request: RequestAuthorizeTab): Promise { 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 { + private extrinsicSign (url: string, request: RequestExtrinsicSign): Promise { 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 { + public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { 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}`); diff --git a/packages/extension/src/background/handlers/index.ts b/packages/extension/src/background/handlers/index.ts index cae96672..e8aa6771 100644 --- a/packages/extension/src/background/handlers/index.ts +++ b/packages/extension/src/background/handlers/index.ts @@ -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 ({ id, message, request }: TransportRequestMessage, port: chrome.runtime.Port): void { const isPopup = port.name === PORT_POPUP; const sender = port.sender as chrome.runtime.MessageSender; const from = isPopup diff --git a/packages/extension/src/background/handlers/subscriptions.ts b/packages/extension/src/background/handlers/subscriptions.ts index 63b46920..52139089 100644 --- a/packages/extension/src/background/handlers/subscriptions.ts +++ b/packages/extension/src/background/handlers/subscriptions.ts @@ -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; 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 (id: string, port: chrome.runtime.Port): (data: SubscriptionMessageTypes[TMessageType]) => void { subscriptions[id] = port; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/extension/src/background/types.ts b/packages/extension/src/background/types.ts index 73669d50..03e6e5b6 100644 --- a/packages/extension/src/background/types.ts +++ b/packages/extension/src/background/types.ts @@ -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 = { + [K in keyof T]: T[K] extends undefined ? never : K +}[keyof T]; -export type AuthorizeRequest = [string, MessageAuthorize, string]; +type NoUndefinedValues = { + [K in KeysWithDefinedValues]: T[K] +}; -export type SigningRequest = [string, MessageExtrinsicSign, string]; +type IsNull = { [K1 in Exclude]: T[K1] } & T[K] extends null ? K : never +type NullKeys = { [K in keyof T]: IsNull }[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 + +export interface TransportRequestMessage { + 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 { + error?: string; + id: string; + response?: ResponseTypes[TMessageType]; + subscription?: SubscriptionMessageTypes[TMessageType]; +} + +interface TransportResponseMessageNoSub { + error?: string; + id: string; + response?: ResponseTypes[TMessageType]; +} + +export type TransportResponseMessage = + TMessageType extends MessageTypesWithNoSubscriptions + ? TransportResponseMessageNoSub + : TMessageType extends MessageTypesWithSubscriptions + ? TransportResponseMessageSub + : 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 diff --git a/packages/extension/src/page/Injected.ts b/packages/extension/src/page/Injected.ts index 3dd053d0..1ea12095 100644 --- a/packages/extension/src/page/Injected.ts +++ b/packages/extension/src/page/Injected.ts @@ -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; diff --git a/packages/extension/src/page/index.ts b/packages/extension/src/page/index.ts index 3d60935f..69c7e0e0 100644 --- a/packages/extension/src/page/index.ts +++ b/packages/extension/src/page/index.ts @@ -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(message: TMessageType): Promise; +function sendMessage(message: TMessageType, request: RequestTypes[TMessageType]): Promise; +function sendMessage (message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any -function sendMessage (message: MessageTypes, request: any = null, subscriber?: (data: any) => void): Promise { +function sendMessage (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: any) => void): Promise { 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 = { + id, + message, + origin: 'page', + request: request || null as RequestTypes[TMessageType] + }; + + window.postMessage(transportRequestMessage, '*'); }); } @@ -48,17 +57,12 @@ async function enable (origin: string): Promise { 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 (data: TransportResponseMessage & {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, { diff --git a/packages/extension/src/page/types.ts b/packages/extension/src/page/types.ts index 8d6fe5eb..90449248 100644 --- a/packages/extension/src/page/types.ts +++ b/packages/extension/src/page/types.ts @@ -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; + (message: TMessageType): Promise; + (message: TMessageType, request: RequestTypes[TMessageType]): Promise; + (message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise; }