From f773342343abae9aca9bcb76355008a880f401a1 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 1 Jun 2020 17:46:30 +0200 Subject: [PATCH] Tie account to genesis (#355) * Tie account to genesis * Allow (faster) partial retrieval * Edit toggles menu --- .../src/background/handlers/Extension.ts | 28 ++++++++++++-- .../extension-base/src/background/types.ts | 7 ++++ .../extension-chains/src/genesisHashes.ts | 29 +++++++++++++++ packages/extension-chains/src/index.ts | 31 ++++++++++++---- .../src/Popup/Accounts/Account.tsx | 37 ++++++++++++++++--- .../extension-ui/src/components/Address.tsx | 27 ++++++++------ .../extension-ui/src/hooks/useMetadata.ts | 14 ++++--- packages/extension-ui/src/messaging.ts | 27 ++++++++++++-- 8 files changed, 163 insertions(+), 37 deletions(-) create mode 100644 packages/extension-chains/src/genesisHashes.ts diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 74f0e92b..65168aa8 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -4,7 +4,7 @@ import { MetadataDef } from '@polkadot/extension-inject/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, ResponseSeedValidate, ResponseType, SigningRequest, RequestJsonRestore, ResponseJsonRestore } from '../types'; +import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, ResponseSeedValidate, ResponseType, SigningRequest, RequestJsonRestore, ResponseJsonRestore } from '../types'; import chrome from '@polkadot/extension-inject/chrome'; import keyring from '@polkadot/ui-keyring'; @@ -79,6 +79,16 @@ export default class Extension { 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); @@ -157,6 +167,10 @@ export default class Extension { 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; } @@ -394,15 +408,21 @@ export default class Extension { case 'pri(accounts.show)': return this.accountsShow(request as RequestAccountShow); - case 'pri(accounts.validate)': - return this.accountsValidate(request as RequestAccountValidate); - 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(); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index d047da91..f9b8cf0f 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -72,6 +72,7 @@ export interface RequestSignatures { 'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport]; 'pri(accounts.forget)': [RequestAccountForget, boolean]; 'pri(accounts.show)': [RequestAccountShow, boolean]; + 'pri(accounts.tie)': [RequestAccountTie, boolean]; 'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]]; 'pri(accounts.validate)': [RequestAccountValidate, boolean]; 'pri(authorize.approve)': [RequestAuthorizeApprove, boolean]; @@ -83,6 +84,7 @@ export interface RequestSignatures { 'pri(json.verify.file)': [RequestJsonRestore, boolean]; 'pri(json.verify.password)': [string, boolean]; 'pri(metadata.approve)': [RequestMetadataApprove, boolean]; + 'pri(metadata.get)': [string | null, MetadataDef | null]; 'pri(metadata.reject)': [RequestMetadataReject, boolean]; 'pri(metadata.requests)': [RequestMetadataSubscribe, boolean, MetadataRequest[]]; 'pri(metadata.list)': [null, MetadataDef[]]; @@ -180,6 +182,11 @@ export interface RequestAccountShow { isShowing: boolean; } +export interface RequestAccountTie { + address: string; + genesisHash: string | null; +} + export interface RequestAccountValidate { address: string; password: string; diff --git a/packages/extension-chains/src/genesisHashes.ts b/packages/extension-chains/src/genesisHashes.ts new file mode 100644 index 00000000..bd5de87c --- /dev/null +++ b/packages/extension-chains/src/genesisHashes.ts @@ -0,0 +1,29 @@ +// Copyright 2019-2020 @polkadot/extension-chains authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +interface Option { + text: string; + value: string; +} + +const hashes: Option[] = [ + { + text: 'Allow use on any chain', + value: '' + }, + { + text: 'Polkadot CC1 (live)', + value: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3' + }, + { + text: 'Kusama (canary)', + value: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe' + }, + { + text: 'Edgeware (live)', + value: '0x742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b' + } +]; + +export default hashes; diff --git a/packages/extension-chains/src/index.ts b/packages/extension-chains/src/index.ts index f02da73e..7d6291a0 100644 --- a/packages/extension-chains/src/index.ts +++ b/packages/extension-chains/src/index.ts @@ -11,20 +11,27 @@ import { Metadata, TypeRegistry } from '@polkadot/types'; // inside the api, run `yarn chain:info --ws ` import kusama from './kusama'; -const definitions: Map = new Map( +const definitions = new Map( [kusama].map((def) => [def.genesisHash, def]) ); -function metadataExpand (definition: MetadataDef): Chain { +const expanded = new Map(); + +export function metadataExpand (definition: MetadataDef, isPartial = false): Chain { + const cached = expanded.get(definition.genesisHash); + + if (cached && cached.specVersion === definition.specVersion) { + return cached; + } + const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types } = definition; const registry = new TypeRegistry(); - registry.register(types); + if (!isPartial) { + registry.register(types); + } const isUnknown = genesisHash === '0x'; - const metadata = metaCalls - ? new Metadata(registry, Buffer.from(metaCalls, 'base64')) - : null; registry.setChainProperties(registry.createType('ChainProperties', { ss58Format, @@ -32,7 +39,11 @@ function metadataExpand (definition: MetadataDef): Chain { tokenSymbol })); - return { + const metadata = metaCalls && !isPartial + ? new Metadata(registry, Buffer.from(metaCalls, 'base64')) + : null; + + const result = { definition, genesisHash: isUnknown ? undefined @@ -47,6 +58,12 @@ function metadataExpand (definition: MetadataDef): Chain { tokenDecimals, tokenSymbol }; + + if (result.genesisHash && !isPartial) { + expanded.set(result.genesisHash, result); + } + + return result; } export function findChain (definitions: MetadataDef[], genesisHash?: string | null): Chain | null { diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index 4963c3ab..db1e0dea 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -7,9 +7,10 @@ import { ThemeProps } from '../../types'; import React, { useCallback, useContext, useMemo, useState } from 'react'; import styled from 'styled-components'; +import genesisOptions from '@polkadot/extension-chains/genesisHashes'; -import { ActionContext, Address, Checkbox, Link } from '../../components'; -import { editAccount, showAccount } from '../../messaging'; +import { ActionContext, Address, Checkbox, Dropdown, Link } from '../../components'; +import { editAccount, showAccount, tieAccount } from '../../messaging'; import { Name } from '../../partials'; interface Props extends AccountJson { @@ -17,13 +18,26 @@ interface Props extends AccountJson { parentName?: string; } -function Account ({ address, className, isExternal, isHidden, parentName, suri }: Props): React.ReactElement { +interface EditState { + isEditing: boolean; + toggleActions: number; +} + +function Account ({ address, className, genesisHash, isExternal, isHidden, parentName, suri }: Props): React.ReactElement { const onAction = useContext(ActionContext); - const [isEditing, setEditing] = useState(false); + const [{ isEditing, toggleActions }, setEditing] = useState({ isEditing: false, toggleActions: 0 }); const [editedName, setName] = useState(null); + const _onChangeGenesis = useCallback( + (genesisHash?: string | null): void => { + tieAccount(address, genesisHash || null) + .catch(console.error); + }, + [address] + ); + const _toggleEdit = useCallback( - (): void => setEditing(!isEditing), + (): void => setEditing(({ toggleActions }) => ({ isEditing: !isEditing, toggleActions: ++toggleActions })), [isEditing] ); @@ -39,6 +53,7 @@ function Account ({ address, className, isExternal, isHidden, parentName, suri } }, [editedName, address, _toggleEdit, onAction] ); + const _toggleVisibility = useCallback( (): void => { showAccount(address, isHidden || false) @@ -87,8 +102,15 @@ function Account ({ address, className, isExternal, isHidden, parentName, suri } label='Visible (always inject)' onClick={_toggleVisibility} /> + - ), [_toggleEdit, _toggleVisibility, address, isExternal, isHidden]); + ), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden]); return (
@@ -96,9 +118,11 @@ function Account ({ address, className, isExternal, isHidden, parentName, suri } actions={_actions} address={address} className='address' + genesisHash={genesisHash} name={editedName} parentName={parentName} suri={suri} + toggleActions={toggleActions} > {isEditing && ( ` font-size: 15px; line-height: 20px; margin: 0; + min-width: 13rem; padding: 4px 16px; } `); diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index 1b1a0d8e..a086c6a3 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -33,6 +33,7 @@ interface Props { actions?: React.ReactNode; parentName?: string | null; suri?: string; + toggleActions?: number; } interface Recoded { @@ -67,12 +68,12 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain: }; } -const ACCOUNTS_SCREEN_HEIGHT = 500; +const ACCOUNTS_SCREEN_HEIGHT = 550; -function Address ({ actions, address, children, className, genesisHash, name, parentName, suri }: Props): React.ReactElement { +function Address ({ actions, address, children, className, genesisHash, name, parentName, suri, toggleActions }: Props): React.ReactElement { const { accounts } = useContext(AccountContext); const settings = useContext(SettingsContext); - const chain = useMetadata(genesisHash); + const chain = useMetadata(genesisHash, true); const [{ account, formatted, prefix }, setRecoded] = useState({ account: null, formatted: null, prefix: 42 }); const [showActionsMenu, setShowActionsMenu] = useState(false); const [moveMenuUp, setIsMovedMenu] = useState(false); @@ -99,6 +100,10 @@ function Address ({ actions, address, children, className, genesisHash, name, pa } }, [showActionsMenu]); + useEffect((): void => { + setShowActionsMenu(false); + }, [toggleActions]); + const theme = ((chain && chain.icon) || 'polkadot') as 'polkadot'; const _onClick = useCallback((): void => setShowActionsMenu(!showActionsMenu), [showActionsMenu]); const _onCopy = useCallback((): void => show('Copied'), [show]); @@ -125,7 +130,7 @@ function Address ({ actions, address, children, className, genesisHash, name, pa className='parentName' data-field='parent' > - {parentName}{suri || ''} + {parentName}  {suri || ''}
{displayedName}
@@ -182,18 +187,18 @@ function Address ({ actions, address, children, className, genesisHash, name, pa ); } -const FullAddress = styled.div` +const FullAddress = styled.div(({ theme }: ThemeProps) => ` width: 270px; overflow: hidden; text-overflow: ellipsis; - color: ${({ theme }: ThemeProps): string => theme.labelColor}; + color: ${theme.labelColor}; font-size: 12px; line-height: 16px; -`; +`); FullAddress.displayName = 'FullAddress'; -const Settings = styled.div` +const Settings = styled.div(({ theme }: ThemeProps) => ` position: relative; display: flex; justify-content: center; @@ -213,14 +218,14 @@ const Settings = styled.div` top: 25%; bottom: 25%; width: 1px; - background: ${({ theme }: ThemeProps): string => theme.boxBorderColor}; + background: ${theme.boxBorderColor}; } &:hover { cursor: pointer; - background: ${({ theme }: ThemeProps): string => theme.readonlyInputBackground}; + background: ${theme.readonlyInputBackground}; } -`; +`); Settings.displayName = 'Details'; diff --git a/packages/extension-ui/src/hooks/useMetadata.ts b/packages/extension-ui/src/hooks/useMetadata.ts index 30ff131d..66ec3efd 100644 --- a/packages/extension-ui/src/hooks/useMetadata.ts +++ b/packages/extension-ui/src/hooks/useMetadata.ts @@ -8,14 +8,18 @@ import { useEffect, useState } from 'react'; import { getMetadata } from '../messaging'; -export default function useMetadata (genesisHash?: string | null): Chain | null { +export default function useMetadata (genesisHash?: string | null, isPartial?: boolean): Chain | null { const [chain, setChain] = useState(null); useEffect((): void => { - getMetadata(genesisHash) - .then(setChain) - .catch(console.error); - }, [genesisHash]); + if (genesisHash) { + getMetadata(genesisHash, isPartial) + .then(setChain) + .catch(console.error); + } else { + setChain(null); + } + }, [genesisHash, isPartial]); return chain; } diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index c6a68609..f68f9f82 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -5,11 +5,12 @@ import { Message } from '@polkadot/extension-base/types'; import { AccountJson, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MetadataRequest, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions, ResponseDeriveValidate, ResponseJsonRestore } from '@polkadot/extension-base/background/types'; import { Chain } from '@polkadot/extension-chains/types'; +import { MetadataDef } from '@polkadot/extension-inject/types'; import { KeypairType } from '@polkadot/util-crypto/types'; import { PORT_EXTENSION } from '@polkadot/extension-base/defaults'; import chrome from '@polkadot/extension-inject/chrome'; -import { findChain } from '@polkadot/extension-chains'; +import { metadataExpand } from '@polkadot/extension-chains'; import { KeyringPair$Json } from '@polkadot/keyring/types'; interface Handler { @@ -22,6 +23,7 @@ interface Handler { type Handlers = Record; +const metadataGets = new Map>(); const port = chrome.runtime.connect({ name: PORT_EXTENSION }); const handlers: Handlers = {}; let idCounter = 0; @@ -71,6 +73,10 @@ export async function showAccount (address: string, isShowing: boolean): Promise return sendMessage('pri(accounts.show)', { address, isShowing }); } +export async function tieAccount (address: string, genesisHash: string | null): Promise { + return sendMessage('pri(accounts.tie)', { address, genesisHash }); +} + export async function exportAccount (address: string, password: string): Promise<{ exportedJson: string }> { return sendMessage('pri(accounts.export)', { address, password }); } @@ -115,10 +121,23 @@ export async function createSeed (length?: SeedLengths, type?: KeypairType): Pro return sendMessage('pri(seed.create)', { length, type }); } -export async function getMetadata (genesisHash?: string | null): Promise { - const definitions = await sendMessage('pri(metadata.list)'); +export async function getMetadata (genesisHash?: string | null, isPartial = false): Promise { + if (!genesisHash) { + return null; + } - return findChain(definitions || [], genesisHash); + let request = metadataGets.get(genesisHash); + + if (!request) { + request = sendMessage('pri(metadata.get)', genesisHash || null); + metadataGets.set(genesisHash, request); + } + + const def = await request; + + return def + ? metadataExpand(def, isPartial) + : null; } export async function rejectAuthRequest (id: string): Promise {