feat: replace the Parity Signer QR flow with "track an address"

Keeps watch-only accounts, drops the protocol that cannot work.

ImportQr scanned a Parity Signer QR — SUBSTRATE_ID 0x53, CRYPTO_SR25519 0x01, a
crypto-type byte with no ML-DSA value — so no Quantus device could ever answer
it, and one branch hardcoded createAccountSuri(..., 'sr25519', ...). But it was
also the *only* caller of createAccountExternal, so deleting it would have taken
watch-only accounts with it. Those are wanted independently of how the address
arrives, and are what the account list is used for.

TrackAddress takes a pasted SS58 string instead, which for Quantus is the common
case anyway: the address is on screen in another wallet, not on a signing device.
It round-trips through decodeAddress/encodeAddress, which rejects a mistyped
address by its checksum rather than storing one that can never receive anything,
and normalises the prefix so an address pasted from a tool using a different one
displays the way the rest of the extension displays it.

The signing path had to change with it, and this is the part that would have been
a bug. `isExternal && !isHardware` previously rendered the QR signer — and a
tracked address satisfies exactly that condition, so leaving it would have shown
a Parity QR that nothing can scan, for an account that can never sign. External
accounts now show the decoded call and say plainly that the extension holds no
key for them.

That makes Signing/Qr.tsx unreachable, along with the CMD_MORTAL and
CMD_SIGN_MESSAGE Parity command bytes and the _onSignature callback, whose whole
job was accepting a signature produced outside the extension. All removed.

approveSignSignature in the background is deliberately left. Unlike Ledger, which
is gone for good, external signing returns when quantus/extension#10 ports the
flow to multipart UR — the message it carries is the right shape for that, and
deleting it would only mean writing it again.

Two specs rewritten rather than deleted: the one asserting a QR scanner appears
for external accounts now asserts the extrinsic and the cannot-sign warning
appear instead.

Typecheck, lint and 65 tests clean; build:chrome completes.

Refs quantus/extension#10, quantus/extension#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
rob thijssen
2026-09-15 12:19:08 +03:00
parent 87a0b3926c
commit 6dfd109991
12 changed files with 180 additions and 446 deletions

View File

@@ -576,7 +576,6 @@ export default class Extension {
case 'pri(accounts.create.external)':
return this.accountsCreateExternal(request as RequestAccountCreateExternal);
case 'pri(accounts.create.suri)':
return this.accountsCreateSuri(request as RequestAccountCreateSuri);

View File

@@ -18,7 +18,7 @@ export const MESSAGE_ORIGIN_CONTENT = `${PORT_PREFIX}-content`;
// '/account/import-ledger' is gone with the Ledger path: no hardware wallet
// speaks ML-DSA, so there is nothing for it to connect to. See quantus/extension#5.
export const ALLOWED_PATH = ['/', '/account/restore-json'] as const;
export const ALLOWED_PATH = ['/', '/account/restore-json', '/account/track-address'] as const;
export const PASSWORD_EXPIRY_MIN = 15;
export const PASSWORD_EXPIRY_MS = PASSWORD_EXPIRY_MIN * 60 * 1000;

View File

@@ -1,140 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@polkadot/extension-mocks/chrome';
import type { ReactWrapper } from 'enzyme';
import type * as _ from '@polkadot/dev-test/globals.d.ts';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import enzyme from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router';
import { Button } from '../components/index.js';
import * as messaging from '../messaging.js';
import { flushAllPromises } from '../testHelpers.js';
import ImportQr from './ImportQr.js';
const { configure, mount } = enzyme;
const mockedAccount = {
content: '12bxf6QJS5hMJgwbJMDjFot1sq93EvgQwyuPWENr9SzJfxtN',
expectedBannerChain: 'Polkadot',
genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
isAddress: true,
name: 'My Polkadot Account'
};
interface ScanType {
isAddress: boolean;
content: string;
genesisHash: string;
name?: string;
}
interface QrScanAddressProps {
className?: string;
onError?: (error: Error) => void;
onScan: (scanned: ScanType) => void;
size?: string | number;
style?: React.CSSProperties;
}
// // NOTE Required for spyOn when using @swc/jest
// // https://github.com/swc-project/swc/issues/3843
// jest.mock('../messaging', (): Record<string, unknown> => ({
// __esModule: true,
// ...jest.requireActual('../messaging')
// }));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
configure({ adapter: new Adapter() });
const typeName = async (wrapper: ReactWrapper, value: string) => {
wrapper.find('input').first().simulate('change', { target: { value } });
await act(flushAllPromises);
wrapper.update();
};
// jest.mock('@polkadot/react-qr', () => {
// return {
// QrScanAddress: (_: QrScanAddressProps): null => {
// return null;
// }
// };
// });
describe('ImportQr component', () => {
let wrapper: ReactWrapper;
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
wrapper = mount(
<MemoryRouter>
<ImportQr />
</MemoryRouter>
);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
act(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
(wrapper.find('QrScanAddress').first().prop('onScan') as unknown as QrScanAddressProps['onScan'])(mockedAccount);
});
await act(flushAllPromises);
wrapper.update();
});
describe('Address component', () => {
it('shows account as external', () => {
expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="qrcode"]').exists()).toBe(true);
});
it('shows the correct name', () => {
expect(wrapper.find('Name span').text()).toEqual(mockedAccount.name);
});
it('shows the correct address', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(mockedAccount.content);
});
it('shows the correct banner', () => {
expect(wrapper.find('[data-field="chain"]').text()).toEqual(mockedAccount.expectedBannerChain);
});
});
it('has the button enabled', () => {
expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
});
it('displays and error and the button is disabled with a short name', async () => {
await typeName(wrapper, 'a');
expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
});
it('has no error message and button enabled with a long name', async () => {
const longName = 'aaa';
await typeName(wrapper, 'a');
await typeName(wrapper, longName);
expect(wrapper.find('.warning-message')).toHaveLength(0);
expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
expect(wrapper.find('Name span').text()).toEqual(longName);
});
it('shows the external name in the input field', () => {
expect(wrapper.find('input').prop('value')).toBe(mockedAccount.name);
});
it('creates the external account', async () => {
jest.spyOn(messaging, 'createAccountExternal').mockImplementation(() => Promise.resolve(false));
wrapper.find(Button).simulate('click');
await act(flushAllPromises);
expect(messaging.createAccountExternal).toHaveBeenCalledWith(mockedAccount.name, mockedAccount.content, mockedAccount.genesisHash);
});
});

View File

@@ -1,115 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useContext, useState } from 'react';
import { QrScanAddress } from '@polkadot/react-qr';
import AccountNamePasswordCreation from '../components/AccountNamePasswordCreation.js';
import { ActionContext, Address, ButtonArea, NextStepButton, VerticalSpace } from '../components/index.js';
import { useTranslation } from '../hooks/index.js';
import { createAccountExternal, createAccountSuri, createSeed } from '../messaging.js';
import { Header, Name } from '../partials/index.js';
interface QrAccount {
content: string;
genesisHash: HexString | null;
isAddress: boolean;
name?: string;
}
export default function ImportQr (): React.ReactElement {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [account, setAccount] = useState<QrAccount | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);
const _setAccount = useCallback(
(qrAccount: QrAccount) => {
setAccount(qrAccount);
setName(qrAccount?.name || null);
if (qrAccount.isAddress) {
setAddress(qrAccount.content);
} else {
createSeed(undefined, qrAccount.content)
.then(({ address }) => setAddress(address))
.catch(console.error);
}
},
[]
);
const _onCreate = useCallback(
(): void => {
if (account && name) {
if (account.isAddress) {
createAccountExternal(name, account.content, account.genesisHash)
.then(() => onAction('/'))
.catch((error: Error) => console.error(error));
} else if (password) {
createAccountSuri(name, password, account.content, 'sr25519', account.genesisHash)
.then(() => onAction('/'))
.catch((error: Error) => console.error(error));
}
}
},
[account, name, onAction, password]
);
return (
<>
<Header
showBackArrow
text={t('Scan Address Qr')}
/>
{!account && (
<div>
<QrScanAddress onScan={_setAccount} />
</div>
)}
{account && (
<>
<div>
<Address
{...account}
address={address}
isExternal={true}
name={name}
/>
</div>
{account.isAddress
? (
<Name
isFocused
onChange={setName}
value={name || ''}
/>
)
: (
<AccountNamePasswordCreation
isBusy={false}
onCreate={_onCreate}
onNameChange={setName}
onPasswordChange={setPassword}
/>
)
}
<VerticalSpace />
<ButtonArea>
<NextStepButton
isDisabled={!name || (!account.isAddress && !password)}
onClick={_onCreate}
>
{t('Add the account with identified address')}
</NextStepButton>
</ButtonArea>
</>
)}
</>
);
}

View File

@@ -1,103 +0,0 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useMemo, useState } from 'react';
import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr';
import { u8aWrapBytes } from '@polkadot/util';
import { Button } from '../../components/index.js';
import { useTranslation } from '../../hooks/index.js';
import { styled } from '../../styled.js';
import { CMD_MORTAL, CMD_SIGN_MESSAGE } from './Request/index.js';
interface Props {
address: string;
children?: React.ReactNode;
className?: string;
cmd: number;
genesisHash: string;
onSignature: ({ signature }: { signature: HexString }) => void;
payload: ExtrinsicPayload | string;
}
function Qr ({ address, className, cmd, genesisHash, onSignature, payload }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isScanning, setIsScanning] = useState(false);
const payloadU8a = useMemo(
() => {
switch (cmd) {
case CMD_MORTAL:
return (payload as ExtrinsicPayload).toU8a();
case CMD_SIGN_MESSAGE:
return u8aWrapBytes(payload as string);
default:
return null;
}
},
[cmd, payload]
);
const _onShowQr = useCallback(
() => setIsScanning(true),
[]
);
if (!payloadU8a) {
return (
<div className={className}>
<div className='qrContainer'>
Transaction command:{cmd} not supported.
</div>
</div>
);
}
return (
<div className={className}>
<div className='qrContainer'>
{isScanning
? <QrScanSignature onScan={onSignature} />
: (
<QrDisplayPayload
address={address}
cmd={cmd}
genesisHash={genesisHash}
payload={payloadU8a}
/>
)
}
</div>
{!isScanning && (
<Button
className='scanButton'
onClick={_onShowQr}
>
{t('Scan signature via camera')}
</Button>
)}
</div>
);
}
export default styled(Qr)<Props>`
height: 100%;
.qrContainer {
margin: 5px auto 10px auto;
width: 65%;
img {
border: white solid 1px;
}
}
.scanButton {
margin-bottom: 8px;
}
`;

View File

@@ -3,19 +3,16 @@
import type { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
import type { HexString } from '@polkadot/util/types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { isExtrinsicRequest } from '@polkadot/extension-base/utils';
import { TypeRegistry } from '@polkadot/types';
import { ActionContext, Address, VerticalSpace, Warning } from '../../../components/index.js';
import { Address, VerticalSpace, Warning } from '../../../components/index.js';
import { useMetadata, useTranslation } from '../../../hooks/index.js';
import { approveSignSignature } from '../../../messaging.js';
import Bytes from '../Bytes.js';
import Extrinsic from '../Extrinsic.js';
import Qr from '../Qr.js';
import SignArea from './SignArea.js';
interface Props {
@@ -32,14 +29,10 @@ interface Data {
payload: ExtrinsicPayload | null;
}
export const CMD_MORTAL = 2;
export const CMD_SIGN_MESSAGE = 3;
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
export default function Request ({ account: { genesisHash: accountGenesisHash, isExternal, isHardware }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const onAction = useContext(ActionContext);
export default function Request ({ account: { isExternal, isHardware }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
@@ -77,18 +70,6 @@ export default function Request ({ account: { genesisHash: accountGenesisHash, i
}
}, [request]);
const _onSignature = useCallback(
({ signature }: { signature: HexString }, signedTransaction?: HexString): void => {
approveSignSignature(signId, signature, signedTransaction)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
});
},
[onAction, signId]
);
// Branch on the request itself rather than on the decoded state, so a render
// that lands before the effect has caught up shows nothing instead of feeding
// the previous request's view a payload of the other shape.
@@ -109,24 +90,17 @@ export default function Request ({ account: { genesisHash: accountGenesisHash, i
isHardware={isHardware}
/>
</div>
{isExternal && !isHardware
? (
<Qr
address={json.address}
cmd={CMD_MORTAL}
genesisHash={json.genesisHash}
onSignature={_onSignature}
payload={payload}
/>
)
: (
<Extrinsic
payload={payload}
request={json}
url={url}
/>
)
}
<Extrinsic
payload={payload}
request={json}
url={url}
/>
{isExternal && (
<>
<Warning>{t('This account is tracked only — the extension holds no key for it and cannot sign.')}</Warning>
<VerticalSpace />
</>
)}
<SignArea
buttonText={buttonText}
error={error}
@@ -152,32 +126,13 @@ export default function Request ({ account: { genesisHash: accountGenesisHash, i
isExternal={isExternal}
/>
</div>
{isExternal && !isHardware && accountGenesisHash
? (
<Qr
address={address}
cmd={CMD_SIGN_MESSAGE}
genesisHash={accountGenesisHash}
onSignature={_onSignature}
payload={data}
/>
)
: (
<Bytes
bytes={data}
url={url}
/>
)
}
<Bytes
bytes={data}
url={url}
/>
<VerticalSpace />
{isExternal && !isHardware && !accountGenesisHash && (
<>
<Warning isDanger>{t('"Allow use on any network" is not supported to show a QR code. You must associate this account with a network.')}</Warning>
<VerticalSpace />
</>
)}
{isHardware && <>
<Warning>{t('Message signing is not supported for hardware wallets.')}</Warning>
{isExternal && <>
<Warning>{t('This account is tracked only — the extension holds no key for it and cannot sign.')}</Warning>
<VerticalSpace />
</>}
<SignArea

View File

@@ -21,7 +21,6 @@ import Request from './Request/index.js';
import Extrinsic from './Extrinsic.js';
import Signing from './index.js';
import { westendMetadata } from './metadataMock.js';
import Qr from './Qr.js';
import TransactionIndex from './TransactionIndex.js';
const { configure, mount } = enzyme;
@@ -211,7 +210,13 @@ describe('Signing requests', () => {
});
describe('External account', () => {
it('shows Qr scanner for external accounts', async () => {
// Was "shows Qr scanner for external accounts". That flow round-tripped a
// signature through a Parity Signer QR, a protocol no Quantus device speaks
// (quantus/extension#10). An external account here is now a tracked address:
// the extension holds no key for it and there is no signer to hand off to, so
// the right behaviour is to show the call and say so rather than display a
// QR nothing can scan.
it('shows the extrinsic and a cannot-sign warning for external accounts', async () => {
signRequests = [{
account: {
address: '5Cf1CGZas62RWwce3d2EPqUvSoi1txaXKd9M5w9bEFSsQtRe',
@@ -251,8 +256,8 @@ describe('Signing requests', () => {
url: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fwestend-rpc.polkadot.io#/accounts'
}];
await mountComponent();
expect(wrapper.find(Extrinsic)).toHaveLength(0);
expect(wrapper.find(Qr)).toHaveLength(1);
expect(wrapper.find(Extrinsic)).toHaveLength(1);
expect(wrapper.text()).toContain('tracked only');
});
});

View File

@@ -0,0 +1,135 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useContext, useState } from 'react';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import { ActionContext, Address, ButtonArea, NextStepButton, VerticalSpace, Warning } from '../components/index.js';
import { useTranslation } from '../hooks/index.js';
import { createAccountExternal } from '../messaging.js';
import { Header, Name } from '../partials/index.js';
import { styled } from '../styled.js';
interface Props {
className?: string;
}
// Replaces ImportQr as the way to add an account the extension holds no key for.
//
// That flow scanned a Parity Signer QR, a protocol no Quantus device speaks
// (quantus/extension#10) — but it was also the *only* caller of
// createAccountExternal, so removing it would have taken watch-only accounts with
// it. Those are wanted independently of how the address arrives, and for Quantus
// pasting is the common case anyway: the address is on screen in another wallet,
// not on a signing device.
function TrackAddress ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [address, setAddress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState<string | null>(null);
const _onChangeAddress = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
const value = event.target.value;
const trimmed = value.trim();
if (!trimmed) {
setAddress(null);
setError(null);
return;
}
try {
// Round-tripping through decode/encode does two jobs: it rejects a
// mistyped address by its checksum rather than storing something that
// will never receive anything, and it normalises the prefix so an
// address pasted from a tool using a different one is stored the way the
// rest of the extension will display it.
setAddress(encodeAddress(decodeAddress(trimmed)));
setError(null);
} catch {
setAddress(null);
setError(t('This is not a valid address'));
}
},
[t]
);
const _onCreate = useCallback(
(): void => {
if (address && name) {
// genesisHash null — "any network". A tracked address is just an
// address; nothing here knows which chain the holder uses it on.
createAccountExternal(name, address, null)
.then(() => onAction('/'))
.catch((error: Error) => {
console.error(error);
setError((error).message);
});
}
},
[address, name, onAction]
);
return (
<>
<Header
showBackArrow
text={t('Track an address')}
/>
<div className={className}>
<Address
address={address}
isExternal={true}
name={name}
/>
<textarea
autoFocus
className='address'
onChange={_onChangeAddress}
placeholder={t('Paste the address you want to track')}
rows={3}
/>
{error && (
<Warning isDanger>{error}</Warning>
)}
<Name
onChange={setName}
value={name || ''}
/>
<Warning>
{t('This account has no key in the extension, so it cannot sign. You will see its balance and can use it as a recipient.')}
</Warning>
</div>
<VerticalSpace />
<ButtonArea>
<NextStepButton
isDisabled={!address || !name}
onClick={_onCreate}
>
{t('Add the address')}
</NextStepButton>
</ButtonArea>
</>
);
}
export default React.memo(styled(TrackAddress)<Props>`
.address {
background: var(--readonlyInputBackground);
border-color: var(--inputBorderColor);
border-radius: var(--borderRadius);
border-style: solid;
border-width: 1px;
color: var(--textColor);
font-family: inherit;
font-size: var(--inputLabelFontSize);
margin-top: 16px;
padding: 12px;
resize: none;
width: 100%;
}
`);

View File

@@ -29,9 +29,9 @@ import AssetHubMigration from './AssetHubMigration.js';
import Export from './Export.js';
import ExportAll from './ExportAll.js';
import Forget from './Forget.js';
import ImportQr from './ImportQr.js';
import PhishingDetected from './PhishingDetected.js';
import RestoreJson from './RestoreJson.js';
import TrackAddress from './TrackAddress.js';
import Welcome from './Welcome.js';
const startSettings = settings.get();
@@ -160,7 +160,7 @@ export default function Popup (): React.ReactElement {
<Route path='/account/forget/:address'>{wrapWithErrorBoundary(<Forget />, 'forget-address')}</Route>
<Route path='/account/export/:address'>{wrapWithErrorBoundary(<Export />, 'export-address')}</Route>
<Route path='/account/export-all'>{wrapWithErrorBoundary(<ExportAll />, 'export-all-address')}</Route>
<Route path='/account/import-qr'>{wrapWithErrorBoundary(<ImportQr />, 'import-qr')}</Route>
<Route path='/account/track-address'>{wrapWithErrorBoundary(<TrackAddress />, 'track-address')}</Route>
<Route path='/account/import-seed'>{wrapWithErrorBoundary(<ImportSeed />, 'import-seed')}</Route>
<Route path='/account/restore-json'>{wrapWithErrorBoundary(<RestoreJson />, 'restore-json')}</Route>
<Route path='/account/derive/:address/locked'>{wrapWithErrorBoundary(<Derive isLocked />, 'derived-address-locked')}</Route>

View File

@@ -1,11 +1,11 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { faCodeBranch, faFileExport, faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
import { faCodeBranch, faEye, faFileExport, faFileUpload, faKey, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useContext } from 'react';
import { AccountContext, Link, MediaContext, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { AccountContext, Link, Menu, MenuDivider, MenuItem } from '../components/index.js';
import { useIsPopup, useTranslation } from '../hooks/index.js';
import { windowOpen } from '../messaging.js';
import { styled } from '../styled.js';
@@ -20,7 +20,6 @@ const jsonPath = '/account/restore-json';
function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { master } = useContext(AccountContext);
const mediaAllowed = useContext(MediaContext);
const isPopup = useIsPopup();
const _openJson = useCallback(
@@ -75,16 +74,9 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
</MenuItem>
<MenuDivider />
<MenuItem className='menuItem'>
<Link
isDisabled={!mediaAllowed}
title={!mediaAllowed
? t('Camera access must be first enabled in the settings')
: ''
}
to='/account/import-qr'
>
<FontAwesomeIcon icon={faQrcode} />
<span>{t('Attach external QR-signer account')}</span>
<Link to='/account/track-address'>
<FontAwesomeIcon icon={faEye} />
<span>{t('Track an address')}</span>
</Link>
</MenuItem>
</Menu>

View File

@@ -82,7 +82,6 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
}, []
);
const _goToAuthList = useCallback(
() => {
onAction('auth-list');

View File

@@ -199,5 +199,12 @@
"I Understand": "",
"Do not teleport balances to the Relay Chain unless:": "",
"Address mismatch: derived {{derived}}, expected {{expected}}. Check that the correct Ledger device is connected and the correct app is selected.": "",
"Possible cause: the Ledger App setting differs from the app originally used to derive this account.": ""
"Possible cause: the Ledger App setting differs from the app originally used to derive this account.": "",
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.": "",
"Track an address": "",
"This account is tracked only — the extension holds no key for it and cannot sign.": "",
"This is not a valid address": "",
"Paste the address you want to track": "",
"This account has no key in the extension, so it cannot sign. You will see its balance and can use it as a recipient.": "",
"Add the address": ""
}