diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f662311..34b85cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # CHANGELOG +## 0.41.1 Nov 8, 2021 + +Contributed: + +- Add search functionality (Thanks to https://github.com/Tbaut) + +Changes: + +- Detect Ascii bytes (& display) when signing +- Correctly detect and create Ethereum-compatible chain accounts +- Ensure site authorization toggle is saved +- Optimize metadata conversion process +- Bump `@polkadot/util` to 7.8.2 +- Bump `@polkadot/api` to 6.7.1 + + ## 0.40.4 Oct 25, 2021 **Important** Not published to the stores, aligns with latest released packages. diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index f7e2f5c2..8d764f3e 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -5,6 +5,7 @@ import type { MetadataDef } from '@polkadot/extension-inject/types'; import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types'; import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; +import type { KeypairType } from '@polkadot/util-crypto/types'; import type { AccountJson, 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 '../types'; import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults'; @@ -22,10 +23,17 @@ type CachedUnlocks = Record; const SEED_DEFAULT_LENGTH = 12; const SEED_LENGTHS = [12, 15, 18, 21, 24]; +const ETH_DERIVE_DEFAULT = "/m/44'/60'/0'/0/0"; // a global registry to use internally const registry = new TypeRegistry(); +function getSuri (seed: string, type?: KeypairType): string { + return type === 'ethereum' + ? `${seed}${ETH_DERIVE_DEFAULT}` + : seed; +} + function transformAccounts (accounts: SubjectInfo): AccountJson[] { return Object.values(accounts).map(({ json: { address, meta }, type }): AccountJson => ({ address, @@ -61,7 +69,7 @@ export default class Extension { } private accountsCreateSuri ({ genesisHash, name, password, suri, type }: RequestAccountCreateSuri): boolean { - keyring.addUri(suri, password, { genesisHash, name }, type); + keyring.addUri(getSuri(suri, type), password, { genesisHash, name }, type); return true; } @@ -300,7 +308,7 @@ export default class Extension { const seed = mnemonicGenerate(length); return { - address: keyring.createFromUri(seed, {}, type).address, + address: keyring.createFromUri(getSuri(seed, type), {}, type).address, seed }; } @@ -317,7 +325,7 @@ export default class Extension { } return { - address: keyring.createFromUri(suri, {}, type).address, + address: keyring.createFromUri(getSuri(suri, type), {}, type).address, suri }; } diff --git a/packages/extension-ui/src/Popup/CreateAccount/index.tsx b/packages/extension-ui/src/Popup/CreateAccount/index.tsx index 1885c4d7..aceff515 100644 --- a/packages/extension-ui/src/Popup/CreateAccount/index.tsx +++ b/packages/extension-ui/src/Popup/CreateAccount/index.tsx @@ -7,8 +7,9 @@ import styled from 'styled-components'; import { ActionContext, Address, Dropdown, Loading } from '../../components'; import AccountNamePasswordCreation from '../../components/AccountNamePasswordCreation'; import useGenesisHashOptions from '../../hooks/useGenesisHashOptions'; +import useMetadata from '../../hooks/useMetadata'; import useTranslation from '../../hooks/useTranslation'; -import { createAccountSuri, createSeed } from '../../messaging'; +import { createAccountSuri, createSeed, validateSeed } from '../../messaging'; import { HeaderWithSteps } from '../../partials'; import { DEFAULT_TYPE } from '../../util/defaultType'; import Mnemonic from './Mnemonic'; @@ -22,25 +23,44 @@ function CreateAccount ({ className }: Props): React.ReactElement { const onAction = useContext(ActionContext); const [isBusy, setIsBusy] = useState(false); const [step, setStep] = useState(1); - const [account, setAccount] = useState(null); + const [address, setAddress] = useState(null); + const [seed, setSeed] = useState(null); const [type, setType] = useState(DEFAULT_TYPE); const [name, setName] = useState(''); const options = useGenesisHashOptions(); const [genesisHash, setGenesis] = useState(''); + const chain = useMetadata(genesisHash, true); useEffect((): void => { - createSeed(undefined, type) - .then(setAccount) - .catch((error: Error) => console.error(error)); - }, [type]); + createSeed(undefined) + .then(({ address, seed }): void => { + setAddress(address); + setSeed(seed); + }) + .catch(console.error); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect((): void => { + if (seed) { + const type = chain && chain.definition.chainType === 'ethereum' + ? 'ethereum' + : DEFAULT_TYPE; + + setType(type); + validateSeed(seed, type) + .then(({ address }) => setAddress(address)) + .catch(console.error); + } + }, [seed, chain]); const _onCreate = useCallback( (name: string, password: string): void => { // this should always be the case - if (name && password && account) { + if (name && password && seed) { setIsBusy(true); - createAccountSuri(name, password, account.seed, type, genesisHash) + createAccountSuri(name, password, seed, type, genesisHash) .then(() => onAction('/')) .catch((error: Error): void => { setIsBusy(false); @@ -48,23 +68,23 @@ function CreateAccount ({ className }: Props): React.ReactElement { }); } }, - [account, genesisHash, onAction, type] + [genesisHash, onAction, seed, type] ); - const _onNextStep = useCallback(() => setStep((step) => step + 1), []); - const _onPreviousStep = useCallback(() => setStep((step) => step - 1), []); + const _onNextStep = useCallback( + () => setStep((step) => step + 1), + [] + ); - const _onChangeNetwork = useCallback((newGenesisHash: string) => { - const chain = options.find(({ value }) => { - return newGenesisHash === value; - }); + const _onPreviousStep = useCallback( + () => setStep((step) => step - 1), + [] + ); - if (chain?.text === 'Moonbase Alpha') { // TODO: use list - setType('ethereum'); - } - - setGenesis(newGenesisHash); - }, [options]); + const _onChangeNetwork = useCallback( + (newGenesisHash: string) => setGenesis(newGenesisHash), + [] + ); return ( <> @@ -75,17 +95,17 @@ function CreateAccount ({ className }: Props): React.ReactElement {
- {account && ( + {seed && ( step === 1 ? ( ) : ( diff --git a/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx b/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx index 264cc9fa..1fc10f1e 100644 --- a/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx +++ b/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx @@ -1,6 +1,7 @@ // Copyright 2019-2021 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { KeypairType } from '@polkadot/util-crypto/types'; import type { ThemeProps } from '../../types'; import type { AccountInfo } from '.'; @@ -10,6 +11,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { validateSeed } from '@polkadot/extension-ui/messaging'; +import { objectSpread } from '@polkadot/util'; import { ButtonArea, Dropdown, InputWithLabel, NextStepButton, TextAreaWithLabel, VerticalSpace, Warning } from '../../components'; import useGenesisHashOptions from '../../hooks/useGenesisHashOptions'; @@ -19,9 +21,10 @@ interface Props { className?: string; onNextStep: () => void; onAccountChange: (account: AccountInfo | null) => void; + type: KeypairType; } -function SeedAndPath ({ className, onAccountChange, onNextStep }: Props): React.ReactElement { +function SeedAndPath ({ className, onAccountChange, onNextStep, type }: Props): React.ReactElement { const { t } = useTranslation(); const genesisOptions = useGenesisHashOptions(); const [address, setAddress] = useState(''); @@ -42,22 +45,23 @@ function SeedAndPath ({ className, onAccountChange, onNextStep }: Props): React. const suri = `${seed || ''}${path || ''}`; - validateSeed(suri).then((validatedAccount) => { - setError(''); - setAddress(validatedAccount.address); - // a spread operator here breaks tests, using Object.assign as a workaround - const newAccount: AccountInfo = Object.assign(validatedAccount, { genesis }); - - onAccountChange(newAccount); - }).catch(() => { - setAddress(''); - onAccountChange(null); - setError(path - ? t('Invalid mnemonic seed or derivation path') - : t('Invalid mnemonic seed') - ); - }); - }, [t, seed, path, onAccountChange, genesis]); + validateSeed(suri, type) + .then((validatedAccount) => { + setError(''); + setAddress(validatedAccount.address); + onAccountChange( + objectSpread({}, validatedAccount, { genesis, type }) + ); + }) + .catch(() => { + setAddress(''); + onAccountChange(null); + setError(path + ? t('Invalid mnemonic seed or derivation path') + : t('Invalid mnemonic seed') + ); + }); + }, [t, genesis, seed, path, onAccountChange, type]); const _onToggleAdvanced = useCallback(() => { setAdvances(!advanced); diff --git a/packages/extension-ui/src/Popup/ImportSeed/index.tsx b/packages/extension-ui/src/Popup/ImportSeed/index.tsx index d661f3a6..29160a7d 100644 --- a/packages/extension-ui/src/Popup/ImportSeed/index.tsx +++ b/packages/extension-ui/src/Popup/ImportSeed/index.tsx @@ -5,9 +5,11 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { AccountContext, ActionContext, Address } from '../../components'; import AccountNamePasswordCreation from '../../components/AccountNamePasswordCreation'; +import useMetadata from '../../hooks/useMetadata'; import useTranslation from '../../hooks/useTranslation'; import { createAccountSuri } from '../../messaging'; import { HeaderWithSteps } from '../../partials'; +import { DEFAULT_TYPE } from '../../util/defaultType'; import SeedAndPath from './SeedAndPath'; export interface AccountInfo { @@ -24,27 +26,44 @@ function ImportSeed (): React.ReactElement { const [account, setAccount] = useState(null); const [name, setName] = useState(null); const [step1, setStep1] = useState(true); + const [type, setType] = useState(DEFAULT_TYPE); + const chain = useMetadata(account && account.genesis, true); useEffect((): void => { !accounts.length && onAction(); }, [accounts, onAction]); + useEffect((): void => { + setType( + chain && chain.definition.chainType === 'ethereum' + ? 'ethereum' + : DEFAULT_TYPE + ); + }, [chain]); + const _onCreate = useCallback((name: string, password: string): void => { // this should always be the case if (name && password && account) { setIsBusy(true); - createAccountSuri(name, password, account.suri, undefined, account.genesis) + createAccountSuri(name, password, account.suri, type, account.genesis) .then(() => onAction('/')) .catch((error): void => { setIsBusy(false); console.error(error); }); } - }, [account, onAction]); + }, [account, onAction, type]); - const _onNextStep = useCallback(() => { setStep1(false); }, []); - const _onBackClick = useCallback(() => { setStep1(true); }, []); + const _onNextStep = useCallback( + () => setStep1(false), + [] + ); + + const _onBackClick = useCallback( + () => setStep1(true), + [] + ); return ( <> @@ -64,6 +83,7 @@ function ImportSeed (): React.ReactElement { ) : ( diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index 3d341797..88752caf 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -83,7 +83,9 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain: // always allow the actual settings to override the display return { account, - formatted: encodeAddress(publicKey, prefix), + formatted: account?.type === 'ethereum' + ? address + : encodeAddress(publicKey, prefix), genesisHash: account?.genesisHash, prefix, type: account?.type || DEFAULT_TYPE @@ -116,11 +118,25 @@ function Address ({ actions, address, children, className, genesisHash, isExtern const accountByAddress = findAccountByAddress(accounts, address); - const recoded = (chain?.definition.chainType === 'ethereum' || accountByAddress?.type === 'ethereum' || (!accountByAddress && givenType === 'ethereum')) - ? { account: accountByAddress, formatted: address, type: 'ethereum' } as Recoded - : recodeAddress(address, accounts, chain, settings); + console.log( + address, + ( + chain?.definition.chainType === 'ethereum' || + accountByAddress?.type === 'ethereum' || + (!accountByAddress && givenType === 'ethereum') + ) + ? { account: accountByAddress, formatted: address, type: 'ethereum' } + : recodeAddress(address, accounts, chain, settings) + ); - setRecoded(recoded || defaultRecoded); + setRecoded( + ( + chain?.definition.chainType === 'ethereum' || + accountByAddress?.type === 'ethereum' || + (!accountByAddress && givenType === 'ethereum') + ) + ? { account: accountByAddress, formatted: address, type: 'ethereum' } + : recodeAddress(address, accounts, chain, settings)); }, [accounts, address, chain, givenType, settings]); useEffect(() => { @@ -140,20 +156,23 @@ function Address ({ actions, address, children, className, genesisHash, isExtern }, [toggleActions]); const theme = ( - chain?.icon - ? chain.icon - : type === 'ethereum' - ? 'ethereum' - : 'polkadot' + type === 'ethereum' + ? 'ethereum' + : (chain?.icon || 'polkadot') ) as IconTheme; - const _onClick = useCallback((): void => setShowActionsMenu(!showActionsMenu), [showActionsMenu]); - const _onCopy = useCallback((): void => show(t('Copied')), [show, t]); + const _onClick = useCallback( + () => setShowActionsMenu(!showActionsMenu), + [showActionsMenu] + ); + + const _onCopy = useCallback( + () => show(t('Copied')), + [show, t] + ); + const _toggleVisibility = useCallback( - (): void => { - address && showAccount(address, isHidden || false) - .catch(console.error); - }, + () => address && showAccount(address, isHidden || false).catch(console.error), [address, isHidden] ); diff --git a/packages/extension-ui/src/util/defaultType.ts b/packages/extension-ui/src/util/defaultType.ts index bf3c39e2..3e1ef2ad 100644 --- a/packages/extension-ui/src/util/defaultType.ts +++ b/packages/extension-ui/src/util/defaultType.ts @@ -1,6 +1,6 @@ // Copyright 2019-2021 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { KeypairType } from '@polkadot/util-crypto/types'; +import type { KeypairType } from '@polkadot/util-crypto/types'; -export const DEFAULT_TYPE = 'sr25519' as KeypairType; +export const DEFAULT_TYPE: KeypairType = 'sr25519';