// Copyright 2019-2022 @polkadot/extension authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { InjectedAccount, InjectedMetadataKnown, MetadataDef, ProviderMeta } from '@soul-wallet/extension-inject/types'; import type { KeyringPair } from '@subwallet/keyring/types'; import type { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; import type { JsonRpcResponse } from '@polkadot/rpc-provider/types'; import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import type { MessageTypes, RequestAccountList, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestTypes, ResponseRpcListProviders, ResponseSigning, ResponseTypes, SubscriptionMessageTypes } from '../types'; import { PHISHING_PAGE_REDIRECT } from '@soul-wallet/extension-base/defaults'; import { canDerive } from '@soul-wallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; import { accounts as accountsObservable } from '@subwallet/ui-keyring/observable/accounts'; import { checkIfDenied } from '@polkadot/phishing'; import { assert, isNumber } from '@polkadot/util'; import RequestBytesSign from '../RequestBytesSign'; import RequestExtrinsicSign from '../RequestExtrinsicSign'; import { withErrorLog } from './helpers'; import State from './State'; import { createSubscription, unsubscribe } from './subscriptions'; function transformAccounts (accounts: SubjectInfo, anyType = false): InjectedAccount[] { return Object .values(accounts) .filter(({ json: { meta: { isHidden } } }) => !isHidden) .filter(({ type }) => anyType ? true : canDerive(type)) .filter(({ type }) => (type !== 'ethereum')) // Quick fix DApp not allow EVM .sort((a, b) => (a.json.meta.whenCreated || 0) - (b.json.meta.whenCreated || 0)) .map(({ json: { address, meta: { genesisHash, name } }, type }): InjectedAccount => ({ address, genesisHash, name, type })); } export default class Tabs { readonly #state: State; constructor (state: State) { this.#state = state; } 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, { anyType }: RequestAccountList): InjectedAccount[] { return transformAccounts(accountsObservable.subject.getValue(), anyType); } // 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<'pub(accounts.subscribe)'>(id, port); const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void => cb(transformAccounts(accounts)) ); port.onDisconnect.addListener((): void => { unsubscribe(id); subscription.unsubscribe(); }); return true; } private getSigningPair (address: string): KeyringPair { const pair = keyring.getPair(address); assert(pair, 'Unable to find account'); return pair; } private bytesSign (url: string, request: SignerPayloadRaw): Promise { const address = request.address; const pair = this.getSigningPair(address); return this.#state.sign(url, new RequestBytesSign(request), { address, ...pair.meta }); } private extrinsicSign (url: string, request: SignerPayloadJSON): Promise { const address = request.address; const pair = this.getSigningPair(address); return this.#state.sign(url, new RequestExtrinsicSign(request), { address, ...pair.meta }); } private metadataProvide (url: string, request: MetadataDef): Promise { return this.#state.injectMetadata(url, request); } // eslint-disable-next-line @typescript-eslint/no-unused-vars private metadataList (url: string): InjectedMetadataKnown[] { return this.#state.knownMetadata.map(({ genesisHash, specVersion }) => ({ genesisHash, specVersion })); } private rpcListProviders (): Promise { return this.#state.rpcListProviders(); } private rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise> { return this.#state.rpcSend(request, port); } private rpcStartProvider (key: string, port: chrome.runtime.Port): Promise { return this.#state.rpcStartProvider(key, port); } private async rpcSubscribe (request: RequestRpcSubscribe, id: string, port: chrome.runtime.Port): Promise { const innerCb = createSubscription<'pub(rpc.subscribe)'>(id, port); const cb = (_error: Error | null, data: SubscriptionMessageTypes['pub(rpc.subscribe)']): void => innerCb(data); const subscriptionId = await this.#state.rpcSubscribe(request, cb, port); port.onDisconnect.addListener((): void => { unsubscribe(id); withErrorLog(() => this.rpcUnsubscribe({ ...request, subscriptionId }, port)); }); return true; } private rpcSubscribeConnected (request: null, id: string, port: chrome.runtime.Port): Promise { const innerCb = createSubscription<'pub(rpc.subscribeConnected)'>(id, port); const cb = (_error: Error | null, data: SubscriptionMessageTypes['pub(rpc.subscribeConnected)']): void => innerCb(data); this.#state.rpcSubscribeConnected(request, cb, port); port.onDisconnect.addListener((): void => { unsubscribe(id); }); return Promise.resolve(true); } private async rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise { return this.#state.rpcUnsubscribe(request, port); } private redirectPhishingLanding (phishingWebsite: string): void { const nonFragment = phishingWebsite.split('#')[0]; const encodedWebsite = encodeURIComponent(nonFragment); const url = `${chrome.extension.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`; chrome.tabs.query({ url: nonFragment }, (tabs) => { tabs .map(({ id }) => id) .filter((id): id is number => isNumber(id)) .forEach((id) => withErrorLog(() => chrome.tabs.update(id, { url })) ); }); } protected async redirectIfPhishing (url: string): Promise { const isInDenyList = await checkIfDenied(url); if (isInDenyList) { this.redirectPhishingLanding(url); return true; } return false; } public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { if (type === 'pub(phishing.redirectIfDenied)') { return this.redirectIfPhishing(url); } switch (type) { case 'pub(authorize.tab)': return this.authorize(url, request as RequestAuthorizeTab); case 'pub(accounts.list)': return this.accountsList(url, request as RequestAccountList); case 'pub(accounts.subscribe)': return this.accountsSubscribe(url, id, port); case 'pub(bytes.sign)': return this.bytesSign(url, request as SignerPayloadRaw); case 'pub(extrinsic.sign)': return this.extrinsicSign(url, request as SignerPayloadJSON); case 'pub(metadata.list)': return this.metadataList(url); case 'pub(metadata.provide)': return this.metadataProvide(url, request as MetadataDef); case 'pub(rpc.listProviders)': return this.rpcListProviders(); case 'pub(rpc.send)': return this.rpcSend(request as RequestRpcSend, port); case 'pub(rpc.startProvider)': return this.rpcStartProvider(request as string, port); case 'pub(rpc.subscribe)': return this.rpcSubscribe(request as RequestRpcSubscribe, id, port); case 'pub(rpc.subscribeConnected)': return this.rpcSubscribeConnected(request as null, id, port); case 'pub(rpc.unsubscribe)': return this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port); default: throw new Error(`Unable to handle message of type ${type}`); } } }