@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
330 lines • 16.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.openAppFromDashboard = void 0;
exports.default = connectAppFactory;
const semver_1 = __importDefault(require("semver"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const errors_1 = require("@ledgerhq/errors");
const currencies_1 = require("../currencies");
const appSupportsQuitApp_1 = __importDefault(require("../appSupportsQuitApp"));
const deviceAccess_1 = require("./deviceAccess");
const inlineAppInstall_1 = __importDefault(require("../apps/inlineAppInstall"));
const isDashboardName_1 = require("./isDashboardName");
const getAppAndVersion_1 = __importDefault(require("./getAppAndVersion"));
const getDeviceInfo_1 = __importDefault(require("./getDeviceInfo"));
const getAddress_1 = __importDefault(require("./getAddress"));
const openApp_1 = __importDefault(require("./openApp"));
const quitApp_1 = __importDefault(require("./quitApp"));
const apps_1 = require("../apps");
const isUpdateAvailable_1 = __importDefault(require("./isUpdateAvailable"));
const getLatestFirmwareForDeviceUseCase_1 = require("../device/use-cases/getLatestFirmwareForDeviceUseCase");
const device_management_kit_1 = require("@ledgerhq/device-management-kit");
const live_dmk_shared_1 = require("@ledgerhq/live-dmk-shared");
const connectAppEventMapper_1 = require("./connectAppEventMapper");
const dmkUtils_1 = require("./dmkUtils");
const openAppFromDashboard = (transport, appName) => (0, rxjs_1.from)((0, getDeviceInfo_1.default)(transport)).pipe((0, operators_1.mergeMap)(deviceInfo => (0, rxjs_1.merge)(
// Nb Allows LLD/LLM to update lastSeenDevice, this can run in parallel
// since there are no more device exchanges.
(0, rxjs_1.from)((0, getLatestFirmwareForDeviceUseCase_1.getLatestFirmwareForDeviceUseCase)(deviceInfo)).pipe((0, operators_1.concatMap)(latestFirmware => (0, rxjs_1.of)({
type: "device-update-last-seen",
deviceInfo,
latestFirmware,
}))), (0, rxjs_1.concat)((0, rxjs_1.of)({
type: "ask-open-app",
appName,
}), (0, rxjs_1.defer)(() => (0, rxjs_1.from)((0, openApp_1.default)(transport, appName))).pipe((0, operators_1.concatMap)(() => (0, rxjs_1.of)({
type: "device-permission-granted",
})), (0, operators_1.catchError)(e => {
if (e && e instanceof errors_1.TransportStatusError) {
switch (e.statusCode) {
case 0x6984: // No StatusCodes definition
case 0x6807: // No StatusCodes definition
return (0, inlineAppInstall_1.default)({
transport,
appNames: [appName],
onSuccessObs: () => (0, rxjs_1.from)((0, exports.openAppFromDashboard)(transport, appName)),
});
case errors_1.StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
case 0x5501: // No StatusCodes definition
return (0, rxjs_1.throwError)(() => new errors_1.UserRefusedOnDevice());
}
}
else if (e instanceof errors_1.LockedDeviceError) {
// openAppFromDashboard is exported, so LockedDeviceError should be handled here too
return (0, rxjs_1.of)({
type: "lockedDevice",
});
}
return (0, rxjs_1.throwError)(() => e);
}))))));
exports.openAppFromDashboard = openAppFromDashboard;
const attemptToQuitApp = (transport, appAndVersion) => appAndVersion && (0, appSupportsQuitApp_1.default)(appAndVersion)
? (0, rxjs_1.from)((0, quitApp_1.default)(transport)).pipe((0, operators_1.concatMap)(() => (0, rxjs_1.of)({
type: "disconnected",
expected: true,
})), (0, operators_1.catchError)(e => (0, rxjs_1.throwError)(() => e)))
: (0, rxjs_1.of)({
type: "ask-quit-app",
});
const derivationLogic = (transport, { requiresDerivation: { currencyId, ...derivationRest }, appAndVersion, appName, }) => (0, rxjs_1.defer)(() => (0, rxjs_1.from)((0, getAddress_1.default)(transport, {
currency: (0, currencies_1.getCryptoCurrencyById)(currencyId),
...derivationRest,
}))).pipe((0, operators_1.map)(({ address }) => ({
type: "opened",
app: appAndVersion,
derivation: {
address,
},
})), (0, operators_1.catchError)(e => {
if (!e)
return (0, rxjs_1.throwError)(() => e);
if (e instanceof errors_1.BtcUnmatchedApp) {
return (0, rxjs_1.of)({
type: "ask-open-app",
appName,
});
}
if (e instanceof errors_1.TransportStatusError) {
const { statusCode } = e;
if (statusCode === errors_1.StatusCodes.SECURITY_STATUS_NOT_SATISFIED ||
statusCode === errors_1.StatusCodes.INCORRECT_LENGTH ||
(0x6600 <= statusCode && statusCode <= 0x67ff)) {
return (0, rxjs_1.of)({
type: "ask-open-app",
appName,
});
}
switch (statusCode) {
case 0x6f04: // FW-90. app was locked... | No StatusCodes definition
case errors_1.StatusCodes.HALTED: // FW-90. app bricked, a reboot fixes it.
case errors_1.StatusCodes.INS_NOT_SUPPORTED:
// this is likely because it's the wrong app (LNS 1.3.1)
return attemptToQuitApp(transport, appAndVersion);
}
}
else if (e instanceof errors_1.LockedDeviceError) {
// derivationLogic is also called inside the catchError of cmd below
// so it needs to handle LockedDeviceError too
return (0, rxjs_1.of)({
type: "lockedDevice",
});
}
return (0, rxjs_1.throwError)(() => e);
}));
/**
* @param allowPartialDependencies If some dependencies need to be installed, and if set to true,
* skip any app install if the app is not found from the provider.
*/
const cmd = (transport, { request }) => {
const { appName, requiresDerivation, dependencies, requireLatestFirmware, outdatedApp, allowPartialDependencies = false, } = request;
return new rxjs_1.Observable(o => {
const timeoutSub = (0, rxjs_1.of)({
type: "unresponsiveDevice",
})
.pipe((0, operators_1.delay)(1000))
.subscribe(e => o.next(e));
const innerSub = ({ appName, dependencies, requireLatestFirmware, }) => (0, rxjs_1.defer)(() => (0, rxjs_1.from)((0, getAppAndVersion_1.default)(transport))).pipe((0, operators_1.concatMap)((appAndVersion) => {
timeoutSub.unsubscribe();
if ((0, isDashboardName_1.isDashboardName)(appAndVersion.name)) {
// check if we meet minimum fw
if (requireLatestFirmware || outdatedApp) {
return (0, rxjs_1.from)((0, getDeviceInfo_1.default)(transport)).pipe((0, operators_1.mergeMap)((deviceInfo) => (0, rxjs_1.from)((0, getLatestFirmwareForDeviceUseCase_1.getLatestFirmwareForDeviceUseCase)(deviceInfo)).pipe((0, operators_1.mergeMap)((latest) => {
const isLatest = !latest || semver_1.default.eq(deviceInfo.version, latest.final.version);
if ((!requireLatestFirmware || (requireLatestFirmware && isLatest)) &&
outdatedApp) {
return (0, rxjs_1.from)((0, isUpdateAvailable_1.default)(deviceInfo, outdatedApp)).pipe((0, operators_1.mergeMap)(isAvailable => isAvailable
? (0, rxjs_1.throwError)(() => new errors_1.UpdateYourApp(undefined, {
managerAppName: outdatedApp.name,
}))
: (0, rxjs_1.throwError)(() => new errors_1.LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
latest: latest?.final.version,
current: deviceInfo.version,
}))));
}
if (isLatest) {
o.next({ type: "latest-firmware-resolved" });
return innerSub({
appName,
dependencies,
allowPartialDependencies,
// requireLatestFirmware // Resolved!.
});
}
else {
return (0, rxjs_1.throwError)(() => new errors_1.LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
latest: latest.final.version,
current: deviceInfo.version,
}));
}
}))));
}
// check if we meet dependencies
if (dependencies?.length) {
const completesInDashboard = (0, isDashboardName_1.isDashboardName)(appName);
return (0, inlineAppInstall_1.default)({
transport,
appNames: [...(completesInDashboard ? [] : [appName]), ...dependencies],
onSuccessObs: () => {
o.next({
type: "dependencies-resolved",
});
return innerSub({
appName,
allowPartialDependencies,
// dependencies // Resolved!
});
},
allowPartialDependencies,
});
}
// maybe we want to be in the dashboard
if (appName === appAndVersion.name) {
const e = {
type: "opened",
app: appAndVersion,
};
return (0, rxjs_1.of)(e);
}
// we're in dashboard
return (0, exports.openAppFromDashboard)(transport, appName);
}
const appNeedsUpgrade = (0, apps_1.mustUpgrade)(appAndVersion.name, appAndVersion.version);
if (appNeedsUpgrade) {
// quit app, check provider's app update for device's minimum requirements.
o.next({
type: "has-outdated-app",
outdatedApp: appAndVersion,
});
}
// need dashboard to check firmware, install dependencies, or verify app update
if (dependencies?.length ||
requireLatestFirmware ||
appAndVersion.name !== appName ||
appNeedsUpgrade) {
return attemptToQuitApp(transport, appAndVersion);
}
if (requiresDerivation) {
return derivationLogic(transport, {
requiresDerivation,
appAndVersion: appAndVersion,
appName,
});
}
else {
const e = {
type: "opened",
app: appAndVersion,
};
return (0, rxjs_1.of)(e);
}
}), (0, operators_1.catchError)((e) => {
if ((typeof e === "object" &&
e !== null &&
"_tag" in e &&
e._tag === "DeviceDisconnectedWhileSendingError") ||
e instanceof errors_1.DisconnectedDeviceDuringOperation ||
e instanceof errors_1.DisconnectedDevice) {
return (0, rxjs_1.of)({
type: "disconnected",
});
}
if (e && e instanceof errors_1.TransportStatusError) {
switch (e.statusCode) {
case errors_1.StatusCodes.CLA_NOT_SUPPORTED: // in 1.3.1 dashboard
case errors_1.StatusCodes.INS_NOT_SUPPORTED: // in 1.3.1 and bitcoin app
// fallback on "old way" because device does not support getAppAndVersion
if (!requiresDerivation) {
// if there is no derivation, there is nothing we can do to check an app (e.g. requiring non coin app)
return (0, rxjs_1.throwError)(() => new errors_1.FirmwareOrAppUpdateRequired());
}
return derivationLogic(transport, {
requiresDerivation,
appName,
});
}
}
else if (e instanceof errors_1.LockedDeviceError) {
return (0, rxjs_1.of)({
type: "lockedDevice",
});
}
return (0, rxjs_1.throwError)(() => e);
}));
const sub = innerSub({
appName,
dependencies,
requireLatestFirmware,
allowPartialDependencies,
}).subscribe(o);
return () => {
timeoutSub.unsubscribe();
sub.unsubscribe();
};
});
};
const appNameToDependency = (appName) => {
const constraints = Object.values(device_management_kit_1.DeviceModelId).reduce((result, model) => {
const minVersion = (0, apps_1.getMinVersion)(appName, model);
if (minVersion) {
result.push({
minVersion: minVersion,
applicableModels: [model],
});
}
return result;
}, []);
return {
name: appName,
constraints,
};
};
function connectAppFactory({ isLdmkConnectAppEnabled, } = { isLdmkConnectAppEnabled: false }) {
if (!isLdmkConnectAppEnabled) {
return ({ deviceId, deviceName, request }) => (0, deviceAccess_1.withDevice)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => cmd(transport, { deviceId, deviceName, request }));
}
return ({ deviceId, deviceName, request }) => {
const { appName, requiresDerivation, dependencies, requireLatestFirmware, allowPartialDependencies = false, } = request;
return (0, deviceAccess_1.withDevice)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => {
if (!(0, dmkUtils_1.isDmkTransport)(transport)) {
return cmd(transport, { deviceId, deviceName, request });
}
const { dmk, sessionId } = transport;
const deviceAction = new live_dmk_shared_1.ConnectAppDeviceAction({
input: {
application: appNameToDependency(appName),
dependencies: dependencies ? dependencies.map(name => appNameToDependency(name)) : [],
requireLatestFirmware,
allowMissingApplication: allowPartialDependencies,
unlockTimeout: 0, // Expect to fail immediately when device is locked
requiredDerivation: requiresDerivation
? async () => {
try {
dmk._unsafeBypassIntentQueue({ bypass: true, sessionId });
const { currencyId, ...derivationRest } = requiresDerivation;
const derivation = await (0, getAddress_1.default)(transport, {
currency: (0, currencies_1.getCryptoCurrencyById)(currencyId),
...derivationRest,
});
return derivation.address;
}
finally {
dmk._unsafeBypassIntentQueue({ bypass: false, sessionId });
}
}
: undefined,
deprecationConfig: (0, apps_1.getDeprecationConfig)(appName, dependencies),
},
});
const observable = dmk.executeDeviceAction({
sessionId,
deviceAction,
});
return new connectAppEventMapper_1.ConnectAppEventMapper(dmk, sessionId, appName, observable).map();
});
};
}
//# sourceMappingURL=connectApp.js.map