@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
731 lines (728 loc) • 35.3 kB
JavaScript
;
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