import { log } from "@ledgerhq/logs"; import { MCUNotGenuineToDashboard } from "@ledgerhq/errors"; import { Observable, from, of, EMPTY, concat, throwError } from "rxjs"; import type { DeviceVersion, FinalFirmware, McuVersion } from "@ledgerhq/types-live"; import { concatMap, delay, filter, map, mergeMap, throttleTime } from "rxjs/operators"; import semver from "semver"; import ManagerAPI from "../manager/api"; import { withDevicePolling, withDevice } from "./deviceAccess"; import { getProviderId } from "../manager/provider"; import getDeviceInfo from "./getDeviceInfo"; import { mcuOutdated, mcuNotGenuine, followDeviceRepair, followDeviceUpdate, } from "../deviceWordings"; import { getDeviceRunningMode } from "./getDeviceRunningMode"; import { fetchMcusUseCase } from "../device/use-cases/fetchMcusUseCase"; const wait2s = of({ type: "wait", }).pipe(delay(2000)); export const repairChoices = [ { id: "mcuOutdated", label: mcuOutdated, forceMCU: "0.7", }, { id: "mcuNotGenuine", label: mcuNotGenuine, forceMCU: "0.7", }, { id: "followDeviceRepair", label: followDeviceRepair, forceMCU: "0.9", }, { id: "followDeviceUpdate", label: followDeviceUpdate, forceMCU: "0.9", }, ]; const repair = ( deviceId: string, forceMCU_?: string | null, ): Observable<{ progress: number; }> => { log("hw", "firmwareUpdate-repair"); const mcusPromise = fetchMcusUseCase(); const withDeviceInfo = withDevicePolling(deviceId)( transport => from(getDeviceInfo(transport)), () => true, // accept all errors. we're waiting forever condition that make getDeviceInfo work ); const waitForBootloader = withDeviceInfo.pipe( concatMap(deviceInfo => (deviceInfo.isBootloader ? EMPTY : concat(wait2s, waitForBootloader))), ); const loop = (forceMCU?: string | null | undefined) => concat( withDeviceInfo.pipe( concatMap(deviceInfo => { const installMcu = (version: string) => withDevice(deviceId)(transport => ManagerAPI.installMcu(transport, "mcu", { targetId: deviceInfo.targetId, version, }), ); if (!deviceInfo.isBootloader) { // finish earlier return EMPTY; } // This is a special case where user is in firmware 1.3.1 // and the device shows MCU Not Genuine. // User needs to press both keys three times to go back to dashboard // and continue the update process if ( forceMCU && forceMCU === "0.7" && (deviceInfo.majMin === "0.6" || deviceInfo.majMin === "0.7") ) { // finish earlier return throwError(() => new MCUNotGenuineToDashboard()); } if (forceMCU) { return concat(installMcu(forceMCU), wait2s, loop()); } switch (deviceInfo.majMin) { case "0.0": return concat(installMcu("0.6"), wait2s, loop()); case "0.6": return installMcu("1.5"); case "0.7": return installMcu("1.6"); case "0.9": return installMcu("1.7"); default: return from(mcusPromise).pipe( concatMap(mcus => { let next; const { seVersion, seTargetId, mcuBlVersion } = deviceInfo; // This is a special case where a user with LNX version >= 2.0.0 // comes back with a broken updated device. We need to be able // to patch MCU or Bootloader if needed if (seVersion && seTargetId) { log("hw", "firmwareUpdate-repair seVersion and seTargetId found", { seVersion, seTargetId, }); const provider = getProviderId(deviceInfo); /** * filter the MCUs that are available on the provider and * have a "from_bootloader_version" different from "none" * */ const availableMcus = mcus.filter( mcu => mcu.providers.includes(provider) && mcu.from_bootloader_version !== "none", ); log("hw", `firmwareUpdate-repair available mcus on provider ${provider}`, { availableMcus, }); return from( ManagerAPI.getDeviceVersion(seTargetId, getProviderId(deviceInfo)), ).pipe( mergeMap((deviceVersion: DeviceVersion) => from( ManagerAPI.getCurrentFirmware({ deviceId: deviceVersion.id, version: seVersion, provider: getProviderId(deviceInfo), }), ), ), mergeMap((finalFirmware: FinalFirmware) => { log("hw", "firmwareUpdate-repair got final firmware", { finalFirmware, }); const mcu = ManagerAPI.findBestMCU( availableMcus.filter(({ id }: McuVersion) => finalFirmware.mcu_versions.includes(id), ), ); log("hw", "firmwareUpdate-repair got mcu", { mcu }); if (!mcu) return EMPTY; const expectedBootloaderVersion = semver.coerce( mcu.from_bootloader_version, )?.version; const currentBootloaderVersion = semver.coerce(mcuBlVersion)?.version; log("hw", "firmwareUpdate-repair bootloader versions", { currentBootloaderVersion, expectedBootloaderVersion, }); if (expectedBootloaderVersion === currentBootloaderVersion) { next = mcu; log("hw", "firmwareUpdate-repair bootloader versions are the same", { next, }); } else { next = { name: mcu.from_bootloader_version, }; log("hw", "firmwareUpdate-repair bootloader versions are different", { next, }); } return installMcu(next.name); }), ); } else { next = ManagerAPI.findBestMCU( ManagerAPI.compatibleMCUForDeviceInfo( mcus, deviceInfo, getProviderId(deviceInfo), ), ); if (next) return installMcu(next.name); } return EMPTY; }), ); } }), ), from( getDeviceRunningMode({ deviceId, unresponsiveTimeoutMs: 4000, cantOpenDeviceRetryLimit: 2, }), ).pipe( mergeMap(result => { if (result.type === "bootloaderMode") { return loop(forceMCU); } else { return EMPTY; } }), ), ); // TODO ideally we should race waitForBootloader with an event "display-bootloader-reboot", it should be a delayed event that is not emitted if waitForBootloader is fast enough.. return concat(waitForBootloader, loop(forceMCU_)).pipe( filter((e: any) => e.type === "bulk-progress"), map(e => ({ progress: e.progress, })), throttleTime(100), ); }; export default repair;