UNPKG

@ledgerhq/live-common

Version:
330 lines • 16.4 kB
"use strict"; 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