UNPKG

@ledgerhq/coin-tron

Version:
188 lines 8.63 kB
import { emptyHistoryCache, encodeAccountId, encodeTokenAccountId, } from "@ledgerhq/coin-framework/account"; import { makeSync } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { findTokenByAddressInCurrency, findTokenById } from "@ledgerhq/cryptoassets/index"; import BigNumber from "bignumber.js"; import compact from "lodash/compact"; import get from "lodash/get"; import { computeBalanceBridge } from "../logic"; import { getOperationsPageSize } from "../logic/pagination"; import { getLastBlock, fetchTronAccount, fetchTronAccountTxs } from "../network"; import { defaultTronResources, getTronResources, isParentTx, txInfoToOperation } from "./utils"; // the balance does not update straightaway so we should ignore recent operations if they are in pending for a bit const PREFER_PENDING_OPERATIONS_UNTIL_BLOCK_VALIDATION = 35; export const getAccountShape = async ({ initialAccount, currency, address, derivationMode }, syncConfig) => { const { height: blockHeight } = await getLastBlock(); const tronAcc = await fetchTronAccount(address); const accountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: address, derivationMode: derivationMode, }); if (tronAcc.length === 0) { return { id: accountId, blockHeight, balance: new BigNumber(0), tronResources: defaultTronResources, }; } const acc = tronAcc[0]; const cacheTransactionInfoById = initialAccount ? { ...(initialAccount?.tronResources?.cacheTransactionInfoById || {}), } : {}; const operationsPageSize = Math.min(1000, getOperationsPageSize(initialAccount && initialAccount.id, syncConfig)); // FIXME: this is not optional especially that we might already have initialAccount // use minimalOperationsBuilderSync to reconciliate and KEEP REF const txs = await fetchTronAccountTxs(address, txs => txs.length < operationsPageSize, cacheTransactionInfoById); const tronResources = await getTronResources(acc, txs, cacheTransactionInfoById); // const tronResources = await getTronResources(acc); const spendableBalance = acc.balance ? new BigNumber(acc.balance) : new BigNumber(0); const balance = computeBalanceBridge(acc); const parentTxs = txs.filter(isParentTx); const parentOperations = compact(parentTxs.map(tx => txInfoToOperation(accountId, address, tx))); const trc10Tokens = get(acc, "assetV2", []).reduce((accumulator, { key, value }) => { const tokenInfo = findTokenById(`tron/trc10/${key}`); if (tokenInfo) { accumulator.push({ key, type: "trc10", tokenId: tokenInfo.id, balance: value.toString(), }); } return accumulator; }, []); const trc20Tokens = get(acc, "trc20", []).reduce((accumulator, trc20) => { const [[contractAddress, balance]] = Object.entries(trc20); const tokenInfo = findTokenByAddressInCurrency(contractAddress, currency.id); if (tokenInfo) { accumulator.push({ key: contractAddress, type: "trc20", tokenId: tokenInfo.id, balance, }); } return accumulator; }, []); const { blacklistedTokenIds = [] } = syncConfig; const subAccounts = compact(trc10Tokens.concat(trc20Tokens).map(({ key, tokenId, balance }) => { const { blacklistedTokenIds = [] } = syncConfig; const token = findTokenById(tokenId); if (!token || blacklistedTokenIds.includes(tokenId)) return; const id = encodeTokenAccountId(accountId, token); const tokenTxs = txs.filter(tx => tx.tokenId === key); const operations = compact(tokenTxs.map(tx => txInfoToOperation(id, address, tx))); const maybeExistingSubAccount = initialAccount?.subAccounts?.find(a => a.id === id); const bnBalance = new BigNumber(balance); const sub = { type: "TokenAccount", id, parentId: accountId, token, balance: bnBalance, spendableBalance: bnBalance, operationsCount: operations.length, operations, pendingOperations: maybeExistingSubAccount ? maybeExistingSubAccount.pendingOperations : [], creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(), swapHistory: maybeExistingSubAccount ? maybeExistingSubAccount.swapHistory : [], balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers }; return sub; })); // Filter blacklisted tokens from the initial account's subAccounts // Could be use to filter out tokens that got their CAL id changed const filteredInitialSubAccounts = (initialAccount?.subAccounts || []).filter(subAccount => !blacklistedTokenIds.includes(subAccount.token.id)); // keep old account with emptyBalance and a history not returned by the BE fixes LIVE-12797 const mergedSubAccounts = mergeSubAccounts(subAccounts, filteredInitialSubAccounts); // get 'OUT' token operations with fee const subOutOperationsWithFee = subAccounts .flatMap(s => s.operations) .filter(o => o.type === "OUT" && o.fee.isGreaterThan(0)) .map((o) => ({ ...o, accountId, value: o.fee, id: encodeOperationId(accountId, o.hash, "OUT"), extra: o.extra, })); // add them to the parent operations and sort by date desc /** * FIXME * * We have a problem here as we're just concatenating ops without ever really linking them. * It means no operation can be "FEES" of a subOp by example. It leads to our issues with TRC10/TRC20 * optimistic operation never really existing in the end. */ const parentOpsAndSubOutOpsWithFee = parentOperations .concat(subOutOperationsWithFee) .sort((a, b) => b.date.valueOf() - a.date.valueOf()); return { id: accountId, balance, spendableBalance, operationsCount: parentOpsAndSubOutOpsWithFee.length, operations: parentOpsAndSubOutOpsWithFee, subAccounts: mergedSubAccounts, tronResources, blockHeight, }; }; const postSync = (initial, parent) => { function evictRecentOpsIfPending(a) { a.pendingOperations.forEach(pending => { const i = a.operations.findIndex(o => o.id === pending.id); if (i !== -1) { const diff = parent.blockHeight - (a.operations[i].blockHeight || 0); if (diff < PREFER_PENDING_OPERATIONS_UNTIL_BLOCK_VALIDATION) { a.operations.splice(i, 1); } } }); } evictRecentOpsIfPending(parent); parent.subAccounts && parent.subAccounts.forEach(evictRecentOpsIfPending); return parent; }; /** * Merges two arrays of subAccounts according to specific rules: * - The first array (subAccounts1) is up-to-date and should not be modified. * - Old duplicates from the second array (subAccounts2) should be filtered out. * - Only new subAccounts with a unique ID from the second array should be included. * - The balance and spendableBalance fields of the second array's subAccounts should be set to 0. * * @param {Array} subAccounts1 - The first array of subAccounts, which is up-to-date and should not be modified. * @param {Array} subAccounts2 - The second array of subAccounts, from which only new unique subAccounts should be included. * @returns {Array} - The merged array of subAccounts. */ const mergeSubAccounts = (subAccounts1, subAccounts2) => { const existingIds = new Set(subAccounts1.map(subAccount => subAccount.id)); const filteredSubAccounts2 = subAccounts2 .map(subAccount => { if (existingIds.has(subAccount.id)) { return null; } else { // Set balance and spendableBalance to 0 has if they are not here it means balance is 0 return { ...subAccount, balance: new BigNumber(0), spendableBalance: new BigNumber(0), }; } }) .filter((elt) => elt !== null); return subAccounts1.concat(filteredSubAccounts2); }; export const sync = makeSync({ getAccountShape, postSync, }); //# sourceMappingURL=synchronization.js.map