Unverified Commit 94def670 authored by Andrei Eres's avatar Andrei Eres Committed by GitHub
Browse files

Fix working along with polkadot-js-extension (#75)

parent d1349123
Pipeline #162267 passed with stages
in 1 minute and 7 seconds
import handlers from '@polkadot/extension-base/background/handlers'
import { PORT_CONTENT, PORT_EXTENSION } from '@polkadot/extension-base/defaults'
import { AccountsStore } from '@polkadot/extension-base/stores'
import chrome from '@polkadot/extension-inject/chrome'
import keyring from '@polkadot/ui-keyring'
import { assert } from '@polkadot/util'
import { cryptoWaitReady } from '@polkadot/util-crypto'
import chrome from '@polkadot/extension-inject/chrome'
import { handler } from './base/handler'
import { AccountsStorage } from './storages/AccountsStorage'
import { PORT_CONTENT, PORT_EXTENSION } from './utils/constants'
// setup the notification (same a FF default background, white text)
void chrome.browserAction.setBadgeBackgroundColor({ color: '#d90000' })
......@@ -16,7 +16,7 @@ chrome.runtime.onConnect.addListener((port) => {
`Unknown connection from ${port.name}`
)
port.onMessage.addListener((data) => handlers(data, port))
port.onMessage.addListener((data) => handler(data, port))
port.onDisconnect.addListener(() =>
console.log(`Disconnected from ${port.name}`)
)
......@@ -25,7 +25,7 @@ chrome.runtime.onConnect.addListener((port) => {
cryptoWaitReady()
.then(() => {
console.log('crypto initialized')
keyring.loadAll({ store: new AccountsStore(), type: 'sr25519' })
keyring.loadAll({ store: new AccountsStorage(), type: 'sr25519' })
console.log('initialization completed')
})
.catch((error) => {
......
import {
createSubscription,
unsubscribe,
} from '@polkadot/extension-base/background/handlers/subscriptions'
import {
AllowedPath,
AuthorizeRequest,
MessageTypes,
MetadataRequest,
RequestAccountBatchExport,
RequestAccountChangePassword,
RequestAccountCreateExternal,
RequestAccountCreateHardware,
RequestAccountCreateSuri,
RequestAccountEdit,
RequestAccountExport,
RequestAccountForget,
RequestAccountShow,
RequestAccountTie,
RequestAccountValidate,
RequestAuthorizeApprove,
RequestAuthorizeReject,
RequestBatchRestore,
RequestDeriveCreate,
RequestDeriveValidate,
RequestJsonRestore,
RequestMetadataApprove,
RequestMetadataReject,
RequestSeedCreate,
RequestSeedValidate,
RequestSigningApprovePassword,
RequestSigningApproveSignature,
RequestSigningCancel,
RequestSigningIsLocked,
RequestTypes,
ResponseAccountExport,
ResponseAccountsExport,
ResponseAuthorizeList,
ResponseDeriveValidate,
ResponseJsonGetAccountInfo,
ResponseSeedCreate,
ResponseSeedValidate,
ResponseSigningIsLocked,
ResponseType,
SigningRequest,
} from '@polkadot/extension-base/background/types'
import {
ALLOWED_PATH,
PASSWORD_EXPIRY_MS,
} from '@polkadot/extension-base/defaults'
import chrome from '@polkadot/extension-inject/chrome'
import { MetadataDef } from '@polkadot/extension-inject/types'
import {
KeyringPair,
KeyringPair$Json,
KeyringPair$Meta,
} from '@polkadot/keyring/types'
import keyring from '@polkadot/ui-keyring'
import { accounts as accountsObservable } from '@polkadot/ui-keyring/observable/accounts'
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'
import { assert, isHex } from '@polkadot/util'
import {
keyExtractSuri,
mnemonicGenerate,
mnemonicValidate,
} from '@polkadot/util-crypto'
import { REGISTRY } from '../utils/constants'
import { isJsonPayload } from '../utils/guards'
import { transformExtensionAccounts } from '../utils/transformExtensionAccounts'
import { State } from './State'
type CachedUnlocks = Record<string, number>
const SEED_DEFAULT_LENGTH = 12
const SEED_LENGTHS = [12, 15, 18, 21, 24]
export class Extension {
readonly #cachedUnlocks: CachedUnlocks = {}
readonly #state: State
constructor(state: State) {
this.#state = state
}
public async handle<TMessageType extends MessageTypes>(
id: string,
type: TMessageType,
request: RequestTypes[TMessageType],
port: chrome.runtime.Port
): Promise<ResponseType<TMessageType>> {
switch (type) {
case 'pri(authorize.approve)':
return this.authorizeApprove(request as RequestAuthorizeApprove)
case 'pri(authorize.list)':
return this.getAuthList()
case 'pri(authorize.reject)':
return this.authorizeReject(request as RequestAuthorizeReject)
case 'pri(authorize.toggle)':
return this.toggleAuthorization(request as string)
case 'pri(authorize.requests)':
return this.authorizeSubscribe(id, port)
case 'pri(accounts.create.external)':
return this.accountsCreateExternal(
request as RequestAccountCreateExternal
)
case 'pri(accounts.create.hardware)':
return this.accountsCreateHardware(
request as RequestAccountCreateHardware
)
case 'pri(accounts.create.suri)':
return this.accountsCreateSuri(request as RequestAccountCreateSuri)
case 'pri(accounts.changePassword)':
return this.accountsChangePassword(
request as RequestAccountChangePassword
)
case 'pri(accounts.edit)':
return this.accountsEdit(request as RequestAccountEdit)
case 'pri(accounts.export)':
return this.accountsExport(request as RequestAccountExport)
case 'pri(accounts.batchExport)':
return this.accountsBatchExport(request as RequestAccountBatchExport)
case 'pri(accounts.forget)':
return this.accountsForget(request as RequestAccountForget)
case 'pri(accounts.show)':
return this.accountsShow(request as RequestAccountShow)
case 'pri(accounts.subscribe)':
return this.accountsSubscribe(id, port)
case 'pri(accounts.tie)':
return this.accountsTie(request as RequestAccountTie)
case 'pri(accounts.validate)':
return this.accountsValidate(request as RequestAccountValidate)
case 'pri(metadata.approve)':
return this.metadataApprove(request as RequestMetadataApprove)
case 'pri(metadata.get)':
return this.metadataGet(request as string)
case 'pri(metadata.list)':
return this.metadataList()
case 'pri(metadata.reject)':
return this.metadataReject(request as RequestMetadataReject)
case 'pri(metadata.requests)':
return this.metadataSubscribe(id, port)
case 'pri(derivation.create)':
return this.derivationCreate(request as RequestDeriveCreate)
case 'pri(derivation.validate)':
return this.derivationValidate(request as RequestDeriveValidate)
case 'pri(json.restore)':
return this.jsonRestore(request as RequestJsonRestore)
case 'pri(json.batchRestore)':
return this.batchRestore(request as RequestBatchRestore)
case 'pri(json.account.info)':
return this.jsonGetAccountInfo(request as KeyringPair$Json)
case 'pri(seed.create)':
return this.seedCreate(request as RequestSeedCreate)
case 'pri(seed.validate)':
return this.seedValidate(request as RequestSeedValidate)
case 'pri(settings.notification)':
return this.#state.setNotification(request as string)
case 'pri(signing.approve.password)':
return this.signingApprovePassword(
request as RequestSigningApprovePassword
)
case 'pri(signing.approve.signature)':
return this.signingApproveSignature(
request as RequestSigningApproveSignature
)
case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel)
case 'pri(signing.isLocked)':
return this.signingIsLocked(request as RequestSigningIsLocked)
case 'pri(signing.requests)':
return this.signingSubscribe(id, port)
case 'pri(window.open)':
return this.windowOpen(request as AllowedPath)
default:
throw new Error(`Unable to handle message of type ${type}`)
}
}
private accountsCreateExternal({
address,
genesisHash,
name,
}: RequestAccountCreateExternal): boolean {
keyring.addExternal(address, { genesisHash, name })
return true
}
private accountsCreateHardware({
accountIndex,
address,
addressOffset,
genesisHash,
hardwareType,
name,
}: RequestAccountCreateHardware): boolean {
keyring.addHardware(address, hardwareType, {
accountIndex,
addressOffset,
genesisHash,
name,
})
return true
}
private accountsCreateSuri({
genesisHash,
name,
password,
suri,
type,
}: RequestAccountCreateSuri): boolean {
keyring.addUri(suri, password, { genesisHash, name }, type)
return true
}
private accountsChangePassword({
address,
newPass,
oldPass,
}: RequestAccountChangePassword): boolean {
const pair = keyring.getPair(address)
assert(pair, 'Unable to find pair')
try {
if (!pair.isLocked) pair.lock()
pair.decodePkcs8(oldPass)
} catch (error) {
throw new Error('oldPass is invalid')
}
keyring.encryptAccount(pair, newPass)
return true
}
private accountsEdit({ address, name }: RequestAccountEdit): boolean {
const pair = keyring.getPair(address)
assert(pair, 'Unable to find pair')
keyring.saveAccountMeta(pair, { ...pair.meta, name })
return true
}
private accountsExport({
address,
password,
}: RequestAccountExport): ResponseAccountExport {
return {
exportedJson: keyring.backupAccount(keyring.getPair(address), password),
}
}
private async accountsBatchExport({
addresses,
password,
}: RequestAccountBatchExport): Promise<ResponseAccountsExport> {
return {
exportedJson: await keyring.backupAccounts(addresses, password),
}
}
private accountsForget({ address }: RequestAccountForget): boolean {
keyring.forgetAccount(address)
return true
}
private refreshAccountPasswordCache(pair: KeyringPair): number {
const { address } = pair
const savedExpiry = this.#cachedUnlocks[address] || 0
const remainingTime = savedExpiry - Date.now()
if (remainingTime < 0) {
this.#cachedUnlocks[address] = 0
pair.lock()
return 0
}
return remainingTime
}
private accountsShow({ address, isShowing }: RequestAccountShow): boolean {
const pair = keyring.getPair(address)
assert(pair, 'Unable to find pair')
keyring.saveAccountMeta(pair, { ...pair.meta, isHidden: !isShowing })
return true
}
private accountsTie({ address, genesisHash }: RequestAccountTie): boolean {
const pair = keyring.getPair(address)
assert(pair, 'Unable to find pair')
keyring.saveAccountMeta(pair, { ...pair.meta, genesisHash })
return true
}
private accountsValidate({
address,
password,
}: RequestAccountValidate): boolean {
try {
keyring.backupAccount(keyring.getPair(address), password)
return true
} catch (e) {
return false
}
}
// FIXME This looks very much like what we have in Tabs
private accountsSubscribe(id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(accounts.subscribe)'>(id, port)
const subscription = accountsObservable.subject.subscribe(
(accounts: SubjectInfo) => cb(transformExtensionAccounts(accounts))
)
port.onDisconnect.addListener(() => {
unsubscribe(id)
subscription.unsubscribe()
})
return true
}
private authorizeApprove({ id }: RequestAuthorizeApprove): boolean {
const queued = this.#state.getAuthRequest(id)
assert(queued, 'Unable to find request')
const { resolve } = queued
resolve(true)
return true
}
private getAuthList(): ResponseAuthorizeList {
return { list: this.#state.authUrls }
}
private authorizeReject({ id }: RequestAuthorizeReject): boolean {
const queued = this.#state.getAuthRequest(id)
assert(queued, 'Unable to find request')
const { reject } = queued
reject(new Error('Rejected'))
return true
}
// FIXME This looks very much like what we have in accounts
private authorizeSubscribe(id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(authorize.requests)'>(id, port)
const subscription = this.#state.authSubject.subscribe(
(requests: AuthorizeRequest[]) => cb(requests)
)
port.onDisconnect.addListener(() => {
unsubscribe(id)
subscription.unsubscribe()
})
return true
}
private metadataApprove({ id }: RequestMetadataApprove): boolean {
const queued = this.#state.getMetaRequest(id)
assert(queued, 'Unable to find request')
const { request, resolve } = queued
this.#state.saveMetadata(request)
resolve(true)
return true
}
private metadataGet(genesisHash: string | null): MetadataDef | null {
return (
this.#state.knownMetadata.find(
(result) => result.genesisHash === genesisHash
) || null
)
}
private metadataList(): MetadataDef[] {
return this.#state.knownMetadata
}
private metadataReject({ id }: RequestMetadataReject): boolean {
const queued = this.#state.getMetaRequest(id)
assert(queued, 'Unable to find request')
const { reject } = queued
reject(new Error('Rejected'))
return true
}
private metadataSubscribe(id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(metadata.requests)'>(id, port)
const subscription = this.#state.metaSubject.subscribe(
(requests: MetadataRequest[]) => cb(requests)
)
port.onDisconnect.addListener(() => {
unsubscribe(id)
subscription.unsubscribe()
})
return true
}
private jsonRestore({ file, password }: RequestJsonRestore) {
try {
keyring.restoreAccount(file, password)
} catch (error) {
throw new Error((error as Error).message)
}
}
private batchRestore({ file, password }: RequestBatchRestore) {
try {
keyring.restoreAccounts(file, password)
} catch (error) {
throw new Error((error as Error).message)
}
}
private jsonGetAccountInfo(
json: KeyringPair$Json
): ResponseJsonGetAccountInfo {
try {
const {
address,
meta: { genesisHash, name },
type,
} = keyring.createFromJson(json)
return {
address,
genesisHash,
name,
type,
} as ResponseJsonGetAccountInfo
} catch (e) {
console.error(e)
throw new Error((e as Error).message)
}
}
private seedCreate({
length = SEED_DEFAULT_LENGTH,
type,
}: RequestSeedCreate): ResponseSeedCreate {
const seed = mnemonicGenerate(length)
return {
address: keyring.createFromUri(seed, {}, type).address,
seed,
}
}
private seedValidate({
suri,
type,
}: RequestSeedValidate): ResponseSeedValidate {
const { phrase } = keyExtractSuri(suri)
if (isHex(phrase)) {
assert(isHex(phrase, 256), 'Hex seed needs to be 256-bits')
} else {
// sadly isHex detects as string, so we need a cast here
assert(
SEED_LENGTHS.includes(phrase.split(' ').length),
`Mnemonic needs to contain ${SEED_LENGTHS.join(', ')} words`
)
assert(mnemonicValidate(phrase), 'Not a valid mnemonic seed')
}
return {
address: keyring.createFromUri(suri, {}, type).address,
suri,
}
}
private signingApprovePassword({
id,
password,
savePass,
}: RequestSigningApprovePassword): boolean {
const queued = this.#state.getSignRequest(id)
assert(queued, 'Unable to find request')
const { reject, request, resolve } = queued
const pair = keyring.getPair(queued.account.address)
// unlike queued.account.address the following
// address is encoded with the default prefix
// which what is used for password caching mapping
const { address } = pair
if (!pair) {
reject(new Error('Unable to find pair'))
return false
}
this.refreshAccountPasswordCache(pair)
// if the keyring pair is locked, the password is needed
if (pair.isLocked && !password)
reject(new Error('Password needed to unlock the account'))
if (pair.isLocked) pair.decodePkcs8(password)
const { payload } = request
if (isJsonPayload(payload)) {
// Get the metadata for the genesisHash
const currentMetadata = this.#state.knownMetadata.find(
(meta: MetadataDef) => meta.genesisHash === payload.genesisHash
)
// set the registry before calling the sign function
REGISTRY.setSignedExtensions(
payload.signedExtensions,
currentMetadata?.userExtensions
)
if (currentMetadata) {
REGISTRY.register(currentMetadata?.types)
}
}
const result = request.sign(REGISTRY, pair)
if (savePass) {
this.#cachedUnlocks[address] = Date.now() + PASSWORD_EXPIRY_MS
} else {
pair.lock()
}
resolve({ id, ...result })
return true
}
private signingApproveSignature({
id,
signature,