"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createImplicitSpeculos = exports.findAppCandidate = exports.findLatestAppCandidate = exports.appCandidatesMatches = exports.listAppCandidates = exports.createSpeculosDevice = exports.releaseSpeculosDevice = exports.closeAllSpeculosDevices = void 0;
// Ledger internal speculos testing framework.
// loading this file have side effects and is only for Node.
const sample_1 = __importDefault(require("lodash/sample"));
const invariant_1 = __importDefault(require("invariant"));
const path_1 = __importDefault(require("path"));
const semver_1 = __importDefault(require("semver"));
const fs_1 = require("fs");
const logs_1 = require("@ledgerhq/logs");
const hw_1 = require("../hw");
const live_env_1 = require("@ledgerhq/live-env");
const polyfill_1 = require("../apps/polyfill");
const currencies_1 = require("../currencies");
const formatters_1 = require("../bot/formatters");
const apps_1 = require("../apps");
const speculos_transport_1 = require("@ledgerhq/speculos-transport");
Object.defineProperty(exports, "closeAllSpeculosDevices", { enumerable: true, get: function () { return speculos_transport_1.closeAllSpeculosDevices; } });
Object.defineProperty(exports, "releaseSpeculosDevice", { enumerable: true, get: function () { return speculos_transport_1.releaseSpeculosDevice; } });
Object.defineProperty(exports, "createSpeculosDevice", { enumerable: true, get: function () { return speculos_transport_1.createSpeculosDevice; } });
const modelMapPriority = {
    apex_p: 7,
    flex: 6,
    stax: 5,
    nanos: 4,
    "nanos+": 3,
    nanox: 2,
    blue: 1,
};
const defaultFirmware = {};
function hackBadSemver(str) {
    const split = str.split(".");
    const [x, y, , ...rest] = split;
    let [, , z] = split;
    if (rest.length) {
        z += "-" + rest.join("-");
    }
    return [x, y, z].filter(Boolean).join(".");
}
// list all possible apps. sorted by latest first
async function listAppCandidates(cwd) {
    let candidates = [];
    const models = (await fs_1.promises.readdir(cwd))
        .map(modelName => [modelName, modelMapPriority[modelName.toLowerCase()]])
        .filter(([, priority]) => priority)
        .sort((a, b) => b[1] - a[1])
        .map(a => a[0]);
    for (const modelName of models) {
        const model = speculos_transport_1.modelMap[modelName.toLowerCase()];
        const p1 = path_1.default.join(cwd, modelName);
        const firmwares = await fs_1.promises.readdir(p1);
        firmwares.sort((a, b) => semver_1.default.compare(hackBadSemver(a), hackBadSemver(b)));
        firmwares.reverse();
        for (const firmware of firmwares) {
            const p2 = path_1.default.join(p1, firmware);
            const appNames = await fs_1.promises.readdir(p2);
            for (const appName of appNames) {
                const p3 = path_1.default.join(p2, appName);
                const elfs = await fs_1.promises.readdir(p3);
                const c = [];
                for (const elf of elfs) {
                    if (elf.startsWith("app_") && elf.endsWith(".elf")) {
                        const p4 = path_1.default.join(p3, elf);
                        const appVersion = elf.slice(4, elf.length - 4);
                        if (semver_1.default.valid(appVersion) &&
                            !(0, apps_1.shouldUpgrade)(appName, appVersion) &&
                            !(0, apps_1.mustUpgrade)(appName, appVersion)) {
                            c.push({
                                path: p4,
                                model,
                                firmware,
                                appName,
                                appVersion,
                            });
                        }
                    }
                }
                c.sort((a, b) => semver_1.default.compare(a.appVersion, b.appVersion));
                c.reverse();
                candidates = candidates.concat(c);
            }
        }
    }
    return candidates;
}
exports.listAppCandidates = listAppCandidates;
function appCandidatesMatches(appCandidate, search) {
    const searchFirmware = search.firmware || defaultFirmware[appCandidate.model];
    return !!((!search.model || search.model === appCandidate.model) &&
        (!search.appName ||
            search.appName.replace(/ /g, "").toLowerCase() ===
                appCandidate.appName.replace(/ /g, "").toLowerCase()) &&
        ((!searchFirmware && !appCandidate.firmware.includes("rc")) ||
            appCandidate.firmware === searchFirmware ||
            (searchFirmware && semver_1.default.satisfies(hackBadSemver(appCandidate.firmware), searchFirmware))) &&
        (appCandidate.appVersion === search.appVersion ||
            (!search.appVersion && !appCandidate.appVersion.includes("-")) ||
            (search.appVersion && semver_1.default.satisfies(appCandidate.appVersion, search.appVersion))));
}
exports.appCandidatesMatches = appCandidatesMatches;
const findLatestAppCandidate = (appCandidates, search) => {
    search.firmware = process.env.SPECULOS_FIRMWARE_VERSION;
    let apps = appCandidates.filter(c => appCandidatesMatches(c, search));
    if (apps.length === 0) {
        return null;
    }
    apps = apps.sort((a, b) => semver_1.default.compare(b.appVersion, a.appVersion));
    return apps[0];
};
exports.findLatestAppCandidate = findLatestAppCandidate;
const findAppCandidate = (appCandidates, search, picker = sample_1.default) => {
    let apps = appCandidates.filter(c => appCandidatesMatches(c, search));
    if (!search.appVersion && apps.length > 0) {
        const appVersion = apps[0].appVersion;
        apps = apps.filter(a => a.appVersion === appVersion);
    }
    const app = picker(apps);
    if (apps.length > 1) {
        (0, logs_1.log)("speculos", apps.length +
            " app candidates (out of " +
            appCandidates.length +
            "):\n" +
            apps.map((a, i) => " [" + i + "] " + (0, formatters_1.formatAppCandidate)(a)).join("\n"));
    }
    return app;
};
exports.findAppCandidate = findAppCandidate;
function eatDevice(parts) {
    if (parts.length > 0) {
        const [modelQ, firmware] = parts[0].split("@");
        const model = speculos_transport_1.modelMap[(modelQ || "").toLowerCase()];
        if (model) {
            parts.shift();
            if (firmware) {
                return {
                    model,
                    firmware,
                };
            }
            return {
                model,
            };
        }
    }
    return {};
}
function parseAppSearch(query) {
    const parts = query.slice(9).split(":");
    const { model, firmware } = eatDevice(parts);
    if (parts.length === 0)
        return;
    const [nameQ, versionQ] = parts[0].split("@");
    const currency = (0, currencies_1.findCryptoCurrencyByKeyword)(nameQ);
    const appName = currency ? currency.managerAppName : nameQ;
    const appVersion = versionQ || undefined;
    let dependency;
    if (currency) {
        dependency = (0, polyfill_1.getDependencies)(currency.managerAppName)[0]?.replace(/ /g, "");
    }
    return {
        search: {
            model,
            firmware,
            appName,
            appVersion,
        },
        appName,
        dependency,
    };
}
async function createImplicitSpeculos(query) {
    const coinapps = (0, live_env_1.getEnv)("COINAPPS");
    (0, invariant_1.default)(coinapps, "COINAPPS folder is missing!");
    const seed = (0, live_env_1.getEnv)("SEED");
    (0, invariant_1.default)(seed, "SEED is missing!");
    const apps = await listAppCandidates(coinapps);
    const match = parseAppSearch(query);
    (0, invariant_1.default)(match, "speculos: invalid format of '%s'. Usage example: speculos:nanoS:bitcoin@1.3.x", query);
    const { search, dependency, appName } = match;
    const appCandidate = (0, exports.findAppCandidate)(apps, search);
    (0, invariant_1.default)(appCandidate, "could not find an app that matches '%s'", query);
    (0, logs_1.log)("speculos", "using app " + (0, formatters_1.formatAppCandidate)(appCandidate));
    return appCandidate
        ? {
            device: await (0, speculos_transport_1.createSpeculosDevice)({
                ...appCandidate,
                coinapps,
                appName,
                dependency,
                seed,
            }),
            appCandidate,
        }
        : null;
}
exports.createImplicitSpeculos = createImplicitSpeculos;
async function openImplicitSpeculos(query) {
    const r = await createImplicitSpeculos(query);
    return r?.device.transport;
}
(0, hw_1.registerTransportModule)({
    id: "speculos",
    open: (id) => {
        if (!id)
            return;
        if (id.startsWith("speculosID")) {
            const obj = (0, speculos_transport_1.getMemorySpeculosDeviceInternal)(id);
            if (!obj) {
                throw new Error("speculos transport was destroyed");
            }
            return Promise.resolve(obj.transport);
        }
        if (id.startsWith("speculos:")) {
            return openImplicitSpeculos(id);
        }
    },
    close: (transport, id) => {
        if (id.startsWith("speculos")) {
            return Promise.resolve();
        } // todo close the speculos: case
    },
    disconnect: speculos_transport_1.releaseSpeculosDevice,
});
//# sourceMappingURL=speculos.js.map