feat: local fork allow fake accounts (#10342)

* fix: upgrade chopsticks

* feat: expose fork in context

* fix lock

* fix: add account to chopsticks

* feat: sign fake

* fix: lint
This commit is contained in:
Qiwei Yang 2024-04-11 10:12:57 +08:00 committed by GitHub
parent 60bdc504b6
commit 453b94f381
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 251 additions and 45 deletions

View File

@ -19,6 +19,7 @@ import { BN_ZERO, isFunction } from '@polkadot/util';
import CreateModal from '../modals/Create.js';
import ImportModal from '../modals/Import.js';
import Ledger from '../modals/Ledger.js';
import Local from '../modals/LocalAdd.js';
import Multisig from '../modals/MultisigCreate.js';
import Proxy from '../modals/ProxiedAdd.js';
import Qr from '../modals/Qr.js';
@ -44,17 +45,18 @@ interface SortControls {
sortFromMax: boolean;
}
type GroupName = 'accounts' | 'hardware' | 'injected' | 'multisig' | 'proxied' | 'qr' | 'testing';
type GroupName = 'accounts' | 'chopsticks' | 'hardware' | 'injected' | 'multisig' | 'proxied' | 'qr' | 'testing';
const DEFAULT_SORT_CONTROLS: SortControls = { sortBy: 'date', sortFromMax: true };
const STORE_FAVS = 'accounts:favorites';
const GROUP_ORDER: GroupName[] = ['accounts', 'injected', 'qr', 'hardware', 'proxied', 'multisig', 'testing'];
const GROUP_ORDER: GroupName[] = ['accounts', 'injected', 'qr', 'hardware', 'proxied', 'multisig', 'testing', 'chopsticks'];
function groupAccounts (accounts: SortedAccount[]): Record<GroupName, string[]> {
const ret: Record<GroupName, string[]> = {
accounts: [],
chopsticks: [],
hardware: [],
injected: [],
multisig: [],
@ -77,6 +79,8 @@ function groupAccounts (accounts: SortedAccount[]): Record<GroupName, string[]>
ret.multisig.push(address);
} else if (cryptoType === 'proxied') {
ret.proxied.push(address);
} else if (cryptoType === 'chopsticks') {
ret.chopsticks.push(address);
} else if (cryptoType === 'qr') {
ret.qr.push(address);
} else {
@ -89,7 +93,7 @@ function groupAccounts (accounts: SortedAccount[]): Record<GroupName, string[]>
function Overview ({ className = '', onStatusChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, isElectron } = useApi();
const { api, fork, isElectron } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const { isIpfs } = useIpfs();
const { isLedgerEnabled } = useLedger();
@ -98,6 +102,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
const [isLedgerOpen, toggleLedger] = useToggle();
const [isMultisigOpen, toggleMultisig] = useToggle();
const [isProxyOpen, toggleProxy] = useToggle();
const [isLocalOpen, toggleLocal] = useToggle();
const [isQrOpen, toggleQr] = useToggle();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
const [balances, setBalances] = useState<Balances>({ accounts: {} });
@ -197,6 +202,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
(): Record<GroupName, [React.ReactNode?, string?, number?, (() => void)?][]> => {
const ret: Record<GroupName, [React.ReactNode?, string?, number?, (() => void)?][]> = {
accounts: [[<>{t('accounts')}<div className='sub'>{t('all locally stored accounts')}</div></>]],
chopsticks: [[<>{t('chopsticks')}<div className='sub'>{t('local accounts added via chopsticks fork')}</div></>]],
hardware: [[<>{t('hardware')}<div className='sub'>{t('accounts managed via hardware devices')}</div></>]],
injected: [[<>{t('extension')}<div className='sub'>{t('accounts available via browser extensions')}</div></>]],
multisig: [[<>{t('multisig')}<div className='sub'>{t('on-chain multisig accounts')}</div></>]],
@ -287,6 +293,12 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
{isLedgerOpen && (
<Ledger onClose={toggleLedger} />
)}
{isLocalOpen && (
<Local
onClose={toggleLocal}
onStatusChange={onStatusChange}
/>
)}
{isMultisigOpen && (
<Multisig
onClose={toggleMultisig}
@ -377,6 +389,13 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
)}
</>
)}
{fork && (
<Button
icon='plus'
label={t('Local')}
onClick={toggleLocal}
/>
)}
</Button.Group>
</SummaryBox>
{!isNextTick || !sortedAccounts.length

View File

@ -0,0 +1,132 @@
// Copyright 2017-2024 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { ModalProps } from '../types.js';
import { setStorage } from '@acala-network/chopsticks-core';
import React, { useCallback, useState } from 'react';
import { Button, Input, InputAddressSimple, Modal } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { useTranslation } from '../translate.js';
interface Props extends ModalProps {
className?: string;
onClose: () => void;
onStatusChange: (status: ActionStatus) => void;
}
interface CreateOptions {
balance: number;
name: string;
tags?: string[];
}
export async function createLocalAccount (chain: Blockchain, address: string, { balance, name }: CreateOptions): Promise<ActionStatus> {
const status = { action: 'create' } as ActionStatus;
try {
keyring.addExternal(address, { isLocal: true, name, tags: ['local'] });
await setStorage(chain, {
System: {
Account: [
[[address], { data: { free: balance * 1e12 }, providers: 1 }]
]
}
});
status.account = address;
status.status = 'success';
status.message = 'Local account created.';
} catch (error) {
status.status = 'error';
status.message = (error as Error).message;
}
return status;
}
function LocalAdd ({ className = '', onClose, onStatusChange }: Props): React.ReactElement<Props> {
const { fork: chain } = useApi();
const { t } = useTranslation();
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [address, setAdress] = useState<string | null>(null);
const [balance, setBalance] = useState<string>('0');
const _createLocalAccount = useCallback(
async () => {
if (!address || !chain) {
return;
}
const options = { balance: balance ? Number(balance) : 0, name: name.trim() };
const status = await createLocalAccount(chain, address, options);
onStatusChange(status);
onClose();
},
[address, name, balance, chain, onClose, onStatusChange]
);
const _onChangeName = useCallback(
(name: string) => setName({ isNameValid: (name.trim().length >= 3), name }),
[]
);
const isValid = isNameValid && !!address;
return (
<Modal
className={className}
header={t('Add a mock account to chopsticks')}
onClose={onClose}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('Any address, this can override other accounts')}>
<InputAddressSimple
autoFocus
label={t('address')}
onChange={setAdress}
placeholder={t('e.g. 5Gg3...')}
/>
</Modal.Columns>
<Modal.Columns hint={t('The name for this account')}>
<Input
className='full'
isError={!isNameValid}
label={t('name')}
onChange={_onChangeName}
placeholder={t('local account name')}
/>
</Modal.Columns>
<Modal.Columns hint={t('Optional. The balance for this account Default to 0.')}>
<Input
className='full'
label={t('balance')}
min={0}
onChange={setBalance}
placeholder={t('1000')}
type='number'
value={balance}
/>
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<Button
icon='plus'
isDisabled={!isValid}
label={t('Add')}
onClick={_createLocalAccount}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(LocalAdd);

View File

@ -1,11 +1,12 @@
// Copyright 2017-2024 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { InjectedExtension } from '@polkadot/extension-inject/types';
import type { ChainProperties, ChainType } from '@polkadot/types/interfaces';
import type { KeyringStore } from '@polkadot/ui-keyring/types';
import type { ApiProps, ApiState } from './types.js';
import type { ApiProps, ApiState, InjectedAccountExt } from './types.js';
import { ChopsticksProvider, setStorage } from '@acala-network/chopsticks-core';
import * as Sc from '@substrate/connect';
@ -36,15 +37,6 @@ interface Props {
store?: KeyringStore;
}
interface InjectedAccountExt {
address: string;
meta: {
name: string;
source: string;
whenCreated: number;
};
}
interface ChainData {
injectedAccounts: InjectedAccountExt[];
properties: ChainProperties;
@ -54,6 +46,11 @@ interface ChainData {
systemVersion: string;
}
interface CreateApiReturn {
types: Record<string, Record<string, string>>;
fork: Blockchain | null;
}
export const DEFAULT_DECIMALS = statics.registry.createType('u32', 12);
export const DEFAULT_SS58 = statics.registry.createType('u32', addressDefaults.prefix);
export const DEFAULT_AUX = ['Aux1', 'Aux2', 'Aux3', 'Aux4', 'Aux5', 'Aux6', 'Aux7', 'Aux8', 'Aux9'];
@ -134,7 +131,7 @@ async function retrieve (api: ApiPromise, injectedPromise: Promise<InjectedExten
};
}
async function loadOnReady (api: ApiPromise, endpoint: LinkOption | null, injectedPromise: Promise<InjectedExtension[]>, store: KeyringStore | undefined, types: Record<string, Record<string, string>>, urlIsEthereum = false): Promise<ApiState> {
async function loadOnReady (api: ApiPromise, endpoint: LinkOption | null, fork: Blockchain | null, injectedPromise: Promise<InjectedExtension[]>, store: KeyringStore | undefined, types: Record<string, Record<string, string>>, urlIsEthereum = false): Promise<ApiState> {
statics.registry.register(types);
const { injectedAccounts, properties, systemChain, systemChainType, systemName, systemVersion } = await retrieve(api, injectedPromise);
@ -189,6 +186,7 @@ async function loadOnReady (api: ApiPromise, endpoint: LinkOption | null, inject
apiDefaultTx,
apiDefaultTxSudo,
chainSS58,
fork,
hasInjectedAccounts: injectedAccounts.length !== 0,
isApiReady: true,
isDevelopment,
@ -230,17 +228,19 @@ async function getLightProvider (chain: string): Promise<ScProvider> {
/**
* @internal
*/
async function createApi (apiUrl: string, signer: ApiSigner, isLocalFork: boolean, onError: (error: unknown) => void): Promise<Record<string, Record<string, string>>> {
async function createApi (apiUrl: string, signer: ApiSigner, isLocalFork: boolean, onError: (error: unknown) => void): Promise<CreateApiReturn> {
const types = getDevTypes();
const isLight = apiUrl.startsWith('light://');
let provider;
let chopsticksFork: Blockchain | null = null;
try {
if (isLight) {
provider = await getLightProvider(apiUrl.replace('light://', ''));
} else if (isLocalFork) {
provider = await ChopsticksProvider.fromEndpoint(apiUrl);
await setStorage(provider.chain, {
chopsticksFork = provider.chain;
await setStorage(chopsticksFork, {
System: {
Account: [
[['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], { data: { free: 1000 * 1e12 }, providers: 1 }]
@ -267,7 +267,7 @@ async function createApi (apiUrl: string, signer: ApiSigner, isLocalFork: boolea
onError(error);
}
return types;
return { fork: chopsticksFork, types };
}
export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore }: Props): React.ReactElement<Props> | null {
@ -304,7 +304,7 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore
};
createApi(apiUrl, new ApiSigner(statics.registry, queuePayload, queueSetTxStatus), isLocalFork, onError)
.then((types): void => {
.then(({ fork, types }): void => {
statics.api.on('connected', () => setIsApiConnected(true));
statics.api.on('disconnected', () => setIsApiConnected(false));
statics.api.on('error', onError);
@ -317,7 +317,7 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore
const urlIsEthereum = !!location.href.includes('keyring-type=ethereum');
loadOnReady(statics.api, apiEndpoint, injectedPromise, keyringStore, types, urlIsEthereum)
loadOnReady(statics.api, apiEndpoint, fork, injectedPromise, keyringStore, types, urlIsEthereum)
.then(setState)
.catch(onError);
});

View File

@ -1,6 +1,7 @@
// Copyright 2017-2024 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type React from 'react';
import type { ApiPromise } from '@polkadot/api';
import type { SubmittableExtrinsicFunction } from '@polkadot/api/promise/types';
@ -15,10 +16,20 @@ export interface BareProps {
className?: string;
}
export interface InjectedAccountExt {
address: string;
meta: {
name: string;
source: string;
whenCreated: number;
};
}
export interface ApiState {
apiDefaultTx: SubmittableExtrinsicFunction;
apiDefaultTxSudo: SubmittableExtrinsicFunction;
chainSS58: number;
fork: Blockchain | null;
hasInjectedAccounts: boolean;
isApiReady: boolean;
isDevelopment: boolean;

View File

@ -21,7 +21,9 @@ export function getAccountCryptoType (accountId: AccountIdIsh): string {
? 'multisig'
: current.meta.isProxied
? 'proxied'
: 'qr'
: current.meta.isLocal
? 'chopsticks'
: 'qr'
: current.type;
}
} catch {

View File

@ -1,6 +1,7 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Blockchain } from '@acala-network/chopsticks-core';
import type { ApiPromise } from '@polkadot/api';
import type { SubmittableExtrinsicFunction } from '@polkadot/api/promise/types';
import type { HeaderExtended } from '@polkadot/api-derive/types';
@ -13,6 +14,7 @@ export interface ApiState {
apiDefaultTx: SubmittableExtrinsicFunction;
apiDefaultTxSudo: SubmittableExtrinsicFunction;
chainSS58: number;
fork: Blockchain | null;
hasInjectedAccounts: boolean;
isApiReady: boolean;
isDevelopment: boolean;

View File

@ -13,7 +13,7 @@ import type { BN } from '@polkadot/util';
import type { HexString } from '@polkadot/util/types';
import type { AddressFlags, AddressProxy, QrState } from './types.js';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { web3FromSource } from '@polkadot/extension-dapp';
import { Button, ErrorBoundary, Modal, Output, styled, Toggle } from '@polkadot/react-components';
@ -80,11 +80,31 @@ function unlockAccount ({ isUnlockCached, signAddress, signPassword }: AddressPr
return null;
}
async function signAndSend (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>): Promise<void> {
async function fakeSignForChopsticks (api: ApiPromise, tx: SubmittableExtrinsic<'promise'>, sender: string): Promise<void> {
const account = await api.query.system.account(sender);
const options = {
blockHash: api.genesisHash,
genesisHash: api.genesisHash,
nonce: account.nonce,
runtimeVersion: api.runtimeVersion
};
const mockSignature = new Uint8Array(64);
mockSignature.fill(0xcd);
mockSignature.set([0xde, 0xad, 0xbe, 0xef]);
tx.signFake(sender, options);
tx.signature.set(mockSignature);
}
async function signAndSend (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>, api: ApiPromise, isMockSign: boolean): Promise<void> {
currentItem.txStartCb && currentItem.txStartCb();
try {
await tx.signAsync(pairOrAddress, options);
if (!isMockSign) {
await tx.signAsync(pairOrAddress, options);
} else {
await fakeSignForChopsticks(api, tx, pairOrAddress as string);
}
console.info('sending', tx.toHex());
@ -101,11 +121,15 @@ async function signAndSend (queueSetTxStatus: QueueTxMessageSetStatus, currentIt
}
}
async function signAsync (queueSetTxStatus: QueueTxMessageSetStatus, { id, txFailedCb = NOOP, txStartCb = NOOP }: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>): Promise<string | null> {
async function signAsync (queueSetTxStatus: QueueTxMessageSetStatus, { id, txFailedCb = NOOP, txStartCb = NOOP }: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>, api: ApiPromise, isMockSign: boolean): Promise<string | null> {
txStartCb();
try {
await tx.signAsync(pairOrAddress, options);
if (!isMockSign) {
await tx.signAsync(pairOrAddress, options);
} else {
await fakeSignForChopsticks(api, tx, pairOrAddress as string);
}
return tx.toJSON();
} catch (error) {
@ -166,14 +190,16 @@ async function wrapTx (api: ApiPromise, currentItem: QueueTx, { isMultiCall, mul
return tx;
}
async function extractParams (api: ApiPromise, address: string, options: Partial<SignerOptions>, getLedger: () => Ledger, setQrState: (state: QrState) => void): Promise<['qr' | 'signing', string, Partial<SignerOptions>]> {
async function extractParams (api: ApiPromise, address: string, options: Partial<SignerOptions>, getLedger: () => Ledger, setQrState: (state: QrState) => void): Promise<['qr' | 'signing', string, Partial<SignerOptions>, boolean]> {
const pair = keyring.getPair(address);
const { meta: { accountOffset, addressOffset, isExternal, isHardware, isInjected, isProxied, source } } = pair;
const { meta: { accountOffset, addressOffset, isExternal, isHardware, isInjected, isLocal, isProxied, source } } = pair;
if (isHardware) {
return ['signing', address, { ...options, signer: new LedgerSigner(api.registry, getLedger, accountOffset || 0, addressOffset || 0) }];
return ['signing', address, { ...options, signer: new LedgerSigner(api.registry, getLedger, accountOffset || 0, addressOffset || 0) }, false];
} else if (isLocal) {
return ['signing', address, { ...options, signer: new AccountSigner(api.registry, pair) }, true];
} else if (isExternal && !isProxied) {
return ['qr', address, { ...options, signer: new QrSigner(api.registry, setQrState) }];
return ['qr', address, { ...options, signer: new QrSigner(api.registry, setQrState) }, false];
} else if (isInjected) {
if (!source) {
throw new Error(`Unable to find injected source for ${address}`);
@ -183,12 +209,12 @@ async function extractParams (api: ApiPromise, address: string, options: Partial
assert(injected, `Unable to find a signer for ${address}`);
return ['signing', address, { ...options, signer: injected.signer }];
return ['signing', address, { ...options, signer: injected.signer }, false];
}
assert(addressEq(address, pair.address), `Unable to retrieve keypair for ${address}`);
return ['signing', address, { ...options, signer: new AccountSigner(api.registry, pair) }];
return ['signing', address, { ...options, signer: new AccountSigner(api.registry, pair) }, false];
}
function tryExtract (address: string | null): AddressFlags {
@ -298,14 +324,14 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
const _onSend = useCallback(
async (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, senderInfo: AddressProxy): Promise<void> => {
if (senderInfo.signAddress) {
const [tx, [status, pairOrAddress, options]] = await Promise.all([
const [tx, [status, pairOrAddress, options, isMockSign]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(api, senderInfo.signAddress, { nonce: -1, tip }, getLedger, setQrState)
]);
queueSetTxStatus(currentItem.id, status);
await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options);
await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options, api, isMockSign);
}
},
[api, getLedger, tip]
@ -314,12 +340,12 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
const _onSign = useCallback(
async (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, senderInfo: AddressProxy): Promise<void> => {
if (senderInfo.signAddress) {
const [tx, [, pairOrAddress, options]] = await Promise.all([
const [tx, [, pairOrAddress, options, isMockSign]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(api, senderInfo.signAddress, { ...signedOptions, tip }, getLedger, setQrState)
]);
setSignedTx(await signAsync(queueSetTxStatus, currentItem, tx, pairOrAddress, options));
setSignedTx(await signAsync(queueSetTxStatus, currentItem, tx, pairOrAddress, options, api, isMockSign));
}
},
[api, getLedger, signedOptions, tip]
@ -357,6 +383,24 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
[_onSend, _onSendPayload, _onSign, _unlock, currentItem, isSubmit, queueSetTxStatus, senderInfo]
);
const signLabel = useMemo(() => {
if (flags.isQr) {
return t('Sign via Qr');
} else if (isSubmit) {
if (flags.isLocal) {
return t('Mock Sign and Submit');
} else {
return t('Sign and Submit');
}
} else {
if (flags.isLocal) {
return t('Mock Sign (no submission)');
} else {
return t('Sign (no submission)');
}
}
}, [flags.isQr, flags.isLocal, isSubmit, t]);
const isAutoCapable = senderInfo.signAddress && (queueSize > 1) && isSubmit && !(flags.isHardware || flags.isMultisig || flags.isProxied || flags.isQr || flags.isUnlockable) && !isRenderError;
if (!isBusy && isAutoCapable && initialIsQueueSubmit) {
@ -443,13 +487,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
}
isBusy={isBusy}
isDisabled={!senderInfo.signAddress || isRenderError}
label={
flags.isQr
? t('Sign via Qr')
: isSubmit
? t('Sign and Submit')
: t('Sign (no submission)')
}
label={signLabel}
onClick={_doStart}
tabIndex={2}
/>

View File

@ -8,6 +8,7 @@ export interface AddressFlags {
addressOffset: number;
hardwareType?: string;
isHardware: boolean;
isLocal: boolean;
isMultisig: boolean;
isProxied: boolean;
isQr: boolean;

View File

@ -9,7 +9,7 @@ import type { AddressFlags } from './types.js';
import { keyring } from '@polkadot/ui-keyring';
const NOOP = () => undefined;
const NO_FLAGS = { accountOffset: 0, addressOffset: 0, isHardware: false, isMultisig: false, isProxied: false, isQr: false, isUnlockable: false, threshold: 0, who: [] };
const NO_FLAGS = { accountOffset: 0, addressOffset: 0, isHardware: false, isLocal: false, isMultisig: false, isProxied: false, isQr: false, isUnlockable: false, threshold: 0, who: [] };
export const UNLOCK_MINS = 15;
@ -43,7 +43,7 @@ export function extractExternal (accountId: string | null): AddressFlags {
}
const pair = keyring.getPair(publicKey);
const { isExternal, isHardware, isInjected, isMultisig, isProxied } = pair.meta;
const { isExternal, isHardware, isInjected, isLocal, isMultisig, isProxied } = pair.meta;
const isUnlockable = !isExternal && !isHardware && !isInjected;
if (isUnlockable) {
@ -60,9 +60,10 @@ export function extractExternal (accountId: string | null): AddressFlags {
addressOffset: pair.meta.addressOffset || 0,
hardwareType: pair.meta.hardwareType as string,
isHardware: !!isHardware,
isLocal: !!isLocal,
isMultisig: !!isMultisig,
isProxied: !!isProxied,
isQr: !!isExternal && !isMultisig && !isProxied && !isHardware && !isInjected,
isQr: !!isExternal && !isMultisig && !isProxied && !isHardware && !isInjected && !isLocal,
isUnlockable: isUnlockable && pair.isLocked,
threshold: pair.meta.threshold || 0,
who: (pair.meta.who || []).map(recodeAddress)