UNPKG

@ledgerhq/live-common

Version:
605 lines • 33.8 kB
"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", "tezos"], 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); if (!(account.spendableBalance.lt(0) && estimation.eq(0))) { 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