"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.testBridge = exports.syncAccount = void 0;
const invariant_1 = __importDefault(require("invariant"));
const bignumber_js_1 = require("bignumber.js");
const operators_1 = require("rxjs/operators");
const flatMap_1 = __importDefault(require("lodash/flatMap"));
const omit_1 = __importDefault(require("lodash/omit"));
const errors_1 = require("@ledgerhq/errors");
const account_1 = require("../../account");
const currencies_1 = require("../../currencies");
const operation_1 = require("../../operation");
const transaction_1 = require("../../transaction");
const bridge_1 = require("../../bridge");
const mockDevice_1 = require("./mockDevice");
const rxjs_1 = require("rxjs");
const accountName_1 = require("@ledgerhq/live-wallet/accountName");
const warnDev = process.env.CI ? (..._args) => { } : (...msg) => console.warn(...msg);
// FIXME move out into DatasetTest to be defined in
const blacklistOpsSumEq = {
    currencies: ["ripple", "ethereum"],
    impls: ["mock"],
};
function expectBalanceIsOpsSum(a) {
    expect(a.balance).toEqual(a.operations.reduce((sum, op) => sum.plus((0, operation_1.getOperationAmountNumber)(op)), new bignumber_js_1.BigNumber(0)));
}
const defaultSyncConfig = {
    paginationConfig: {},
    blacklistedTokenIds: ["ethereum/erc20/ampleforth", "ethereum/erc20/steth"],
};
function syncAccount(bridge, account, syncConfig = defaultSyncConfig) {
    return (0, rxjs_1.firstValueFrom)(bridge.sync(account, syncConfig).pipe((0, operators_1.reduce)((a, f) => f(a), account)));
}
exports.syncAccount = syncAccount;
function testBridge(data) {
    // covers all bridges through many different accounts
    // to test the common shared properties of bridges.
    const accountsRelated = [];
    const currenciesRelated = [];
    const { implementations, currencies } = data;
    Object.keys(currencies).forEach(currencyId => {
        const currencyData = currencies[currencyId];
        const currency = (0, currencies_1.getCryptoCurrencyById)(currencyId);
        currenciesRelated.push({
            currencyData,
            currency,
        });
        const accounts = currencyData.accounts || [];
        accounts.forEach(accountData => implementations.forEach(impl => {
            if (accountData.implementations && !accountData.implementations.includes(impl)) {
                return;
            }
            const account = (0, account_1.fromAccountRaw)({
                ...accountData.raw,
                id: (0, account_1.encodeAccountId)({
                    ...(0, account_1.decodeAccountId)(accountData.raw.id),
                    type: impl,
                }),
            });
            accountsRelated.push({
                currencyData,
                accountData,
                account,
                impl,
            });
        }));
    });
    const accountsFoundInScanAccountsMap = {};
    currenciesRelated.map(({ currencyData, currency }) => {
        const bridge = (0, bridge_1.getCurrencyBridge)(currency);
        const scanAccounts = async (apdus) => {
            const deviceId = await (0, mockDevice_1.mockDeviceWithAPDUs)(apdus, currencyData.mockDeviceOptions);
            try {
                const accounts = await (0, rxjs_1.firstValueFrom)(bridge
                    .scanAccounts({
                    currency,
                    deviceId,
                    syncConfig: defaultSyncConfig,
                })
                    .pipe((0, operators_1.filter)(e => e.type === "discovered"), (0, operators_1.map)(e => e.account), (0, operators_1.reduce)((all, a) => all.concat(a), [])));
                return accounts;
            }
            catch (e) {
                console.error(e.message);
                throw e;
            }
            finally {
                (0, mockDevice_1.releaseMockDevice)(deviceId);
            }
        };
        const scanAccountsCaches = {};
        const scanAccountsCached = apdus => scanAccountsCaches[apdus] || (scanAccountsCaches[apdus] = scanAccounts(apdus));
        describe(currency.id + " currency bridge", () => {
            const { scanAccounts, FIXME_ignoreAccountFields, FIXME_ignoreOperationFields, FIXME_ignorePreloadFields, } = currencyData;
            test("functions are defined", () => {
                expect(typeof bridge.scanAccounts).toBe("function");
                expect(typeof bridge.preload).toBe("function");
                expect(typeof bridge.hydrate).toBe("function");
            });
            if (FIXME_ignorePreloadFields !== true) {
                test("preload and rehydrate", async () => {
                    const data1 = (await bridge.preload(currency)) || {};
                    const data1filtered = (0, omit_1.default)(data1, FIXME_ignorePreloadFields || []);
                    bridge.hydrate(data1filtered, currency);
                    if (data1filtered) {
                        const serialized1 = JSON.parse(JSON.stringify(data1filtered));
                        bridge.hydrate(serialized1, currency);
                        expect(serialized1).toBeDefined();
                        const data2 = (await bridge.preload(currency)) || {};
                        const data2filtered = (0, omit_1.default)(data2, FIXME_ignorePreloadFields || []);
                        if (data2filtered) {
                            bridge.hydrate(data2filtered, currency);
                            expect(data1filtered).toMatchObject(data2filtered);
                            const serialized2 = JSON.parse(JSON.stringify(data2filtered));
                            expect(serialized1).toMatchObject(serialized2);
                            bridge.hydrate(serialized2, currency);
                        }
                    }
                });
            }
            if (scanAccounts) {
                if (FIXME_ignoreOperationFields && FIXME_ignoreOperationFields.length) {
                    warnDev(currency.id +
                        " is ignoring operation fields: " +
                        FIXME_ignoreOperationFields.join(", "));
                }
                if (FIXME_ignoreAccountFields && FIXME_ignoreAccountFields.length) {
                    warnDev(currency.id + " is ignoring account fields: " + FIXME_ignoreAccountFields.join(", "));
                }
                describe("scanAccounts", () => {
                    scanAccounts.forEach(sa => {
                        // we start running the scan accounts in parallel!
                        test(sa.name, async () => {
                            const accounts = await scanAccountsCached(sa.apdus);
                            accounts.forEach(a => {
                                accountsFoundInScanAccountsMap[a.id] = a;
                            });
                            if (!sa.unstableAccounts) {
                                const raws = (0, flatMap_1.default)(accounts, a => {
                                    const main = (0, account_1.toAccountRaw)(a);
                                    if (!main.subAccounts)
                                        return [main];
                                    return [{ ...main, subAccounts: [] }, ...main.subAccounts];
                                });
                                const heads = raws.map(a => {
                                    const copy = (0, omit_1.default)(a, [
                                        "operations",
                                        "lastSyncDate",
                                        "creationDate",
                                        "blockHeight",
                                        "balanceHistory",
                                        "balanceHistoryCache",
                                    ].concat(FIXME_ignoreAccountFields || []));
                                    return copy;
                                });
                                const ops = raws.map(({ operations }) => operations
                                    .slice(0)
                                    .sort((a, b) => a.id.localeCompare(b.id))
                                    .map(op => {
                                    const copy = (0, omit_1.default)(op, ["date"].concat(FIXME_ignoreOperationFields || []));
                                    return copy;
                                }));
                                expect(heads).toMatchSnapshot();
                                expect(ops).toMatchSnapshot();
                            }
                            const testFn = sa.test;
                            if (testFn) {
                                await testFn(expect, accounts, bridge);
                            }
                        });
                        test("estimateMaxSpendable is between 0 and account balance", async () => {
                            const accounts = await scanAccountsCached(sa.apdus);
                            for (const account of accounts) {
                                const accountBridge = (0, bridge_1.getAccountBridge)(account);
                                const estimation = await accountBridge.estimateMaxSpendable({
                                    account,
                                });
                                expect(estimation.gte(0)).toBe(true);
                                expect(estimation.lte(account.spendableBalance)).toBe(true);
                                for (const sub of account.subAccounts || []) {
                                    const estimation = await accountBridge.estimateMaxSpendable({
                                        parentAccount: account,
                                        account: sub,
                                    });
                                    expect(estimation.gte(0)).toBe(true);
                                    expect(estimation.lte(sub.balance)).toBe(true);
                                }
                            }
                        });
                        test("no unconfirmed account", async () => {
                            const accounts = await scanAccountsCached(sa.apdus);
                            for (const account of (0, account_1.flattenAccounts)(accounts)) {
                                expect({
                                    id: account.id,
                                    unconfirmed: (0, account_1.isAccountBalanceUnconfirmed)(account),
                                }).toEqual({
                                    id: account.id,
                                    unconfirmed: false,
                                });
                            }
                        });
                        test("creationDate is correct", async () => {
                            const accounts = await scanAccountsCached(sa.apdus);
                            for (const account of (0, account_1.flattenAccounts)(accounts)) {
                                if (account.operations.length) {
                                    const op = account.operations[account.operations.length - 1];
                                    if (account.creationDate.getTime() > op.date.getTime()) {
                                        warnDev(`OP ${op.id} have date=${op.date.toISOString()} older than account.creationDate=${account.creationDate.toISOString()}`);
                                    }
                                    expect(account.creationDate.getTime()).not.toBeGreaterThan(op.date.getTime());
                                }
                            }
                        });
                    });
                });
            }
            const currencyDataTest = currencyData.test;
            if (currencyDataTest) {
                test(currency.id + " specific test", () => currencyDataTest(expect, bridge));
            }
        });
        const accounts = currencyData.accounts || [];
        if (accounts.length) {
            const accountsInScan = [];
            const accountsNotInScan = [];
            accounts.forEach(({ raw }) => {
                if (accountsFoundInScanAccountsMap[raw.id]) {
                    accountsInScan.push(raw.id);
                }
                else {
                    accountsNotInScan.push(raw.id);
                }
            });
            if (accountsInScan.length === 0) {
                warnDev(`/!\\ CURRENCY '${currency.id}' define accounts that are NOT in scanAccounts. please add at least one account that is from scanAccounts. This helps testing scanned accounts are fine and it also help performance.`);
            }
            if (accountsNotInScan.length === 0) {
                warnDev(`/!\\ CURRENCY '${currency.id}' define accounts that are ONLY in scanAccounts. please add one account that is NOT from scanAccounts. This helps covering the "recovering from xpub" mecanism.`);
            }
        }
    });
    accountsRelated
        .map(({ account, ...rest }) => {
        const bridge = (0, bridge_1.getAccountBridge)(account, null);
        if (!bridge)
            throw new Error("no bridge for " + account.id);
        let accountSyncedPromise;
        // lazy eval so we don't run this yet
        const getSynced = () => accountSyncedPromise || (accountSyncedPromise = syncAccount(bridge, account));
        return {
            getSynced,
            bridge,
            initialAccount: account,
            ...rest,
        };
    })
        .forEach(arg => {
        const { getSynced, bridge, initialAccount, accountData, impl } = arg;
        const makeTest = (name, fn) => {
            if (accountData.FIXME_tests && accountData.FIXME_tests.some(r => name.match(r))) {
                warnDev("FIXME test was skipped. " + name + " for " + (0, accountName_1.getDefaultAccountName)(initialAccount));
                return;
            }
            test(name, fn);
        };
        describe(impl + " bridge on account " + (0, accountName_1.getDefaultAccountName)(initialAccount), () => {
            describe("sync", () => {
                makeTest("succeed", async () => {
                    const account = await getSynced();
                    expect((0, account_1.fromAccountRaw)((0, account_1.toAccountRaw)(account))).toBeDefined();
                });
                if (impl !== "mock") {
                    const accFromScanAccounts = accountsFoundInScanAccountsMap[initialAccount.id];
                    if (accFromScanAccounts) {
                        makeTest("matches the same account from scanAccounts", async () => {
                            const acc = await getSynced();
                            expect(acc).toMatchObject(accFromScanAccounts);
                        });
                    }
                }
                makeTest("account have no NaN values", async () => {
                    const account = await getSynced();
                    [account, ...(account.subAccounts || [])].forEach(a => {
                        expect(a.balance.isNaN()).toBe(false);
                        expect(a.operations.find(op => op.value.isNaN())).toBe(undefined);
                        expect(a.operations.find(op => op.fee.isNaN())).toBe(undefined);
                    });
                });
                if (!blacklistOpsSumEq.currencies.includes(initialAccount.currency.id) &&
                    !blacklistOpsSumEq.impls.includes(impl)) {
                    makeTest("balance is sum of ops", async () => {
                        const account = await getSynced();
                        expectBalanceIsOpsSum(account);
                        if (account.subAccounts) {
                            account.subAccounts.forEach(expectBalanceIsOpsSum);
                        }
                    });
                    makeTest("balance and spendableBalance boundaries", async () => {
                        const account = await getSynced();
                        expect(account.balance).toBeInstanceOf(bignumber_js_1.BigNumber);
                        expect(account.spendableBalance).toBeInstanceOf(bignumber_js_1.BigNumber);
                        expect(account.balance.lt(0)).toBe(false);
                        expect(account.spendableBalance.lt(0)).toBe(false);
                        expect(account.spendableBalance.lte(account.balance)).toBe(true);
                    });
                }
                makeTest("existing operations object refs are preserved", async () => {
                    const account = await getSynced();
                    const count = Math.floor(account.operations.length / 2);
                    const operations = account.operations.slice(count);
                    const copy = {
                        ...account,
                        operations,
                        blockHeight: 0,
                    };
                    const synced = await syncAccount(bridge, copy);
                    if (initialAccount.id.includes("ripple"))
                        return; // ripple wont work because of the current implementation of pagination
                    expect(synced.operations.length).toBe(account.operations.length);
                    // same ops are restored
                    expect(synced.operations).toEqual(account.operations);
                    if (initialAccount.id.startsWith("ethereumjs"))
                        return; // ethereumjs seems to have a bug on this, we ignore because the impl will be dropped.
                    // existing ops are keeping refs
                    synced.operations.slice(count).forEach((op, i) => {
                        expect(op).toStrictEqual(operations[i]);
                    });
                });
                makeTest("pendingOperations are cleaned up", async () => {
                    const account = await getSynced();
                    if (initialAccount.id.includes("ripple"))
                        return; // ripple wont work because of the current implementation of pagination
                    if (account.operations.length) {
                        const operations = account.operations.slice(1);
                        const pendingOperations = [account.operations[0]];
                        const copy = {
                            ...account,
                            operations,
                            pendingOperations,
                            blockHeight: 0,
                        };
                        const synced = await syncAccount(bridge, copy);
                        // same ops are restored
                        expect(synced.operations).toEqual(account.operations);
                        // pendingOperations is empty
                        expect(synced.pendingOperations).toEqual([]);
                    }
                });
                makeTest("there are no Operation dups (by id)", async () => {
                    const account = await getSynced();
                    const seen = {};
                    account.operations.forEach(op => {
                        expect(seen[op.id]).toBeUndefined();
                        seen[op.id] = op.id;
                    });
                });
            });
            describe("createTransaction", () => {
                makeTest("empty transaction is an object with empty recipient and zero amount", () => {
                    expect(bridge.createTransaction(initialAccount)).toMatchObject({
                        amount: new bignumber_js_1.BigNumber(0),
                        recipient: "",
                    });
                });
                makeTest("empty transaction is equals to itself", () => {
                    expect(bridge.createTransaction(initialAccount)).toEqual(bridge.createTransaction(initialAccount));
                });
                makeTest("empty transaction correctly serialize", () => {
                    const t = bridge.createTransaction(initialAccount);
                    expect((0, transaction_1.fromTransactionRaw)((0, transaction_1.toTransactionRaw)(t))).toEqual(t);
                });
                makeTest("transaction with amount and recipient correctly serialize", async () => {
                    const account = await getSynced();
                    const t = {
                        ...bridge.createTransaction(account),
                        amount: new bignumber_js_1.BigNumber(1000),
                        recipient: account.freshAddress,
                    };
                    expect((0, transaction_1.fromTransactionRaw)((0, transaction_1.toTransactionRaw)(t))).toEqual(t);
                });
            });
            describe("updateTransaction", () => {
                // stability: function called twice will return the same object reference
                // (=== convergence so we can stop looping, typically because transaction will be a hook effect dependency of prepareTransaction)
                function expectStability(t, patch) {
                    const t2 = bridge.updateTransaction(t, patch);
                    const t3 = bridge.updateTransaction(t2, patch);
                    expect(t2).toBe(t3);
                }
                makeTest("ref stability on empty transaction", async () => {
                    const account = await getSynced();
                    const tx = bridge.createTransaction(account);
                    expectStability(tx, {});
                });
                makeTest("ref stability on self transaction", async () => {
                    const account = await getSynced();
                    const tx = bridge.createTransaction(account);
                    expectStability(tx, {
                        amount: new bignumber_js_1.BigNumber(1000),
                        recipient: account.freshAddress,
                    });
                });
            });
            describe("prepareTransaction", () => {
                // stability: function called twice will return the same object reference
                // (=== convergence so we can stop looping, typically because transaction will be a hook effect dependency of prepareTransaction)
                async function expectStability(account, t) {
                    let t2 = await bridge.prepareTransaction(account, t);
                    let t3 = await bridge.prepareTransaction(account, t2);
                    t2 = (0, omit_1.default)(t2, arg.currencyData.IgnorePrepareTransactionFields || []);
                    t3 = (0, omit_1.default)(t3, arg.currencyData.IgnorePrepareTransactionFields || []);
                    expect(t2).toStrictEqual(t3);
                }
                makeTest("ref stability on empty transaction", async () => {
                    const account = await getSynced();
                    await expectStability(account, bridge.createTransaction(account));
                });
                makeTest("ref stability on self transaction", async () => {
                    const account = await getSynced();
                    await expectStability(account, {
                        ...bridge.createTransaction(account),
                        amount: new bignumber_js_1.BigNumber(1000),
                        recipient: account.freshAddress,
                    });
                });
                makeTest("can be run in parallel and all yield same results", async () => {
                    const account = await getSynced();
                    const t = {
                        ...bridge.createTransaction(account),
                        amount: new bignumber_js_1.BigNumber(1000),
                        recipient: account.freshAddress,
                    };
                    const stable = await bridge.prepareTransaction(account, t);
                    const first = (0, omit_1.default)(await bridge.prepareTransaction(account, stable), arg.currencyData.IgnorePrepareTransactionFields || []);
                    const concur = await Promise.all(Array(3)
                        .fill(null)
                        .map(() => bridge.prepareTransaction(account, stable)));
                    concur.forEach(r => {
                        r = (0, omit_1.default)(r, arg.currencyData.IgnorePrepareTransactionFields || []);
                        expect(r).toEqual(first);
                    });
                });
            });
            describe("getTransactionStatus", () => {
                makeTest("can be called on an empty transaction", async () => {
                    const account = await getSynced();
                    const t = {
                        ...bridge.createTransaction(account),
                        feePerByte: new bignumber_js_1.BigNumber(0.0001),
                    };
                    const s = await bridge.getTransactionStatus(account, t);
                    expect(s).toBeDefined();
                    expect(s.errors).toHaveProperty("recipient");
                    expect(s).toHaveProperty("totalSpent");
                    expect(s.totalSpent).toBeInstanceOf(bignumber_js_1.BigNumber);
                    expect(s).toHaveProperty("estimatedFees");
                    expect(s.estimatedFees).toBeInstanceOf(bignumber_js_1.BigNumber);
                    expect(s).toHaveProperty("amount");
                    expect(s.amount).toBeInstanceOf(bignumber_js_1.BigNumber);
                    expect(s.amount).toEqual(new bignumber_js_1.BigNumber(0));
                });
                makeTest("can be called on an empty prepared transaction", async () => {
                    const account = await getSynced();
                    const t = await bridge.prepareTransaction(account, {
                        ...bridge.createTransaction(account),
                        feePerByte: new bignumber_js_1.BigNumber(0.0001),
                    });
                    const s = await bridge.getTransactionStatus(account, t);
                    expect(s).toBeDefined(); // FIXME i'm not sure if we can establish more shared properties
                });
                makeTest("Default empty recipient have a recipientError", async () => {
                    const account = await getSynced();
                    const t = {
                        ...bridge.createTransaction(account),
                        feePerByte: new bignumber_js_1.BigNumber(0.0001),
                    };
                    const status = await bridge.getTransactionStatus(account, t);
                    expect(status.errors.recipient).toBeInstanceOf(errors_1.RecipientRequired);
                });
                makeTest("invalid recipient have a recipientError", async () => {
                    const account = await getSynced();
                    const t = {
                        ...bridge.createTransaction(account),
                        feePerByte: new bignumber_js_1.BigNumber(0.0001),
                        recipient: "invalidADDRESS",
                    };
                    const status = await bridge.getTransactionStatus(account, t);
                    expect(status.errors.recipient).toBeInstanceOf(errors_1.InvalidAddress);
                });
                makeTest("Default empty amount has an amount error", async () => {
                    const account = await getSynced();
                    const t = await bridge.prepareTransaction(account, {
                        ...bridge.createTransaction(account),
                        feePerByte: new bignumber_js_1.BigNumber(0.0001),
                    });
                    const status = await bridge.getTransactionStatus(account, t);
                    expect(status.errors.amount).toBeInstanceOf(errors_1.AmountRequired);
                });
                const accountDataTest = accountData.test;
                if (accountDataTest) {
                    makeTest("account specific test", async () => accountDataTest(expect, await getSynced(), bridge));
                }
                (accountData.transactions || []).forEach(({ name, transaction, expectedStatus, apdus, testSignedOperation, test: testFn }) => {
                    makeTest("transaction " + name, async () => {
                        const account = await getSynced();
                        let t = typeof transaction === "function"
                            ? transaction(bridge.createTransaction(account), account, bridge)
                            : transaction;
                        t = await bridge.prepareTransaction(account, {
                            feePerByte: new bignumber_js_1.BigNumber(0.0001),
                            ...t,
                        });
                        const s = await bridge.getTransactionStatus(account, t);
                        if (expectedStatus) {
                            const es = typeof expectedStatus === "function"
                                ? expectedStatus(account, t, s)
                                : expectedStatus;
                            const { errors, warnings } = es;
                            // we match errors and warnings
                            errors && expect(s.errors).toMatchObject(errors);
                            warnings && expect(s.warnings).toMatchObject(warnings);
                            // now we match rest of fields but using the raw version for better readability
                            const restRaw = (0, transaction_1.toTransactionStatusRaw)({
                                ...s,
                                ...es,
                            }, account.currency.family);
                            delete restRaw.errors;
                            delete restRaw.warnings;
                            for (const k in restRaw) {
                                if (!(k in es)) {
                                    delete restRaw[k];
                                }
                            }
                            expect((0, transaction_1.toTransactionStatusRaw)(s, account.currency.family)).toMatchObject(restRaw);
                        }
                        if (testFn) {
                            await testFn(expect, t, s, bridge);
                        }
                        if (Object.keys(s.errors).length === 0) {
                            const { subAccountId } = t;
                            const { subAccounts } = account;
                            const inferSubAccount = () => {
                                (0, invariant_1.default)(subAccounts, "sub accounts available");
                                const a = subAccounts.find(a => a.id === subAccountId);
                                (0, invariant_1.default)(a, "sub account not found");
                                return a;
                            };
                            const obj = subAccountId
                                ? {
                                    transaction: t,
                                    account: inferSubAccount(),
                                    parentAccount: account,
                                }
                                : {
                                    transaction: t,
                                    account: account,
                                };
                            if ((typeof t.mode !== "string" || t.mode === "send") &&
                                t.model &&
                                t.model.kind !== "stake.createAccount") {
                                const estimation = await bridge.estimateMaxSpendable(obj);
                                expect(estimation.gte(0)).toBe(true);
                                expect(estimation.lte(obj.account.balance)).toBe(true);
                                if (t.useAllAmount) {
                                    expect(estimation.toString()).toBe(s.amount.toString());
                                }
                            }
                        }
                        if (apdus && impl !== "mock") {
                            const deviceId = await (0, mockDevice_1.mockDeviceWithAPDUs)(apdus);
                            try {
                                const signedOperation = await (0, rxjs_1.firstValueFrom)(bridge
                                    .signOperation({
                                    account,
                                    deviceId,
                                    transaction: t,
                                })
                                    .pipe((0, operators_1.filter)(e => e.type === "signed"), (0, operators_1.map)((e) => e.signedOperation)));
                                if (testSignedOperation) {
                                    await testSignedOperation(expect, signedOperation, account, t, s, bridge);
                                }
                            }
                            finally {
                                (0, mockDevice_1.releaseMockDevice)(deviceId);
                            }
                        }
                    });
                });
            });
            describe("signOperation and broadcast", () => {
                makeTest("method is available on bridge", async () => {
                    expect(typeof bridge.signOperation).toBe("function");
                    expect(typeof bridge.broadcast).toBe("function");
                }); // NB for now we are not going farther because most is covered by bash tests
            });
        });
    });
}
exports.testBridge = testBridge;
//# sourceMappingURL=bridge.js.map