"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.autoSignTransaction = exports.runOnAccount = exports.runWithAppSpec = void 0;
const expect_1 = __importDefault(require("expect"));
const invariant_1 = __importDefault(require("invariant"));
const performance_now_1 = __importDefault(require("performance-now"));
const sample_1 = __importDefault(require("lodash/sample"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const logs_1 = require("@ledgerhq/logs");
const bridge_1 = require("../bridge");
const promise_1 = require("../promise");
const account_1 = require("../account");
const operation_1 = require("../operation");
const live_env_1 = require("@ledgerhq/live-env");
const promise_2 = require("../promise");
const speculos_1 = require("../load/speculos");
const formatters_1 = require("./formatters");
const cache_1 = require("../bridge/cache");
const bot_test_context_1 = require("@ledgerhq/coin-framework/bot/bot-test-context");
const accountName_1 = require("@ledgerhq/live-wallet/accountName");
let appCandidates;
const localCache = {};
const cache = (0, cache_1.makeBridgeCacheSystem)({
    saveData(c, d) {
        localCache[c.id] = d;
        return Promise.resolve();
    },
    getData(c) {
        return Promise.resolve(localCache[c.id]);
    },
});
const defaultScanAccountsRetries = 2;
const delayBetweenScanAccountRetries = 5000;
async function runWithAppSpec(spec, reportLog) {
    (0, logs_1.log)("engine", `spec ${spec.name}`);
    const seed = (0, live_env_1.getEnv)("SEED");
    (0, invariant_1.default)(seed, "SEED is not set");
    const coinapps = (0, live_env_1.getEnv)("COINAPPS");
    (0, invariant_1.default)(coinapps, "COINAPPS is not set");
    if (!appCandidates) {
        appCandidates = await (0, speculos_1.listAppCandidates)(coinapps);
    }
    const mutationReports = [];
    const { appQuery, currency, dependency, onSpeculosDeviceCreated } = spec;
    const appCandidate = (0, speculos_1.findAppCandidate)(appCandidates, appQuery);
    if (!appCandidate) {
        console.warn("no app found for " + spec.name);
        console.warn(appQuery);
        console.warn(JSON.stringify(appCandidates, undefined, 2));
    }
    (0, invariant_1.default)(appCandidate, "%s: no app found. Are you sure your COINAPPS is up to date?", spec.name, coinapps);
    (0, logs_1.log)("engine", `spec ${spec.name} will use ${(0, formatters_1.formatAppCandidate)(appCandidate)}`);
    const deviceParams = {
        ...appCandidate,
        appName: spec.currency.managerAppName,
        seed,
        dependency,
        coinapps,
        onSpeculosDeviceCreated,
    };
    let device;
    const hintWarnings = [];
    const appReport = {
        spec,
        hintWarnings,
        skipMutationsTimeoutReached: false,
    };
    // staticly check that all mutations declared a test too (if no generic spec test)
    if (!spec.test) {
        const list = spec.mutations.filter(m => !m.test);
        if (list.length > 0) {
            hintWarnings.push("mutations should define a test(): " + list.map(m => m.name).join(", "));
        }
    }
    // staticly assess if testDestination is necessary
    const mutationThatProducedDestinationsWithoutTests = [];
    const mutationWithDestinationTestsWithoutDestination = [];
    try {
        device = await (0, speculos_1.createSpeculosDevice)(deviceParams);
        appReport.appPath = device.appPath;
        const bridge = (0, bridge_1.getCurrencyBridge)(currency);
        const syncConfig = {
            paginationConfig: {},
        };
        let t = (0, performance_now_1.default)();
        const preloadedData = await cache.prepareCurrency(currency);
        const preloadDuration = (0, performance_now_1.default)() - t;
        appReport.preloadDuration = preloadDuration;
        // Scan all existing accounts
        const beforeScanTime = (0, performance_now_1.default)();
        t = (0, performance_now_1.default)();
        let accounts = await (0, rxjs_1.firstValueFrom)(bridge
            .scanAccounts({
            currency,
            deviceId: device.id,
            syncConfig,
        })
            .pipe((0, operators_1.retry)({
            count: spec.scanAccountsRetries || defaultScanAccountsRetries,
            delay: delayBetweenScanAccountRetries,
        }), (0, operators_1.filter)(e => e.type === "discovered"), (0, operators_1.map)(e => deepFreezeAccount(e.account)), (0, operators_1.reduce)((all, a) => all.concat(a), []), (0, operators_1.timeout)({
            each: (0, live_env_1.getEnv)("BOT_TIMEOUT_SCAN_ACCOUNTS"),
            with: () => (0, rxjs_1.throwError)(() => new Error("scan accounts timeout for currency " + currency.name)),
        })));
        appReport.scanDuration = (0, performance_now_1.default)() - beforeScanTime;
        // check if there are more accounts than mutation declared as a hint for the dev
        if (accounts.length <= spec.mutations.length) {
            hintWarnings.push(`There are not enough accounts (${accounts.length}) to cover all mutations (${spec.mutations.length}).\nPlease increase the account target to at least ${spec.mutations.length + 1} accounts`);
        }
        appReport.accountsBefore = accounts;
        if (!spec.allowEmptyAccounts) {
            (0, invariant_1.default)(accounts.length > 0, "unexpected empty accounts for " + currency.name);
        }
        const preloadStats = preloadDuration > 10 ? ` (preload: ${(0, formatters_1.formatTime)(preloadDuration)})` : "";
        reportLog(`Spec ${spec.name} found ${accounts.length} ${currency.name} accounts${preloadStats}. Will use ${(0, formatters_1.formatAppCandidate)(appCandidate)}\n${accounts.map(a => (0, account_1.formatAccount)(a, "head")).join("\n")}\n`);
        if (accounts.every(account_1.isAccountEmpty)) {
            reportLog(`This SEED does not have ${currency.name}. Please send funds to ${accounts
                .map(a => a.freshAddress)
                .join(" or ")}\n`);
            appReport.accountsAfter = accounts;
            return appReport;
        }
        const mutationsStartTime = (0, performance_now_1.default)();
        const skipMutationsTimeout = spec.skipMutationsTimeout || (0, live_env_1.getEnv)("BOT_SPEC_DEFAULT_TIMEOUT");
        let mutationsCount = {};
        // we sequentially iterate on the initial account set to perform mutations
        const length = accounts.length;
        const totalTries = spec.multipleRuns || 1;
        // dynamic buffer with ids of accounts that need a resync (between runs)
        let accountIdsNeedResync = [];
        for (let j = 0; j < totalTries; j++) {
            for (let i = 0; i < length; i++) {
                t = (0, performance_now_1.default)();
                if (t - mutationsStartTime > skipMutationsTimeout) {
                    appReport.skipMutationsTimeoutReached = true;
                    break;
                }
                (0, logs_1.log)("engine", `spec ${spec.name} sync all accounts (try ${j} run ${i})`);
                // resync all accounts that needs to be resynced
                const resynced = await (0, promise_1.promiseAllBatched)((0, live_env_1.getEnv)("SYNC_MAX_CONCURRENT"), accounts.filter(a => accountIdsNeedResync.includes(a.id)), syncAccount);
                accounts = accounts.map((a) => {
                    const i = accountIdsNeedResync.indexOf(a.id);
                    return i !== -1 ? resynced[i] : a;
                });
                accountIdsNeedResync = [];
                appReport.accountsAfter = accounts;
                const resyncAccountsDuration = (0, performance_now_1.default)() - t;
                const account = accounts[i];
                const report = await runOnAccount({
                    appCandidate,
                    spec,
                    device,
                    account,
                    accounts,
                    mutationsCount,
                    resyncAccountsDuration,
                    accountIdsNeedResync,
                    preloadedData,
                });
                if (report.finalAccount) {
                    // optim: no need to resync if all went well with finalAccount
                    const finalAccount = report.finalAccount;
                    accountIdsNeedResync = accountIdsNeedResync.filter(id => id !== finalAccount.id);
                    accounts = accounts.map((a) => (a.id === finalAccount.id ? finalAccount : a));
                }
                if (report.finalDestination) {
                    // optim: no need to resync if all went well with finalDestination
                    const finalDestination = report.finalDestination;
                    accountIdsNeedResync = accountIdsNeedResync.filter(id => id !== finalDestination.id);
                    accounts = accounts.map((a) => a.id === finalDestination.id ? finalDestination : a);
                }
                else if (report.mutation) {
                    const { mutation } = report;
                    if (report.destination) {
                        if (!mutation.testDestination &&
                            !mutationThatProducedDestinationsWithoutTests.includes(mutation)) {
                            mutationThatProducedDestinationsWithoutTests.push(mutation);
                        }
                    }
                    else {
                        if (mutation.testDestination &&
                            !mutationWithDestinationTestsWithoutDestination.includes(mutation)) {
                            mutationWithDestinationTestsWithoutDestination.push(mutation);
                        }
                    }
                }
                // eslint-disable-next-line no-console
                console.log((0, formatters_1.formatReportForConsole)(report));
                mutationReports.push(report);
                appReport.mutations = mutationReports;
                if (report.error ||
                    (report.latestSignOperationEvent &&
                        report.latestSignOperationEvent.type === "device-signature-requested")) {
                    (0, logs_1.log)("engine", `spec ${spec.name} is recreating the device because deviceAction didn't finished`);
                    await (0, speculos_1.releaseSpeculosDevice)(device.id);
                    device = await (0, speculos_1.createSpeculosDevice)(deviceParams);
                    if (spec.onSpeculosDeviceCreated) {
                        await spec.onSpeculosDeviceCreated(device);
                    }
                }
            }
            mutationsCount = {};
        }
        if (mutationReports.every(r => !r.mutation) &&
            accounts.some(a => a.spendableBalance.gt(spec.minViableAmount || 0))) {
            hintWarnings.push("No mutation were found possible. Yet there are funds in the accounts, please investigate.");
        }
        if (mutationThatProducedDestinationsWithoutTests.length) {
            hintWarnings.push("mutations should define a testDestination(): " +
                mutationThatProducedDestinationsWithoutTests.map(m => m.name).join(", "));
        }
        if (mutationWithDestinationTestsWithoutDestination.length) {
            hintWarnings.push("mutations should NOT define a testDestination() because there are no 'destination' sibling account found: " +
                mutationWithDestinationTestsWithoutDestination.map(m => m.name).join(", "));
        }
        mutationReports.forEach(m => {
            m.hintWarnings.forEach(h => {
                const txt = `mutation ${m.mutation?.name || "?"}: ${h}`;
                if (!hintWarnings.includes(txt)) {
                    hintWarnings.push(txt);
                }
            });
        });
        appReport.mutations = mutationReports;
        appReport.accountsAfter = accounts;
    }
    catch (e) {
        if (process.env.CI)
            console.error(e);
        appReport.fatalError = e;
        (0, logs_1.log)("engine", `spec ${spec.name} failed with ${String(e)}`);
    }
    finally {
        (0, logs_1.log)("engine", `spec ${spec.name} finished`);
        if (device)
            await (0, speculos_1.releaseSpeculosDevice)(device.id);
    }
    return appReport;
}
exports.runWithAppSpec = runWithAppSpec;
async function runOnAccount({ appCandidate, spec, device, account, accounts, accountIdsNeedResync, mutationsCount, resyncAccountsDuration, preloadedData, }) {
    const { mutations } = spec;
    let latestSignOperationEvent;
    const hintWarnings = [];
    const report = {
        spec,
        appCandidate,
        resyncAccountsDuration,
        hintWarnings,
    };
    try {
        const accountBridge = (0, bridge_1.getAccountBridge)(account);
        const accountBeforeTransaction = account;
        report.account = account;
        (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)}`);
        const maxSpendable = await accountBridge.estimateMaxSpendable({
            account,
        });
        report.maxSpendable = maxSpendable;
        (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)} maxSpendable=${maxSpendable.toString()}`);
        const candidates = [];
        const unavailableMutationReasons = [];
        for (const mutation of mutations) {
            try {
                const count = mutationsCount[mutation.name] || 0;
                (0, invariant_1.default)(count < (mutation.maxRun || Infinity), "maximum mutation run reached (%s)", count);
                const arg = {
                    appCandidate,
                    account,
                    bridge: accountBridge,
                    siblings: accounts.filter(a => a !== account),
                    maxSpendable,
                    preloadedData,
                };
                if (spec.transactionCheck)
                    spec.transactionCheck(arg);
                const r = mutation.transaction(arg);
                candidates.push({
                    mutation,
                    tx: r.transaction,
                    // $FlowFixMe what the hell
                    updates: r.updates,
                    destination: r.destination,
                });
            }
            catch (error) {
                if (process.env.CI)
                    console.error(error);
                unavailableMutationReasons.push({
                    mutation,
                    error,
                });
            }
        }
        const candidate = (0, sample_1.default)(candidates);
        if (!candidate) {
            // no mutation were suitable
            report.unavailableMutationReasons = unavailableMutationReasons;
            return report;
        }
        // a mutation was chosen
        const { tx, mutation, updates } = candidate;
        report.mutation = mutation;
        report.mutationTime = (0, performance_now_1.default)();
        // prepare the transaction and ensure it's valid
        let status;
        let errors = [];
        let transaction = await accountBridge.prepareTransaction(account, tx);
        deepFreezeTransaction(transaction);
        for (const patch of updates) {
            if (patch) {
                await accountBridge.getTransactionStatus(account, transaction); // result is unused but that would happen in normal flow
                report.transaction = transaction;
                transaction = await accountBridge.updateTransaction(transaction, patch);
                report.transaction = transaction;
                transaction = await accountBridge.prepareTransaction(account, transaction);
                deepFreezeTransaction(transaction);
            }
        }
        report.transaction = transaction;
        const destination = candidate.destination || accounts.find(a => a.freshAddress === transaction.recipient);
        report.destination = destination;
        status = await accountBridge.getTransactionStatus(account, transaction);
        errors = Object.values(status.errors);
        deepFreezeStatus(status);
        report.status = status;
        report.statusTime = (0, performance_now_1.default)();
        const warnings = Object.values(status.warnings);
        if (mutation.recoverBadTransactionStatus) {
            if (errors.length || warnings.length) {
                // there is something to recover from
                const recovered = mutation.recoverBadTransactionStatus({
                    transaction,
                    status,
                    account,
                    bridge: accountBridge,
                });
                if (recovered && recovered !== transaction) {
                    deepFreezeTransaction(recovered);
                    report.recoveredFromTransactionStatus = {
                        transaction,
                        status,
                    };
                    report.transaction = transaction = recovered;
                    status = await accountBridge.getTransactionStatus(account, transaction);
                    errors = Object.values(status.errors);
                    deepFreezeStatus(status);
                    report.status = status;
                    report.statusTime = (0, performance_now_1.default)();
                }
            }
        }
        // without recovering mechanism, we simply assume an error is a failure
        if (errors.length) {
            console.warn(status);
            (0, bot_test_context_1.botTest)("mutation must not have tx status errors", () => {
                // all mutation must express transaction that are POSSIBLE
                // recoveredFromTransactionStatus can also be used to solve this for tricky cases
                throw errors[0];
            });
        }
        const { expectStatusWarnings } = mutation;
        if (warnings.length || expectStatusWarnings) {
            const expected = expectStatusWarnings &&
                expectStatusWarnings({
                    transaction,
                    status,
                    account,
                    bridge: accountBridge,
                });
            if (expected) {
                (0, bot_test_context_1.botTest)("verify status.warnings expectations", () => (0, expect_1.default)(status.warnings).toEqual(expected));
            }
            else {
                for (const k in status.warnings) {
                    const e = status.warnings[k];
                    hintWarnings.push(`unexpected status.warnings.${k} = ${String(e)} – Please implement expectStatusWarnings on the mutation if expected`);
                }
            }
        }
        mutationsCount[mutation.name] = (mutationsCount[mutation.name] || 0) + 1;
        // sign the transaction with speculos
        (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)} signing`);
        const signedOperation = await (0, rxjs_1.firstValueFrom)(accountBridge
            .signOperation({
            account,
            transaction,
            deviceId: device.id,
        })
            .pipe((0, operators_1.tap)(e => {
            latestSignOperationEvent = e;
            (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)}: ${e.type}`);
        }), autoSignTransaction({
            transport: device.transport,
            deviceAction: mutation.deviceAction || spec.genericDeviceAction,
            appCandidate,
            account,
            transaction,
            status,
        }), (0, operators_1.first)((e) => e.type === "signed"), (0, operators_1.map)(e => ((0, invariant_1.default)(e.type === "signed", "signed operation"), e.signedOperation))));
        deepFreezeSignedOperation(signedOperation);
        report.signedOperation = signedOperation;
        report.signedTime = (0, performance_now_1.default)();
        // at this stage, we are about to broadcast we assume we will need to resync receiver account
        if (report.destination) {
            accountIdsNeedResync.push(report.destination.id);
        }
        // even if the test will actively sync the account, we need to pessimisticly assume it won't, we may not reach the final step of it.
        // after the runOnAccount() call, we actively remove from accountIdsNeedResync the account.id if it is actually sucessful
        accountIdsNeedResync.push(account.id);
        // broadcast the transaction
        const optimisticOperation = (0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")
            ? signedOperation.operation
            : await accountBridge
                .broadcast({
                account,
                signedOperation,
            })
                .catch(e => {
                // wrap the error into some bot test context
                (0, bot_test_context_1.botTest)("during broadcast", () => {
                    throw e;
                });
            });
        deepFreezeOperation(optimisticOperation);
        report.optimisticOperation = optimisticOperation;
        report.broadcastedTime = (0, performance_now_1.default)();
        (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)}/${optimisticOperation.hash} broadcasted`);
        // wait the condition are good (operation confirmed)
        // test() is run over and over until either timeout is reach OR success
        const testBefore = (0, performance_now_1.default)();
        const timeOut = mutation.testTimeout || spec.testTimeout || 5 * 60 * 1000;
        const step = account => {
            if (spec.skipOperationHistory) {
                return optimisticOperation;
            }
            const timedOut = (0, performance_now_1.default)() - testBefore > timeOut;
            const operation = account.operations.find(o => o.id === optimisticOperation.id);
            if (timedOut && !operation) {
                (0, bot_test_context_1.botTest)("waiting operation id to appear after broadcast", () => {
                    throw new Error("could not find optimisticOperation " + optimisticOperation.id);
                });
            }
            if (operation) {
                try {
                    const arg = {
                        accountBeforeTransaction,
                        transaction,
                        status,
                        optimisticOperation,
                        operation,
                        account,
                    };
                    transactionTest(arg);
                    if (spec.test)
                        spec.test(arg);
                    if (mutation.test)
                        mutation.test(arg);
                    report.testDuration = (0, performance_now_1.default)() - testBefore;
                }
                catch (e) {
                    // this is too critical to "ignore"
                    if (e instanceof TypeError || e instanceof SyntaxError) {
                        report.testDuration = (0, performance_now_1.default)() - testBefore;
                        throw e;
                    }
                    // We never reach the final test success
                    if (timedOut) {
                        report.testDuration = (0, performance_now_1.default)() - testBefore;
                        throw e;
                    }
                    (0, logs_1.log)("bot", "failed confirm test. trying again. " + String(e));
                    // We will try again
                    return;
                }
            }
            return operation;
        };
        const result = await awaitAccountOperation({ account, step });
        const { account: finalAccount, value: operation } = result;
        report.finalAccount = finalAccount;
        report.operation = operation;
        report.confirmedTime = (0, performance_now_1.default)();
        (0, logs_1.log)("engine", `spec ${spec.name}/${(0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account)}/${optimisticOperation.hash} confirmed`);
        const destinationBeforeTransaction = destination;
        if (destination && mutation.testDestination) {
            const { testDestination } = mutation;
            // test() is run over and over until either timeout is reach OR success
            const ntestBefore = (0, performance_now_1.default)();
            const newTimeOut = Math.max(10000, timeOut - (ntestBefore - testBefore));
            (0, logs_1.log)("bot", "remaining time to test destination: " + (newTimeOut / 1000).toFixed(0) + "s");
            const sendingOperation = operation;
            const step = account => {
                if (spec.skipOperationHistory) {
                    return sendingOperation;
                }
                const timedOut = (0, performance_now_1.default)() - ntestBefore > newTimeOut;
                let operation;
                try {
                    operation = account.operations.find(op => op.hash === sendingOperation.hash);
                    (0, bot_test_context_1.botTest)("destination account should receive an operation (by tx hash)", () => (0, invariant_1.default)(operation, "no operation found with hash %s", sendingOperation.hash));
                    if (!operation)
                        throw new Error();
                    const arg = {
                        transaction,
                        status,
                        sendingAccount: finalAccount,
                        sendingOperation,
                        operation,
                        destinationBeforeTransaction,
                        destination: account,
                    };
                    (0, bot_test_context_1.botTest)("destination", () => testDestination(arg));
                    report.testDestinationDuration = (0, performance_now_1.default)() - ntestBefore;
                }
                catch (e) {
                    // this is too critical to "ignore"
                    if (e instanceof TypeError || e instanceof SyntaxError) {
                        report.testDuration = (0, performance_now_1.default)() - testBefore;
                        throw e;
                    }
                    // We never reach the final test success
                    if (timedOut) {
                        report.testDestinationDuration = (0, performance_now_1.default)() - ntestBefore;
                        throw e;
                    }
                    (0, logs_1.log)("bot", "failed destination confirm test. trying again. " + String(e));
                    // We will try again
                    return;
                }
                return operation;
            };
            const result = await awaitAccountOperation({
                account: destination,
                step,
            });
            report.finalDestination = result.account;
            report.finalDestinationOperation = result.value;
            report.destinationConfirmedTime = (0, performance_now_1.default)();
        }
    }
    catch (error) {
        if (process.env.CI)
            console.error(error);
        (0, logs_1.log)("mutation-error", spec.name + ": " + (0, formatters_1.formatError)(error, true));
        report.error = error;
        report.errorTime = (0, performance_now_1.default)();
    }
    report.latestSignOperationEvent = latestSignOperationEvent;
    return report;
}
exports.runOnAccount = runOnAccount;
async function syncAccount(initialAccount) {
    const acc = await (0, rxjs_1.firstValueFrom)((0, bridge_1.getAccountBridge)(initialAccount)
        .sync(initialAccount, {
        paginationConfig: {},
    })
        .pipe((0, operators_1.reduce)((a, f) => f(a), initialAccount), (0, operators_1.timeout)({
        each: 10 * 60 * 1000,
        with: () => (0, rxjs_1.throwError)(() => new Error("account sync timeout for " +
            (0, accountName_1.getDefaultAccountNameForCurrencyIndex)(initialAccount))),
    })));
    return deepFreezeAccount(acc);
}
function autoSignTransaction({ transport, deviceAction, appCandidate, account, transaction, status, disableStrictStepValueValidation, }) {
    let sub;
    let observer;
    let state;
    const recentEvents = [];
    return (0, operators_1.mergeMap)(e => {
        if (e.type === "device-signature-requested") {
            return new rxjs_1.Observable(o => {
                if (observer) {
                    o.error(new Error("device-signature-requested should not be called twice!"));
                    return;
                }
                observer = o;
                o.next(e);
                const timeout = setTimeout(() => {
                    o.error(new Error("device action timeout. Recent events was:\n" +
                        recentEvents.map(e => JSON.stringify(e)).join("\n")));
                }, 60 * 1000);
                sub = transport.automationEvents
                    .pipe(
                // deduplicate two successive identical text in events (that can sometimes occur with speculos)
                (0, operators_1.distinctUntilChanged)((a, b) => a.text === b.text))
                    .subscribe({
                    next: event => {
                        recentEvents.push(event);
                        if (recentEvents.length > 5) {
                            recentEvents.shift();
                        }
                        try {
                            state = deviceAction({
                                appCandidate,
                                account,
                                transaction,
                                event,
                                transport,
                                state,
                                status,
                                disableStrictStepValueValidation,
                            });
                        }
                        catch (e) {
                            o.error(e);
                        }
                    },
                    complete: () => {
                        o.complete();
                    },
                    error: e => {
                        o.error(e);
                    },
                });
                return () => {
                    clearTimeout(timeout);
                    sub.unsubscribe();
                };
            });
        }
        else if (observer) {
            observer.complete();
            observer = null;
        }
        if (sub) {
            sub.unsubscribe();
        }
        return (0, rxjs_1.of)(e);
    });
}
exports.autoSignTransaction = autoSignTransaction;
function awaitAccountOperation({ account, step, }) {
    (0, logs_1.log)("engine", "awaitAccountOperation on " + (0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account));
    let syncCounter = 0;
    let acc = account;
    let lastSync = (0, performance_now_1.default)();
    const loopDebounce = 1000;
    const targetInterval = (0, live_env_1.getEnv)("SYNC_PENDING_INTERVAL");
    async function loop() {
        const value = step(acc);
        if (value) {
            return {
                account: acc,
                value,
            };
        }
        const spent = (0, performance_now_1.default)() - lastSync;
        await (0, promise_2.delay)(Math.max(loopDebounce, targetInterval - spent));
        lastSync = (0, performance_now_1.default)();
        (0, logs_1.log)("engine", "sync #" + syncCounter++ + " on " + (0, accountName_1.getDefaultAccountNameForCurrencyIndex)(account));
        acc = await syncAccount(acc);
        const r = await loop();
        return r;
    }
    return loop();
}
// generic transaction test: make sure you are sure all coins suit the tests here
function transactionTest({ operation, optimisticOperation, account, accountBeforeTransaction, }) {
    const dt = Date.now() - operation.date.getTime();
    const lowerThreshold = -60 * 1000; // -1mn accepted
    const upperThreshold = 30 * 60 * 1000; // 30mn up
    (0, bot_test_context_1.botTest)("operation.date must not be in future", () => (0, expect_1.default)(dt).toBeGreaterThan(lowerThreshold));
    (0, bot_test_context_1.botTest)("operation.date less than 30mn ago", () => (0, expect_1.default)(dt).toBeLessThan(upperThreshold));
    (0, bot_test_context_1.botTest)("operation must not failed", () => {
        (0, expect_1.default)(!operation.hasFailed).toBe(true);
    });
    const { blockAvgTime } = account.currency;
    if (blockAvgTime && account.blockHeight) {
        const expected = (0, operation_1.getOperationConfirmationNumber)(operation, account);
        const expectedMax = Math.ceil(upperThreshold / blockAvgTime);
        (0, bot_test_context_1.botTest)("low amount of confirmations", () => (0, invariant_1.default)(expected <= expectedMax, "There are way too much operation confirmation for a small amount of time. %s < %s", expected, expectedMax));
    }
    (0, bot_test_context_1.botTest)("optimisticOperation.value must not be NaN", () => (0, expect_1.default)(!optimisticOperation.value.isNaN()).toBe(true));
    (0, bot_test_context_1.botTest)("optimisticOperation.fee must not be NaN", () => (0, expect_1.default)(!optimisticOperation.fee.isNaN()).toBe(true));
    (0, bot_test_context_1.botTest)("operation.value must not be NaN", () => (0, expect_1.default)(!operation.value.isNaN()).toBe(true));
    (0, bot_test_context_1.botTest)("operation.fee must not be NaN", () => (0, expect_1.default)(!operation.fee.isNaN()).toBe(true));
    (0, bot_test_context_1.botTest)("successful tx should increase by at least 1 the number of account.operations", () => (0, expect_1.default)(account.operations.length).toBeGreaterThanOrEqual(accountBeforeTransaction.operations.length + 1));
}
/*
function deepFreeze(object, path: string[] = []) {
  // Retrieve the property names defined on object
  const propNames = Reflect.ownKeys(object);

  // Freeze properties before freezing self
  for (const name of propNames) {
    const value = object[name];

    if (value && typeof value === "object") {
      deepFreeze(value, [...path, name.toString()]);
    }
  }

  try {
    return Object.freeze(object);
  } catch (e) {
    console.warn("Can't freeze at " + path.join("."));
  }
}
*/
// deepFreeze logic specialized to freeze an account (it's too problematic to deep freeze all objects of Account too deeply)
function deepFreezeAccount(account) {
    Object.freeze(account);
    if (account.type === "Account") {
        account.subAccounts?.forEach(deepFreezeAccount);
        account.nfts?.forEach(Object.freeze);
    }
    account.operations.forEach(deepFreezeOperation);
    account.pendingOperations.forEach(deepFreezeOperation);
    return account;
}
function deepFreezeOperation(operation) {
    Object.freeze(operation);
    return operation;
}
function deepFreezeSignedOperation(signedOperation) {
    Object.freeze(signedOperation);
    Object.freeze(signedOperation.operation);
    return signedOperation;
}
function deepFreezeStatus(transactionStatus) {
    Object.freeze(transactionStatus);
    return transactionStatus;
}
function deepFreezeTransaction(transaction) {
    Object.freeze(transaction);
    return transaction;
}
//# sourceMappingURL=engine.js.map