@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
154 lines (135 loc) • 4.48 kB
text/typescript
import { DisconnectedDevice, UnresponsiveDeviceError } from "@ledgerhq/errors";
import { log } from "@ledgerhq/logs";
import type { DeviceId, DeviceInfo, FirmwareInfo } from "@ledgerhq/types-live";
import { getVersion } from "../commands/getVersion";
import isDevFirmware from "../../hw/isDevFirmware";
import { PROVIDERS } from "../../manager/provider";
import { Observable } from "rxjs";
import { map, switchMap } from "rxjs/operators";
import { SharedTaskEvent, retryOnErrorsCommandWrapper, sharedLogicTaskWrapper } from "./core";
import { quitApp } from "../commands/quitApp";
import { withTransport } from "../transports/core";
import { SendApduEmptyResponseError } from "@ledgerhq/device-management-kit";
const ManagerAllowedFlag = 0x08;
const PinValidatedFlag = 0x80;
export type GetDeviceInfoTaskArgs = { deviceId: DeviceId; deviceName: string | null };
// No taskError for getDeviceInfoTask. Kept for consistency with other tasks.
export type GetDeviceInfoTaskError = "None";
export type GetDeviceInfoTaskErrorEvent = {
type: "taskError";
error: GetDeviceInfoTaskError;
};
export type GetDeviceInfoTaskEvent =
| { type: "data"; deviceInfo: DeviceInfo }
| GetDeviceInfoTaskErrorEvent
| SharedTaskEvent;
// Exported for tests
export function internalGetDeviceInfoTask({
deviceId,
deviceName,
}: GetDeviceInfoTaskArgs): Observable<GetDeviceInfoTaskEvent> {
return new Observable(subscriber => {
return (
withTransport(
deviceId,
deviceName ? { matchDeviceByName: deviceName } : undefined,
)(({ transportRef }) =>
quitApp(transportRef.current).pipe(
switchMap(() => {
return retryOnErrorsCommandWrapper({
command: getVersion,
allowedErrors: [{ maxRetries: 3, errorClass: DisconnectedDevice }],
allowedDmkErrors: [new SendApduEmptyResponseError()],
})(transportRef, {});
}),
map(value => {
if (value.type === "unresponsive") {
return {
type: "error" as const,
error: new UnresponsiveDeviceError(),
retrying: true,
};
}
const { firmwareInfo } = value;
const deviceInfo = parseDeviceInfo(firmwareInfo);
return { type: "data" as const, deviceInfo };
}),
),
)
// Any error will be handled by the sharedLogicTaskWrapper, which will map it a relevant event
.subscribe(subscriber)
);
});
}
export const parseDeviceInfo = (firmwareInfo: FirmwareInfo): DeviceInfo => {
const {
isBootloader,
rawVersion,
targetId,
seVersion,
seTargetId,
mcuBlVersion,
mcuVersion,
mcuTargetId,
flags,
bootloaderVersion,
hardwareVersion,
languageId,
charonState,
} = firmwareInfo;
const isOSU = rawVersion.includes("-osu");
const version = rawVersion.replace("-osu", "");
const m = rawVersion.match(/([0-9]+.[0-9]+(.[0-9]+){0,1})?(-(.*))?/);
const [, majMin, , , postDash] = m || [];
const providerName = PROVIDERS[postDash] ? postDash : null;
const flag = flags.length > 0 ? flags[0] : 0;
const managerAllowed = !!(flag & ManagerAllowedFlag);
const pinValidated = !!(flag & PinValidatedFlag);
let isRecoveryMode = false;
let onboarded = true;
if (flags.length === 4) {
// Nb Since LNS+ unseeded devices are visible + extra flags
isRecoveryMode = !!(flags[0] & 0x01);
onboarded = !!(flags[0] & 0x04);
}
log(
"hw",
"deviceInfo: se@" +
version +
" mcu@" +
mcuVersion +
(isOSU ? " (osu)" : isBootloader ? " (bootloader)" : ""),
);
const hasDevFirmware = isDevFirmware(seVersion);
const deviceInfo: DeviceInfo = {
version,
mcuVersion,
seVersion,
mcuBlVersion,
majMin,
providerName: providerName || null,
targetId,
hasDevFirmware,
seTargetId,
mcuTargetId,
isOSU,
isBootloader,
isRecoveryMode,
managerAllowed,
pinValidated,
onboarded,
bootloaderVersion,
hardwareVersion,
languageId,
seFlags: flags,
charonState: charonState,
};
return deviceInfo;
};
/**
* Task to get the `DeviceInfo` of a device
*
* @param `deviceId` A device id, or an empty string if device is usb plugged
* @returns An observable that emits `GetDeviceInfoTaskEvent` events
*/
export const getDeviceInfoTask = sharedLogicTaskWrapper(internalGetDeviceInfoTask);