import network from "@ledgerhq/live-network/network";
import { log } from "@ledgerhq/logs";
import { from, interval } from "rxjs";
import { filter, share, switchMap } from "rxjs/operators";
import semver from "semver";
import { getCryptoCurrencyById } from "../../currencies";
import { getEnv } from "@ledgerhq/live-env";
import { RPCHostInvalid, RPCHostRequired, RPCPassRequired, RPCUserRequired, SatStackAccessDown, SatStackStillSyncing, SatStackVersionTooOld, } from "../../errors";
import { getCurrencyExplorer } from "@ledgerhq/coin-bitcoin/explorer";
const minVersionMatch = ">=0.11.1";
function isAcceptedVersion(version) {
    return !!version && semver.satisfies(semver.coerce(version) || "", minVersionMatch);
}
let mockStatus = {
    type: "ready",
};
export function setMockStatus(s) {
    mockStatus = s;
}
export function isValidHost(host) {
    const pattern = new RegExp("^" + // beginning of url
        "(((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,})|^[a-z\\d-]{2,}|" + // domain name
        "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
        "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*$"); // port and path
    return !!pattern.test(host);
}
// we would call this only during the user validation of the rpc node configuration
export function validateRPCNodeConfig(config) {
    const errors = [];
    if (!config.host) {
        errors.push({
            field: "host",
            error: new RPCHostRequired(),
        });
    }
    else if (!isValidHost(config.host)) {
        errors.push({
            field: "host",
            error: new RPCHostInvalid(),
        });
    }
    if (!config.username) {
        errors.push({
            field: "username",
            error: new RPCUserRequired(),
        });
    }
    if (!config.password) {
        errors.push({
            field: "password",
            error: new RPCPassRequired(),
        });
    }
    return errors;
}
// we would call this only during the "Testing node connection" step
// Check if the node is accessible by RPC. promise fails if not.
export async function checkRPCNodeConfig(config) {
    const errors = validateRPCNodeConfig(config);
    if (errors.length) {
        throw errors[0].error;
    }
    if (getEnv("MOCK")) {
        if (mockStatus.type === "node-disconnected") {
            throw new Error("mock disconnected");
        }
        return;
    }
    const { host, username, password, tls } = config;
    await network({
        url: `http${tls ? "s" : ""}://${host}`,
        method: "POST",
        data: {
            jsonrpc: "1.0",
            id: "ledger-live-full-node-check",
            method: "getblockchaininfo",
            params: [],
        },
        auth: {
            username,
            password,
        },
    });
}
// we would need to call this any time we would "Edit" the flow
export function parseSatStackConfig(json) {
    const obj = JSON.parse(json);
    if (obj && typeof obj === "object") {
        const { accounts, rpcurl, rpcuser, rpcpass, notls, ...extra } = obj;
        if (!rpcurl || typeof rpcurl !== "string")
            return;
        if (!rpcuser || typeof rpcuser !== "string")
            return;
        if (!rpcpass || typeof rpcpass !== "string")
            return;
        const result = {
            node: {
                host: rpcurl,
                username: rpcuser,
                password: rpcpass,
                tls: !notls,
            },
            accounts: [],
            extra,
        };
        if (accounts && typeof accounts === "object" && Array.isArray(accounts)) {
            result.accounts = accounts
                .map(a => {
                const { external, internal, ...extra } = a;
                if (!external || typeof external !== "string")
                    return;
                if (!internal || typeof internal !== "string")
                    return;
                return {
                    descriptor: {
                        external,
                        internal,
                    },
                    extra,
                };
            })
                .filter(Boolean);
        }
        return result;
    }
}
// we would need this at the end of the setup flow, when we save to a jss.json configuration file
export function stringifySatStackConfig(config) {
    return JSON.stringify({
        accounts: config.accounts.map(a => ({
            external: a.descriptor.external,
            internal: a.descriptor.internal,
            ...a.extra,
        })),
        rpcurl: config.node.host,
        rpcuser: config.node.username,
        rpcpass: config.node.password,
        notls: !config.node.tls,
        ...config.extra,
    }, null, 2);
}
// We would need it to apply an edition over an existing sats stack configuration (before saving it over)
export function editSatStackConfig(existing, edit) {
    const accounts = existing.accounts.concat(
    // append accounts that would not already exist
    (edit.accounts || []).filter(a => !existing.accounts.some(existing => a.descriptor.internal === existing.descriptor.internal)));
    return {
        ...existing,
        ...edit,
        // edit is patching existing fields
        accounts,
    };
}
export function isSatStackEnabled() {
    return Boolean(getEnv("SATSTACK"));
}
// We would need it any time we want to check if the Sats Stack is up and what status is it at currently
// - during the configuration flow (on last step) (actively polling)
// - on the settings screen (actively polling)
// - before doing a sync
// - before doing an add account
// NB the promise is never rejected
export async function fetchSatStackStatus() {
    if (!isSatStackEnabled()) {
        return {
            type: "satstack-disconnected",
        };
    }
    if (getEnv("MOCK")) {
        return mockStatus;
    }
    const ce = getCurrencyExplorer(getCryptoCurrencyById("bitcoin"));
    const r = await network({
        method: "GET",
        url: `${ce.endpoint}/blockchain/${ce.version}/explorer/status`,
    }).catch(() => null);
    if (!r || !r.data) {
        return {
            type: "satstack-disconnected",
        };
    }
    const { chain, status, sync_progress, scan_progress, version } = r.data;
    if (!isAcceptedVersion(version)) {
        return {
            type: "satstack-outdated",
        };
    }
    if (chain !== "main") {
        return {
            type: "invalid-chain",
            found: chain,
        };
    }
    if (status === "initializing" || status === "pending-scan") {
        return {
            type: "initializing",
        };
    }
    if (status === "node-disconnected") {
        return {
            type: "node-disconnected",
        };
    }
    if (status === "syncing") {
        return {
            type: "syncing",
            progress: (sync_progress || 0) / 100,
        };
    }
    if (status === "scanning") {
        return {
            type: "scanning",
            progress: (scan_progress || 0) / 100,
        };
    }
    return {
        type: "ready",
    };
}
export async function checkDescriptorExists(descriptor) {
    if (getEnv("MOCK")) {
        return true;
    }
    const r = await network({
        method: "POST",
        url: `${getEnv("EXPLORER_SATSTACK")}/control/descriptors/has`,
        data: {
            descriptor,
        },
    });
    log("satstack", "checkDescriptorExists " + descriptor + " is " + r.data.exists);
    return Boolean(r.data.exists);
}
export async function requiresSatStackReady() {
    if (isSatStackEnabled()) {
        const status = await fetchSatStackStatus();
        switch (status.type) {
            case "ready":
                return;
            case "syncing":
            case "scanning":
                throw new SatStackStillSyncing();
            case "satstack-outdated":
                throw new SatStackVersionTooOld();
            default:
                throw new SatStackAccessDown();
        }
    }
}
export const statusObservable = interval(1000).pipe(switchMap(() => from(fetchSatStackStatus())), filter((e, i) => i > 4 || e.type !== "satstack-disconnected"), share());
//# sourceMappingURL=satstack.js.map