Skip to content

Commit

Permalink
feat(connect): improve error handling for missing capabilities in FW
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Nov 1, 2024
1 parent a045d03 commit de48672
Show file tree
Hide file tree
Showing 32 changed files with 75 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/connect-popup/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const handleResponseEvent = (data: MethodResponseMessage) => {
default:
fail({
type: 'error',
code,
detail: 'response-event-error',
message: ('error' in data.payload && data.payload.error) || 'Unknown error',
});
Expand Down
23 changes: 22 additions & 1 deletion packages/connect-ui/src/views/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
variables,
} from '@trezor/components';
import { isFirefox } from '@trezor/env-utils';
import { HELP_FIRMWARE_TYPE } from '@trezor/urls';
import { spacings } from '@trezor/theme';

export interface ErrorViewProps {
type: 'error';
Expand All @@ -22,6 +24,7 @@ export interface ErrorViewProps {
| 'core-missing' // core was loaded correctly but became unavailable later
| 'core-failed-to-load'; // dynamic import of core failed
// future errors when using connect-ui in different contexts
code?: string;
message?: string;
}

Expand Down Expand Up @@ -145,7 +148,25 @@ const getTroubleshootingTips = (props: ErrorViewProps) => {
icon: 'question',
title: 'Action not completed',
detail: {
steps: [<Step>{props.message}</Step>],
steps: [
props.code === 'Device_MissingCapabilityBtcOnly' ? (
<>
<Step>{props.message}</Step>
<Button
href={HELP_FIRMWARE_TYPE}
size="small"
variant="tertiary"
icon="arrowRight"
iconAlignment="right"
margin={{ top: spacings.md }}
>
Learn more
</Button>
</>
) : (
<Step>{props.message}</Step>
),
],
},
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/binance/api/binanceGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class BinanceGetAddress extends AbstractMethod<'binanceGetAddress
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Binance'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('BNB'), this.firmwareRange);

// create a bundle with only one batch if bundle doesn't exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class BinanceGetPublicKey extends AbstractMethod<

init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Binance'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('BNB'), this.firmwareRange);

// create a bundle with only one batch if bundle doesn't exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default class BinanceSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Binance'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('BNB'), this.firmwareRange);

const { payload } = this;
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/cardano/api/cardanoGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class CardanoGetAddress extends AbstractMethod<'cardanoGetAddress
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Cardano'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Cardano'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class CardanoGetNativeScriptHash extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Cardano'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Cardano'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class CardanoGetPublicKey extends AbstractMethod<'cardanoGetPubli

init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Cardano'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Cardano'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class CardanoSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Cardano'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Cardano'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/eos/api/eosGetPublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class EosGetPublicKey extends AbstractMethod<

init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_EOS'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('EOS'), this.firmwareRange);

// create a bundle with only one batch if bundle doesn't exists
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/eos/api/eosSignTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Params = {
export default class EosSignTransaction extends AbstractMethod<'eosSignTransaction', Params> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_EOS'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('EOS'), this.firmwareRange);

const { payload } = this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class EthereumGetAddress extends AbstractMethod<'ethereumGetAddre
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

// create a bundle with only one batch if bundle doesn't exists
this.hasBundle = !!this.payload.bundle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default class EthereumGetPublicKey extends AbstractMethod<'ethereumGetPub

init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

// create a bundle with only one batch if bundle doesn't exists
this.hasBundle = !!this.payload.bundle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Params = PROTO.EthereumSignMessage & {
export default class EthereumSignMessage extends AbstractMethod<'ethereumSignMessage', Params> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

const { payload } = this;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default class EthereumSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

const { payload } = this;
// validate incoming parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const Params = Type.Intersect([
export default class EthereumSignTypedData extends AbstractMethod<'ethereumSignTypedData', Params> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

const { payload } = this;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default class EthereumVerifyMessage extends AbstractMethod<
init() {
this.requiredPermissions = ['read', 'write'];
this.firmwareRange = getFirmwareRange(this.name, null, this.firmwareRange);
this.requiredDeviceCapabilities = ['Capability_Ethereum'];

const { payload } = this;

Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/nem/api/nemGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class NEMGetAddress extends AbstractMethod<'nemGetAddress', Param
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_NEM'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('NEM'), this.firmwareRange);

// create a bundle with only one batch if bundle doesn't exists
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/nem/api/nemSignTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class NEMSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_NEM'];
this.firmwareRange = getFirmwareRange(this.name, getMiscNetwork('NEM'), this.firmwareRange);

const { payload } = this;
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/ripple/api/rippleGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class RippleGetAddress extends AbstractMethod<'rippleGetAddress',
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Ripple'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Ripple'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default class RippleSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Ripple'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Ripple'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/solana/api/solanaGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class SolanaGetAddress extends AbstractMethod<'solanaGetAddress',
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Solana'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Solana'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/solana/api/solanaGetPublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class SolanaGetPublicKey extends AbstractMethod<
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Solana'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Solana'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class SolanaSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Solana'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Solana'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/stellar/api/stellarGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class StellarGetAddress extends AbstractMethod<'stellarGetAddress
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Stellar'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Stellar'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class StellarSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Stellar'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Stellar'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/tezos/api/tezosGetAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class TezosGetAddress extends AbstractMethod<'tezosGetAddress', P
init() {
this.noBackupConfirmationMode = 'always';
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Tezos'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Tezos'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/tezos/api/tezosGetPublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class TezosGetPublicKey extends AbstractMethod<

init() {
this.requiredPermissions = ['read'];
this.requiredDeviceCapabilities = ['Capability_Tezos'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Tezos'),
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/api/tezos/api/tezosSignTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class TezosSignTransaction extends AbstractMethod<
> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Tezos'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Tezos'),
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/constants/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const ERROR_CODES = {
Device_InvalidState: 'Passphrase is incorrect', // authorization error (device state comparison)
Device_CallInProgress: 'Device call in progress', // thrown when trying to make another call while current is still running
Device_MultipleNotSupported: 'Multiple devices are not supported', // thrown by methods which require single device
Device_MissingCapability: 'Device is missing capability', // thrown by methods which require specific capability
Device_MissingCapabilityBtcOnly: 'Device is missing capability (BTC only)', // thrown by methods which require specific capability when using BTC only firmware

Failure_ActionCancelled: 'Action cancelled by user',
Failure_FirmwareError: 'Firmware installation failed',
Expand Down
21 changes: 21 additions & 0 deletions packages/connect/src/core/AbstractMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { getHost } from '../utils/urlUtils';
import type { Device } from '../device/Device';
import type { FirmwareRange, DeviceState, StaticSessionId, DeviceUniquePath } from '../types';
import { Capability } from '@trezor/protobuf/src/messages';

export type Payload<M> = Extract<CallMethodPayload, { method: M }> & { override?: boolean };
export type MethodReturnType<M extends CallMethodPayload['method']> = CallMethodResponse<M>;
Expand Down Expand Up @@ -133,6 +134,8 @@ export abstract class AbstractMethod<Name extends CallMethodPayload['method'], P

requireDeviceMode: DeviceMode[];

requiredDeviceCapabilities: Capability[] = [];

network: NETWORK.NetworkType;

useCardanoDerivation: boolean;
Expand Down Expand Up @@ -333,6 +336,24 @@ export abstract class AbstractMethod<Name extends CallMethodPayload['method'], P
};
}

checkDeviceCapability() {
const deviceHasAllRequiredCapabilities = (this.requiredDeviceCapabilities || []).every(
capability => this.device.features.capabilities.includes(capability),
);
if (!deviceHasAllRequiredCapabilities) {
if (this.device.firmwareType === 'bitcoin-only') {
throw ERRORS.TypedError(
'Device_MissingCapabilityBtcOnly',
`Trezor has Bitcoin-only firmware installed, which does not support the ${this.info} method. Please install Universal firmware through Trezor Suite.`,
);
}
throw ERRORS.TypedError(
'Device_MissingCapability',
'Device does not have capability to call this method. Make sure you have the latest firmware installed.',
);
}
}

abstract run(): Promise<MethodReturnType<Name>>;

dispose() {}
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,8 @@ const inner = async (context: CoreContext, method: AbstractMethod<any>, device:
sendCoreMessage(createPopupMessage(POPUP.CANCEL_POPUP_REQUEST));
}

method.checkDeviceCapability();

// run method
try {
const response = await method.run();
Expand Down

0 comments on commit de48672

Please sign in to comment.