Tie account to genesis (#355)

* Tie account to genesis

* Allow (faster) partial retrieval

* Edit toggles menu
This commit is contained in:
Jaco Greeff 2020-06-01 17:46:30 +02:00 committed by GitHub
parent d521d5780b
commit f773342343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 163 additions and 37 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -11,20 +11,27 @@ import { Metadata, TypeRegistry } from '@polkadot/types';
// inside the api, run `yarn chain:info --ws <url>`
import kusama from './kusama';
const definitions: Map<string, MetadataDef> = new Map(
const definitions = new Map<string, MetadataDef>(
[kusama].map((def) => [def.genesisHash, def])
);
function metadataExpand (definition: MetadataDef): Chain {
const expanded = new Map<string, Chain>();
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 {

View File

@ -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<Props> {
interface EditState {
isEditing: boolean;
toggleActions: number;
}
function Account ({ address, className, genesisHash, isExternal, isHidden, parentName, suri }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const [isEditing, setEditing] = useState(false);
const [{ isEditing, toggleActions }, setEditing] = useState<EditState>({ isEditing: false, toggleActions: 0 });
const [editedName, setName] = useState<string | null>(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}
/>
<Dropdown
className='menuItem'
label=''
onChange={_onChangeGenesis}
options={genesisOptions}
value={genesisHash || ''}
/>
</>
), [_toggleEdit, _toggleVisibility, address, isExternal, isHidden]);
), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden]);
return (
<div className={className}>
@ -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 && (
<Name
@ -141,6 +165,7 @@ export default styled(Account)(({ theme }: ThemeProps) => `
font-size: 15px;
line-height: 20px;
margin: 0;
min-width: 13rem;
padding: 4px 16px;
}
`);

View File

@ -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<Props> {
function Address ({ actions, address, children, className, genesisHash, name, parentName, suri, toggleActions }: Props): React.ReactElement<Props> {
const { accounts } = useContext(AccountContext);
const settings = useContext(SettingsContext);
const chain = useMetadata(genesisHash);
const chain = useMetadata(genesisHash, true);
const [{ account, formatted, prefix }, setRecoded] = useState<Recoded>({ 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}&nbsp;&nbsp;{suri || ''}
</div>
</div>
<div className='name displaced'>{displayedName}</div>
@ -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';

View File

@ -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<Chain | null>(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;
}

View File

@ -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<string, Handler>;
const metadataGets = new Map<string, Promise<MetadataDef | null>>();
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<boolean> {
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<Chain | null> {
const definitions = await sendMessage('pri(metadata.list)');
export async function getMetadata (genesisHash?: string | null, isPartial = false): Promise<Chain | null> {
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<boolean> {