@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
175 lines (149 loc) • 5.41 kB
text/typescript
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.
);
};