Move to Ledger usage with hook (#4195)

* Move ledgerChains to apps-config

* Swap Ledger capabilities to hook

* Fix type usage

* getLedger returns promise

* Revert promise result

* cleanup

* use has ledger support (#4413)

* use hasLedgerSupport

* Bump common

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>
This commit is contained in:
Jaco Greeff 2021-01-14 20:58:02 +01:00 committed by GitHub
parent 3ba546d77d
commit 19d49f09be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 159 additions and 97 deletions

View File

@ -3,11 +3,12 @@
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { DeriveBalancesAll, DeriveDemocracyLock } from '@polkadot/api-derive/types';
import type { Ledger } from '@polkadot/ledger';
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { ThemeDef } from '@polkadot/react-components/types';
import type { Option } from '@polkadot/types';
import type { ProxyDefinition, RecoveryConfig } from '@polkadot/types/interfaces';
import type { KeyringAddress } from '@polkadot/ui-keyring/types';
import type { KeyringAddress, KeyringJson$Meta } from '@polkadot/ui-keyring/types';
import type { Delegation } from '../types';
import BN from 'bn.js';
@ -15,9 +16,8 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
import styled, { ThemeContext } from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { getLedger } from '@polkadot/react-api';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
import { useAccountInfo, useApi, useCall, useLedger, useToggle } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { BN_ZERO, formatBalance, formatNumber } from '@polkadot/util';
@ -74,6 +74,12 @@ function createClearDemocracyTx (api: ApiPromise, address: string, unlockableIds
);
}
async function showLedgerAddress (getLedger: () => Ledger, meta: KeyringJson$Meta): Promise<void> {
const ledger = getLedger();
await ledger.getAddress(true, meta.accountOffset as number || 0, meta.addressOffset as number || 0);
}
const transformRecovery = {
transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
};
@ -83,6 +89,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
const { theme } = useContext<ThemeDef>(ThemeContext);
const { queueExtrinsic } = useContext(StatusContext);
const api = useApi();
const { getLedger } = useLedger();
const bestNumber = useCall<BN>(api.api.derive.chain.bestNumber);
const balancesAll = useCall<DeriveBalancesAll>(api.api.derive.balances.all, [address]);
const democracyLocks = useCall<DeriveDemocracyLock[]>(api.api.derive.democracy?.locks, [address]);
@ -191,13 +198,11 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
// TODO: we should check the hardwareType from metadata here as well,
// for now we are always assuming hardwareType === 'ledger'
(): void => {
getLedger()
.getAddress(true, meta.accountOffset as number || 0, meta.addressOffset as number || 0)
.catch((error): void => {
console.error(`ledger: ${(error as Error).message}`);
});
showLedgerAddress(getLedger, meta).catch((error): void => {
console.error(`ledger: ${(error as Error).message}`);
});
},
[meta]
[getLedger, meta]
);
if (!isVisible) {

View File

@ -9,9 +9,8 @@ import BN from 'bn.js';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { isLedger } from '@polkadot/react-api';
import { Button, Input, Table } from '@polkadot/react-components';
import { useAccounts, useApi, useCall, useFavorites, useIpfs, useLoadingDelay, useToggle } from '@polkadot/react-hooks';
import { useAccounts, useApi, useCall, useFavorites, useIpfs, useLedger, useLoadingDelay, useToggle } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { BN_ZERO } from '@polkadot/util';
@ -49,6 +48,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
const { api } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const { isIpfs } = useIpfs();
const { isLedgerEnabled } = useLedger();
const [isCreateOpen, toggleCreate] = useToggle();
const [isImportOpen, toggleImport] = useToggle();
const [isLedgerOpen, toggleLedger] = useToggle();
@ -208,7 +208,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
label={t<string>('Add via Qr')}
onClick={toggleQr}
/>
{isLedger() && (
{isLedgerEnabled && (
<>
<Button
icon='question'

View File

@ -1,10 +1,12 @@
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Ledger } from '@polkadot/ui-keyring';
import React, { useCallback, useRef, useState } from 'react';
import { getLedger } from '@polkadot/react-api';
import { Button, Dropdown, MarkError, Modal } from '@polkadot/react-components';
import { useLedger } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { useTranslation } from '../translate';
@ -22,21 +24,15 @@ interface Props {
const AVAIL = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
// query the ledger for the address, adding it to the keyring
async function queryLedger (accountOffset: number, addressOffset: number): Promise<void> {
try {
const ledger = getLedger();
const { address } = await ledger.getAddress(false, accountOffset, addressOffset);
async function queryLedger (getLedger: () => Ledger, accountOffset: number, addressOffset: number): Promise<void> {
const { address } = await getLedger().getAddress(false, accountOffset, addressOffset);
keyring.addHardware(address, 'ledger', { accountOffset, addressOffset, name: `ledger ${accountOffset}/${addressOffset}` });
} catch (error) {
console.error(error);
throw error;
}
keyring.addHardware(address, 'ledger', { accountOffset, addressOffset, name: `ledger ${accountOffset}/${addressOffset}` });
}
function Ledger ({ className, onClose }: Props): React.ReactElement<Props> {
function LedgerModal ({ className, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { getLedger } = useLedger();
const [accIndex, setAccIndex] = useState(0);
const [addIndex, setAddIndex] = useState(0);
const [error, setError] = useState<Error | null>(null);
@ -57,14 +53,16 @@ function Ledger ({ className, onClose }: Props): React.ReactElement<Props> {
setError(null);
setIsBusy(true);
queryLedger(accIndex, addIndex)
queryLedger(getLedger, accIndex, addIndex)
.then(() => onClose())
.catch((error): void => {
console.error(error);
setIsBusy(false);
setError(error);
});
},
[accIndex, addIndex, onClose]
[accIndex, addIndex, getLedger, onClose]
);
return (
@ -118,4 +116,4 @@ function Ledger ({ className, onClose }: Props): React.ReactElement<Props> {
);
}
export default React.memo(Ledger);
export default React.memo(LedgerModal);

View File

@ -7,8 +7,8 @@ import type { SettingsStruct } from '@polkadot/ui-settings/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createLanguages, createSs58 } from '@polkadot/apps-config';
import { isLedgerCapable } from '@polkadot/react-api';
import { Button, Dropdown } from '@polkadot/react-components';
import { useLedger } from '@polkadot/react-hooks';
import { settings } from '@polkadot/ui-settings';
import { useTranslation } from './translate';
@ -22,6 +22,7 @@ const ledgerConnOptions = settings.availableLedgerConn;
function General ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { isLedgerCapable } = useLedger();
// tri-state: null = nothing changed, false = no reload, true = reload required
const [changed, setChanged] = useState<boolean | null>(null);
const [state, setSettings] = useState((): SettingsStruct => {
@ -108,7 +109,7 @@ function General ({ className = '' }: Props): React.ReactElement<Props> {
options={iconOptions}
/>
</div>
{isLedgerCapable() && (
{isLedgerCapable && (
<div className='ui--row'>
<Dropdown
defaultValue={ledgerConn}

View File

@ -4,7 +4,6 @@
import Api, { api, DEFAULT_DECIMALS, DEFAULT_SS58 } from './Api';
import ApiContext from './ApiContext';
import { withApi, withCallDiv, withCalls, withMulti, withObservable } from './hoc';
import { clearLedger, getLedger, isLedger, isLedgerCapable } from './ledger';
export {
api,
@ -16,9 +15,5 @@ export {
withCalls,
withCallDiv,
withMulti,
withObservable,
clearLedger,
getLedger,
isLedger,
isLedgerCapable
withObservable
};

View File

@ -1,44 +0,0 @@
// Copyright 2017-2021 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import networks from '@polkadot/networks';
import { Ledger } from '@polkadot/ui-keyring';
import uiSettings from '@polkadot/ui-settings';
import { assert } from '@polkadot/util';
import { api } from './Api';
let ledger: Ledger | null = null;
const ledgerChains = networks.filter((network) => network.hasLedgerSupport);
export function isLedgerCapable (): boolean {
try {
return (
!!(window as unknown as { USB?: unknown }).USB &&
!!api && ledgerChains.map(({ genesisHash }) => genesisHash[0]).includes(api.genesisHash.toHex())
);
} catch (error) {
return false;
}
}
export function isLedger (): boolean {
return isLedgerCapable() && uiSettings.ledgerConn !== 'none';
}
export function clearLedger (): void {
ledger = null;
}
export function getLedger (): Ledger {
if (!ledger) {
const genesisHex = api.genesisHash.toHex();
const def = api && ledgerChains.find(({ genesisHash }) => genesisHash[0] === genesisHex);
assert(def, `Unable to find supported chain for ${genesisHex}`);
ledger = new Ledger(uiSettings.ledgerConn as 'u2f', def.network);
}
return ledger;
}

View File

@ -20,6 +20,7 @@ export { useIncrement } from './useIncrement';
export { calcInflation, useInflation } from './useInflation';
export { useIpfs } from './useIpfs';
export { useIsMountedRef } from './useIsMountedRef';
export { useLedger } from './useLedger';
export { useLoadingDelay } from './useLoadingDelay';
export { useMembers } from './useMembers';
export { useModal } from './useModal';

View File

@ -0,0 +1,67 @@
// Copyright 2017-2020 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import { useCallback, useMemo } from 'react';
import { Ledger } from '@polkadot/ledger';
import networks from '@polkadot/networks';
import uiSettings from '@polkadot/ui-settings';
import { assert } from '@polkadot/util';
import { useApi } from './useApi';
interface StateBase {
isLedgerCapable: boolean;
isLedgerEnabled: boolean;
}
interface State extends StateBase {
getLedger: () => Ledger;
}
const EMPTY_STATE: StateBase = {
isLedgerCapable: false,
isLedgerEnabled: false
};
const hasWebUsb = !!(window as unknown as { USB?: unknown }).USB;
let ledger: Ledger | null = null;
const ledgerChains = networks.filter((network) => network.hasLedgerSupport);
function retrieveLedger (api: ApiPromise): Ledger {
if (!ledger) {
const genesisHex = api.genesisHash.toHex();
const def = ledgerChains.find(({ genesisHash }) => genesisHash[0] === genesisHex);
assert(def, `Unable to find supported chain for ${genesisHex}`);
ledger = new Ledger(uiSettings.ledgerConn as 'u2f', def.network);
}
return ledger;
}
function getState (api: ApiPromise): StateBase {
const isLedgerCapable = hasWebUsb && ledgerChains.map(({ genesisHash }) => genesisHash[0]).includes(api.genesisHash.toHex());
return {
isLedgerCapable,
isLedgerEnabled: isLedgerCapable && uiSettings.ledgerConn !== 'none'
};
}
export function useLedger (): State {
const { api, isApiReady } = useApi();
const getLedger = useCallback(
() => retrieveLedger(api),
[api]
);
return useMemo(
() => ({ ...(isApiReady ? getState(api) : EMPTY_STATE), getLedger }),
[api, getLedger, isApiReady]
);
}

View File

@ -7,6 +7,7 @@ import type { KeyringPair } from '@polkadot/keyring/types';
import type { QueueTx, QueueTxMessageSetStatus } from '@polkadot/react-components/Status/types';
import type { Option } from '@polkadot/types';
import type { Multisig, Timepoint } from '@polkadot/types/interfaces';
import type { Ledger } from '@polkadot/ui-keyring';
import type { AddressProxy, QrState } from './types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
@ -15,7 +16,7 @@ import styled from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { web3FromSource } from '@polkadot/extension-dapp';
import { Button, ErrorBoundary, Modal, Output, StatusContext, Toggle } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { useApi, useLedger, useToggle } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { assert, BN_ZERO } from '@polkadot/util';
@ -137,12 +138,12 @@ async function wrapTx (api: ApiPromise, currentItem: QueueTx, { isMultiCall, mul
return tx;
}
async function extractParams (api: ApiPromise, address: string, options: Partial<SignerOptions>, 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>]> {
const pair = keyring.getPair(address);
const { meta: { accountOffset, addressOffset, isExternal, isHardware, isInjected, source } } = pair;
if (isHardware) {
return ['signing', address, { ...options, signer: new LedgerSigner(api.registry, accountOffset as number || 0, addressOffset as number || 0) }];
return ['signing', address, { ...options, signer: new LedgerSigner(api.registry, getLedger, accountOffset as number || 0, addressOffset as number || 0) }];
} else if (isExternal) {
return ['qr', address, { ...options, signer: new QrSigner(api.registry, setQrState) }];
} else if (isInjected) {
@ -157,8 +158,9 @@ async function extractParams (api: ApiPromise, address: string, options: Partial
}
function TxSigned ({ className, currentItem, requestAddress }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const { t } = useTranslation();
const { api } = useApi();
const { getLedger } = useLedger();
const { queueSetTxStatus } = useContext(StatusContext);
const [flags, setFlags] = useState(extractExternal(requestAddress));
const [error, setError] = useState<Error | null>(null);
@ -240,7 +242,7 @@ function TxSigned ({ className, currentItem, requestAddress }: Props): React.Rea
if (senderInfo.signAddress) {
const [tx, [status, pairOrAddress, options]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(api, senderInfo.signAddress, { nonce: -1, tip }, setQrState)
extractParams(api, senderInfo.signAddress, { nonce: -1, tip }, getLedger, setQrState)
]);
queueSetTxStatus(currentItem.id, status);
@ -248,7 +250,7 @@ function TxSigned ({ className, currentItem, requestAddress }: Props): React.Rea
await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options);
}
},
[api, tip]
[api, getLedger, tip]
);
const _onSign = useCallback(
@ -256,13 +258,13 @@ function TxSigned ({ className, currentItem, requestAddress }: Props): React.Rea
if (senderInfo.signAddress) {
const [tx, [, pairOrAddress, options]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(api, senderInfo.signAddress, { ...signedOptions, tip }, setQrState)
extractParams(api, senderInfo.signAddress, { ...signedOptions, tip }, getLedger, setQrState)
]);
setSignedTx(await signAsync(queueSetTxStatus, currentItem, tx, pairOrAddress, options));
}
},
[api, signedOptions, tip]
[api, getLedger, signedOptions, tip]
);
const _doStart = useCallback(

View File

@ -3,25 +3,26 @@
import type { Signer, SignerResult } from '@polkadot/api/types';
import type { Registry, SignerPayloadJSON } from '@polkadot/types/types';
import { getLedger } from '@polkadot/react-api';
import type { Ledger } from '@polkadot/ui-keyring';
let id = 0;
export default class LedgerSigner implements Signer {
readonly #accountOffset: number;
readonly #addressOffset: number;
readonly #getLedger: () => Ledger;
readonly #registry: Registry;
constructor (registry: Registry, accountOffset: number, addressOffset: number) {
constructor (registry: Registry, getLedger: () => Ledger, accountOffset: number, addressOffset: number) {
this.#accountOffset = accountOffset;
this.#addressOffset = addressOffset;
this.#getLedger = getLedger;
this.#registry = registry;
}
public async signPayload (payload: SignerPayloadJSON): Promise<SignerResult> {
const raw = this.#registry.createType('ExtrinsicPayload', payload, { version: payload.version });
const { signature } = await getLedger().sign(raw.toU8a(true), this.#accountOffset, this.#addressOffset);
const { signature } = await this.#getLedger().sign(raw.toU8a(true), this.#accountOffset, this.#addressOffset);
return { id: ++id, signature };
}

View File

@ -3754,7 +3754,14 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:4.12.0, @typescript-eslint/typescript-estree@npm:^4.8.2":
"@typescript-eslint/types@npm:4.13.0":
version: 4.13.0
resolution: "@typescript-eslint/types@npm:4.13.0"
checksum: ed2e27ad1f7d0db28d13d3a5fe900b7638981689fea1b5556de4fcd87729f73cfe2e3792005cf4e05ba4c405f2abe9bc55c076354d4ac3f9576ded65c1b4ddf5
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:4.12.0":
version: 4.12.0
resolution: "@typescript-eslint/typescript-estree@npm:4.12.0"
dependencies:
@ -3773,6 +3780,25 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:^4.8.2":
version: 4.13.0
resolution: "@typescript-eslint/typescript-estree@npm:4.13.0"
dependencies:
"@typescript-eslint/types": 4.13.0
"@typescript-eslint/visitor-keys": 4.13.0
debug: ^4.1.1
globby: ^11.0.1
is-glob: ^4.0.1
lodash: ^4.17.15
semver: ^7.3.2
tsutils: ^3.17.1
peerDependenciesMeta:
typescript:
optional: true
checksum: 8b7485f192f247d659779f1bc95d927612e53b23801981acc462c2fd76219a61ac1094348bf7ac46034b58c1751793053128d11264268e378241eb6e686f5e8e
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:4.12.0":
version: 4.12.0
resolution: "@typescript-eslint/visitor-keys@npm:4.12.0"
@ -3783,6 +3809,16 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:4.13.0":
version: 4.13.0
resolution: "@typescript-eslint/visitor-keys@npm:4.13.0"
dependencies:
"@typescript-eslint/types": 4.13.0
eslint-visitor-keys: ^2.0.0
checksum: 35f20062885da380ef9d6e07f058ea9629a36b3d264dcf7e776e81da7c8f544373a287d6f49c88fd4fc546bb4afea27efdcd4e5be972c4202746b46cf1682a2d
languageName: node
linkType: hard
"@vue/component-compiler-utils@npm:^3.2.0":
version: 3.2.0
resolution: "@vue/component-compiler-utils@npm:3.2.0"
@ -17896,10 +17932,10 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"typedoc-default-themes@npm:0.12.0":
version: 0.12.0
resolution: "typedoc-default-themes@npm:0.12.0"
checksum: 112a092fe01a9fde6255cd0992c952cb28056efb6c1fb13426131be21cbbda283a9f27d294fdc10f51fa8ab5b0b47a1da74d908e24e7ad0d814939a6eb629a86
"typedoc-default-themes@npm:0.12.1":
version: 0.12.1
resolution: "typedoc-default-themes@npm:0.12.1"
checksum: 491e8aeb5192760c6b9a563140a87a7f10322750e23412ffa526e5a417c366992321332db45096d5717dadf3f00a611b06e1d495ead0707c27d80f8d3a42bf58
languageName: node
linkType: hard
@ -17937,7 +17973,7 @@ fsevents@~2.1.2:
progress: ^2.0.3
shelljs: ^0.8.4
shiki: ^0.2.7
typedoc-default-themes: 0.12.0
typedoc-default-themes: 0.12.1
peerDependencies:
typescript: 3.9.x || 4.0.x || 4.1.x
bin: