UNPKG

@ledgerhq/live-common

Version:
731 lines (728 loc) 35.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runWithAppSpec = runWithAppSpec; exports.runOnAccount = runOnAccount; exports.autoSignTransaction = autoSignTransaction; 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 speculos_1 = require("../load/speculos"); const formatters_1 = require("./formatters"); const cache_1 = require("../bridge/cache"); const bot_test_context_1 = require("@ledgerhq/ledger-wallet-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; } 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; } 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); }); } 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_1.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