UNPKG

@ledgerhq/live-common

Version:
457 lines • 19.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.predictOptimisticState = exports.getNextAppOp = exports.getActionPlan = exports.updateAllProgress = exports.isLiveSupportedApp = exports.isOutOfMemoryState = exports.isIncompleteState = exports.distribute = exports.reducer = exports.initState = void 0; exports.getBlockSize = getBlockSize; const rxjs_1 = require("rxjs"); const flatMap_1 = __importDefault(require("lodash/flatMap")); const semver_1 = __importDefault(require("semver")); const devices_1 = require("@ledgerhq/devices"); const types_1 = require("./types"); const currencies_1 = require("../currencies"); const errors_1 = require("../errors"); const live_env_1 = require("@ledgerhq/live-env"); const errors_2 = require("@ledgerhq/errors"); const initState = ({ deviceModelId, appsListNames, installed, appByName, ...listAppsResult }, appsToRestore) => { let state = { ...listAppsResult, installed, appByName, apps: appsListNames.map(name => appByName[name]).filter(Boolean), deviceModel: (0, devices_1.getDeviceModel)(deviceModelId), recentlyInstalledApps: [], installQueue: [], uninstallQueue: [], updateAllQueue: [], currentProgressSubject: new rxjs_1.Subject(), currentError: null, currentAppOp: null, skippedAppOps: [], skipAppDataBackup: false, }; if (appsToRestore) { state = appsToRestore .filter(name => appByName[name] && !installed.some(a => a.name === name)) .map(name => ({ type: "install", name, })) .reduce(exports.reducer, state); } return state; }; exports.initState = initState; // ^TODO move this to legacyDependencies.js // we should have dependency as part of the data! const reorderInstallQueue = (appByName, apps) => { const list = []; apps.forEach(app => { if (list.includes(app)) return; if (app in appByName) { const deps = appByName[app].dependencies; deps.forEach(dep => { if (apps.includes(dep) && !list.includes(dep)) { list.push(dep); } }); } list.push(app); }); return list; }; const reorderUninstallQueue = (appByName, apps) => reorderInstallQueue(appByName, apps.slice(0).reverse()).reverse(); const findDependents = (appByName, name) => { const all = []; for (const k in appByName) { const app = appByName[k]; if (app.dependencies.includes(name)) { all.push(app.name); } } return all; }; const reducer = (state, action) => { switch (action.type) { case "reset": return action.initialState; case "onRunnerEvent": { // an app operation was correctly prefered. update state accordingly const { event } = action; const { appOp } = event; if (event.type === "runStart") { return { ...state, currentAppOp: appOp, currentProgressSubject: new rxjs_1.Subject(), }; } else if (event.type === "runSuccess") { if (state.currentProgressSubject) { state.currentProgressSubject.complete(); } let nextState; if (appOp.type === "install") { const app = state.apps.find(a => a.name === appOp.name); nextState = { ...state, currentAppOp: null, currentProgressSubject: null, currentError: null, recentlyInstalledApps: state.recentlyInstalledApps.concat(appOp.name), // append the app to known installed apps installed: state.installed .filter(o => o.name !== appOp.name) .concat({ name: appOp.name, updated: true, hash: app ? app.hash : "", blocks: app && app.bytes ? Math.ceil(app.bytes / getBlockSize(state)) : 0, version: app ? app.version : "", availableVersion: app ? app.version : "", }), // remove the install action installQueue: state.installQueue.filter(name => appOp.name !== name), }; } else { nextState = { ...state, currentAppOp: null, currentProgressSubject: null, currentError: null, // remove apps to known installed apps installed: state.installed.filter(i => appOp.name !== i.name), // remove the uninstall action uninstallQueue: state.uninstallQueue.filter(name => appOp.name !== name), }; } if (nextState.installQueue.length + nextState.uninstallQueue.length === 0) { nextState.updateAllQueue = []; } return nextState; } else if (event.type === "runError") { // TO BE CONTINUED LL-2138 // to handle recovering from error. however we are not correctly using it at the moment. /* const error = event.error; if (error instanceof ManagerDeviceLockedError) { return { ...state, currentError: { appOp: appOp, error: event.error } }; } */ if (state.currentProgressSubject) { state.currentProgressSubject.complete(); } // any other error stops everything return { ...state, uninstallQueue: [], installQueue: [], updateAllQueue: [], currentAppOp: null, currentProgressSubject: null, currentError: { appOp: appOp, error: event.error, }, }; } else if (event.type === "runProgress") { // we just emit on the subject if (state.currentProgressSubject) { state.currentProgressSubject.next(event.progress); } return state; // identity state will not re-render the UI } return state; } case "recover": return { ...state, currentError: null }; case "wiped": // Reconciliate an already achieved wipe via single uninstall apdu return { ...state, installed: [], skipAppDataBackup: false, }; case "wipe": return { ...state, currentError: null, installQueue: [], uninstallQueue: reorderUninstallQueue(state.appByName, state.installed.map(({ name }) => name)), skipAppDataBackup: true, }; case "updateAll": { let installList = state.installQueue.slice(0); let uninstallList = state.uninstallQueue.slice(0); state.installed .filter(({ updated, name }) => ((0, live_env_1.getEnv)("MOCK_APP_UPDATE") || !updated) && state.appByName[name]) .forEach(app => { const dependents = state.installed .filter(a => { const depApp = state.appByName[a.name]; return depApp && depApp.dependencies.includes(app.name); }) .map(a => a.name); uninstallList = uninstallList.concat([app.name, ...dependents]); installList = installList.concat([app.name, ...dependents]); }); const installQueue = reorderInstallQueue(state.appByName, installList); const uninstallQueue = reorderUninstallQueue(state.appByName, uninstallList); /** since install queue === uninstall queue in this case we can map the update queue to either one */ const updateAllQueue = installQueue; return { ...state, currentError: null, installQueue, uninstallQueue, updateAllQueue, }; } case "install": { const { name, allowPartialDependencies } = action; if (state.installQueue.includes(name)) { // already queued for install return state; } const existing = state.installed.find(app => app.name === name); const skippedAppOps = [...state.skippedAppOps]; if (existing && existing.updated && state.installedAvailable) { // already installed and up to date skippedAppOps.push({ reason: types_1.SkipReason.AppAlreadyInstalled, appOp: action, }); return { ...state, skippedAppOps }; } const appToInstall = state.appByName[name]; // The target application was not found in the specified provider BUT // we can update the firmware instead, and perhaps in the new FW it is available. if (!appToInstall && state.firmware?.updateAvailable?.final?.version && semver_1.default.lt(state.deviceInfo.version, state.firmware?.updateAvailable?.final?.version || "")) { throw new errors_2.LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", { current: state.deviceInfo.version, latest: state.firmware?.updateAvailable?.final?.version, }); } // The target application was not found in the specified provider. if (!appToInstall) { if (allowPartialDependencies) { // Some flows are resillient to missing operations. skippedAppOps.push({ reason: types_1.SkipReason.NoSuchAppOnProvider, appOp: action, }); return { ...state, skippedAppOps }; } else { // Some flows are not, and we want to stop with an error. throw new errors_1.NoSuchAppOnProvider("", { appName: name }); } } const dependencies = appToInstall.dependencies; const dependentsOfDep = (0, flatMap_1.default)(dependencies, dep => findDependents(state.appByName, dep)); const depsInstalledOutdated = state.installed.filter(a => dependencies.includes(a.name) && !a.updated); let installList = state.installQueue; // installing an app will remove if planned for uninstalling let uninstallList = state.uninstallQueue.filter(u => name !== u && !dependencies.includes(u)); if (state.uninstallQueue.length !== uninstallList.length) { // app was asked for uninstall so it means we need to just cancel. // TODO cover this in tests... } else { // if app is already installed but outdated, we'll need to update related deps if ((existing && !existing.updated) || depsInstalledOutdated.length) { // if app has installed direct dependent apps, we'll need to update them too const directDependents = findDependents(state.appByName, name).filter(d => state.installed.some(a => a.name === d)); const outdated = state.installed .filter(a => !a.updated && [name, ...dependencies, ...directDependents, ...dependentsOfDep].includes(a.name)) .map(a => a.name); uninstallList = uninstallList.concat(outdated); installList = installList.concat(outdated); } installList = installList.concat([ ...dependencies.filter(d => !state.installed.some(a => a.name === d)), name, ]); } const installQueue = reorderInstallQueue(state.appByName, installList); const uninstallQueue = reorderUninstallQueue(state.appByName, uninstallList); return { ...state, currentError: null, installQueue, uninstallQueue }; } case "setCustomImage": { const { lastSeenCustomImage } = action; const { size } = lastSeenCustomImage; return { ...state, customImageBlocks: Math.ceil(size / getBlockSize(state)), }; } case "uninstall": { const { name } = action; if (state.uninstallQueue.includes(name)) { // already queued return state; } // uninstalling an app will remove from installQueue as well as direct deps const installQueue = state.installQueue.filter(u => name !== u); let uninstallQueue = state.uninstallQueue; if (state.installed.some(a => a.name === name || a.name === "") || action.force || // if installed unavailable and it was not a cancellation // TODO cover this in tests... (!state.installedAvailable && !state.installQueue.includes(name))) { uninstallQueue = reorderUninstallQueue(state.appByName, uninstallQueue.concat([ ...findDependents(state.appByName, name).filter(d => state.installed.some(a => a.name === d)), name, ])); } return { ...state, currentError: null, installQueue, uninstallQueue, skipAppDataBackup: true, }; } } }; exports.reducer = reducer; const defaultConfig = { warnMemoryRatio: 0.1, sortApps: false, }; // calculate all size information useful for display const distribute = (state, config) => { const { customImageBlocks } = state; const { warnMemoryRatio, sortApps } = { ...defaultConfig, ...config }; const blockSize = getBlockSize(state); const totalBytes = state.deviceModel.memorySize; const totalBlocks = Math.floor(totalBytes / blockSize); const osBytes = (state.firmware && state.firmware.bytes) || 0; const osBlocks = Math.ceil(osBytes / blockSize); const languagePackBytes = state.installedLanguagePack?.bytes || 0; const languagePackBlocks = Math.ceil(languagePackBytes / blockSize); const appsSpaceBlocks = totalBlocks - osBlocks - customImageBlocks - languagePackBlocks; const appsSpaceBytes = appsSpaceBlocks * blockSize; let totalAppsBlocks = 0; const apps = state.installed.map(app => { const { name, blocks } = app; totalAppsBlocks += blocks; const currency = // try to find the "official" currency when possible (2 currencies can have the same manager app and ticker) (0, currencies_1.findCryptoCurrency)(c => c.name === name) || // Else take the first one with that manager app (0, currencies_1.findCryptoCurrency)(c => c.managerAppName === name); return { currency, name, blocks, bytes: blocks * blockSize, }; }); if (sortApps) { apps.sort((a, b) => b.blocks - a.blocks); } const totalAppsBytes = totalAppsBlocks * blockSize; const freeSpaceBlocks = appsSpaceBlocks - totalAppsBlocks; const freeSpaceBytes = freeSpaceBlocks * blockSize; const shouldWarnMemory = freeSpaceBlocks / appsSpaceBlocks < warnMemoryRatio; return { totalBlocks, totalBytes, osBlocks, osBytes, apps, appsSpaceBlocks, appsSpaceBytes, totalAppsBlocks, customImageBlocks, languagePackBlocks, totalAppsBytes, freeSpaceBlocks, freeSpaceBytes, shouldWarnMemory, }; }; exports.distribute = distribute; function getBlockSize(state) { return state.deviceModel.getBlockSize(state.deviceInfo.version); } // tells if the state is "incomplete" to implement the Manager v2 feature // this happens when some apps are unrecognized const isIncompleteState = (state) => state.installed.some(a => !a.name); exports.isIncompleteState = isIncompleteState; // calculate if a given state (typically a predicted one) is out of memory (meaning impossible to reach with a device) const isOutOfMemoryState = (state) => { const { totalAppsBlocks, appsSpaceBlocks } = (0, exports.distribute)(state); return totalAppsBlocks > appsSpaceBlocks; }; exports.isOutOfMemoryState = isOutOfMemoryState; const isLiveSupportedApp = (app) => { const currency = app?.currencyId ? (0, currencies_1.findCryptoCurrencyById)(app.currencyId) : null; return currency ? (0, currencies_1.isCurrencySupported)(currency) : false; }; exports.isLiveSupportedApp = isLiveSupportedApp; const updateAllProgress = (state) => { const total = state.updateAllQueue.length; /** each uninstall and install comes in a pair and have a weight of 0.5 in the progress */ const current = (state.uninstallQueue.length + state.installQueue.length) / 2; if (total === 0 || current === 0) return 1; return Math.max(0, Math.min((total - current) / total, 1)); }; exports.updateAllProgress = updateAllProgress; // a series of operation to perform on the device for current state const getActionPlan = (state) => state.uninstallQueue .map(name => ({ type: "uninstall", name, })) .concat(state.installQueue.map(name => ({ type: "install", name, }))); exports.getActionPlan = getActionPlan; // get next operation to perform const getNextAppOp = (state) => { if (state.uninstallQueue.length) { return { type: "uninstall", name: state.uninstallQueue[0], }; } else if (state.installQueue.length) { return { type: "install", name: state.installQueue[0], }; } }; exports.getNextAppOp = getNextAppOp; // resolve the State to predict when all queued ops are done const predictOptimisticState = (state) => { const s = { ...state, currentProgressSubject: null }; return (0, exports.getActionPlan)(s) .map(appOp => ({ type: "onRunnerEvent", event: { type: "runSuccess", appOp, }, })) .reduce(exports.reducer, s); }; exports.predictOptimisticState = predictOptimisticState; //# sourceMappingURL=logic.js.map