UNPKG

@ledgerhq/live-common

Version:
175 lines (149 loc) • 5.41 kB
import { DeviceOnDashboardExpected, LanguageNotFound, ManagerNotEnoughSpaceError, StatusCodes, TransportError, TransportStatusError, } from "@ledgerhq/errors"; import { Observable, from, of, throwError } from "rxjs"; import { catchError, concatMap, delay, mergeMap } from "rxjs/operators"; import Transport from "@ledgerhq/hw-transport"; import network from "@ledgerhq/live-network/network"; import { Language, LanguagePackage } from "@ledgerhq/types-live"; import { LanguageInstallRefusedOnDevice } from "../errors"; import ManagerAPI from "../manager/api"; import attemptToQuitApp, { AttemptToQuitAppEvent } from "./attemptToQuitApp"; import { withDevice } from "./deviceAccess"; import getAppAndVersion from "./getAppAndVersion"; import getDeviceInfo from "./getDeviceInfo"; import { isDashboardName } from "./isDashboardName"; export type InstallLanguageEvent = | AttemptToQuitAppEvent | { type: "progress"; progress: number; } | { type: "devicePermissionRequested"; } | { type: "languageInstalled"; }; export type InstallLanguageRequest = { language: Language }; export type Input = { deviceId: string; deviceName: string | null; request: InstallLanguageRequest; }; export default function installLanguage({ deviceId, deviceName, request, }: Input): Observable<InstallLanguageEvent> { const { language } = request; const sub = withDevice( deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined, )( transport => new Observable(subscriber => { const timeoutSub = of<InstallLanguageEvent>({ type: "unresponsiveDevice", }) .pipe(delay(1000)) .subscribe(e => subscriber.next(e)); const sub = from(getDeviceInfo(transport)) .pipe( mergeMap(async deviceInfo => { timeoutSub.unsubscribe(); if (language === "english") { await uninstallAllLanguages(transport); subscriber.next({ type: "languageInstalled", }); subscriber.complete(); return; } const languages = await ManagerAPI.getLanguagePackagesForDevice(deviceInfo); const packs: LanguagePackage[] = languages.filter( (l: any) => l.language === language, ); if (!packs.length) return subscriber.error(new LanguageNotFound(language)); const pack = packs[0]; const { apdu_install_url } = pack; const url = apdu_install_url; const { data: rawApdus } = await network({ method: "GET", url, }); const apdus = rawApdus.split(/\r?\n/).filter(Boolean); await uninstallAllLanguages(transport); for (let i = 0; i < apdus.length; i++) { if (apdus[i].startsWith("e030")) { subscriber.next({ type: "devicePermissionRequested", }); } const response = await transport.exchange(Buffer.from(apdus[i], "hex")); const status = response.readUInt16BE(response.length - 2); const statusStr = status.toString(16); // Some error handling if (status === StatusCodes.USER_REFUSED_ON_DEVICE) { return subscriber.error(new LanguageInstallRefusedOnDevice(statusStr)); } else if (status === StatusCodes.NOT_ENOUGH_SPACE) { return subscriber.error(new ManagerNotEnoughSpaceError()); } else if (status !== StatusCodes.OK) { return subscriber.error( new TransportError("Unexpected device response", statusStr), ); } subscriber.next({ type: "progress", progress: (i + 1) / apdus.length, }); } subscriber.next({ type: "languageInstalled", }); subscriber.complete(); }), catchError((e: unknown) => { if ( e instanceof DeviceOnDashboardExpected || (e && e instanceof TransportStatusError && [0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(e.statusCode)) ) { return from(getAppAndVersion(transport)).pipe( concatMap(appAndVersion => { return !isDashboardName(appAndVersion.name) ? attemptToQuitApp(transport, appAndVersion) : of<InstallLanguageEvent>({ type: "appDetected", }); }), ); } return throwError(() => e); }), ) .subscribe(subscriber); return () => { timeoutSub.unsubscribe(); sub.unsubscribe(); }; }), ); return sub as Observable<InstallLanguageEvent>; } const uninstallAllLanguages = async (transport: Transport) => { await transport.send( 0xe0, 0x33, 0xff, 0x00, undefined, [0x9000, 0x5501], // Expected responses when uninstalling. ); };