"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;
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 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 isDmkTransport = (transport) => {
    return ("dmk" in transport &&
        transport.dmk !== undefined &&
        "sessionId" in transport &&
        transport.sessionId !== undefined);
};
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 (!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,
                    requiredDerivation: requiresDerivation
                        ? async () => {
                            const { currencyId, ...derivationRest } = requiresDerivation;
                            const derivation = await (0, getAddress_1.default)(transport, {
                                currency: (0, currencies_1.getCryptoCurrencyById)(currencyId),
                                ...derivationRest,
                            });
                            return derivation.address;
                        }
                        : undefined,
                },
            });
            const observable = dmk.executeDeviceAction({
                sessionId,
                deviceAction,
            });
            return new connectAppEventMapper_1.ConnectAppEventMapper(dmk, sessionId, appName, observable).map();
        });
    };
}
exports.default = connectAppFactory;
//# sourceMappingURL=connectApp.js.map