UNPKG

@ledgerhq/coin-tron

Version:
247 lines (221 loc) 8.79 kB
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; import { emptyHistoryCache, encodeAccountId, encodeTokenAccountId, } from "@ledgerhq/ledger-wallet-framework/account"; import { GetAccountShape, makeSync } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers"; import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation"; import { Account, TokenAccount } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import compact from "lodash/compact"; import get from "lodash/get"; import { computeBalanceBridge, lastBlock } from "../logic"; import { getAccount } from "../logic/getAccount"; import { getOperationsPageSize } from "../logic/pagination"; import { defaultFetchParams, fetchTronAccountTxs } from "../network"; import { AccountTronAPI } from "../network/types"; import { TronAccount, TrongridExtraTxInfo, TronOperation } from "../types"; import { defaultTronResources, getTronResources, isParentTx, txInfoToOperation, isAccountEmpty, } from "./utils"; type TronToken = { key: string; type: "trc10" | "trc20"; tokenId: string; balance: string; }; // 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; const MAX_OPERATIONS_PAGE_SIZE = 1000; async function getTrc10Tokens(acc: AccountTronAPI): Promise<TronToken[]> { const trc10Tokens: TronToken[] = []; for (const { key, value } of get(acc, "assetV2", []) as { key: string; value: number }[]) { const tokenInfo = await getCryptoAssetsStore().findTokenById(`tron/trc10/${key}`); if (tokenInfo) { trc10Tokens.push({ key, type: "trc10", tokenId: tokenInfo.id, balance: value.toString(), }); } } return trc10Tokens; } async function getTrc20Tokens(acc: AccountTronAPI, currencyId: string): Promise<TronToken[]> { const trc20Tokens: TronToken[] = []; for (const trc20 of get(acc, "trc20", []) as Record<string, string>[]) { const [[contractAddress, balance]] = Object.entries(trc20); const tokenInfo = await getCryptoAssetsStore().findTokenByAddressInCurrency( contractAddress, currencyId, ); if (tokenInfo) { trc20Tokens.push({ key: contractAddress, type: "trc20", tokenId: tokenInfo.id, balance, }); } } return trc20Tokens; } export const getAccountShape: GetAccountShape<TronAccount> = async ( { initialAccount, currency, address, derivationMode }, syncConfig, ) => { const { height: blockHeight } = await lastBlock(); const tronAcc = await getAccount(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 operationsPageSize = Math.min( MAX_OPERATIONS_PAGE_SIZE, getOperationsPageSize(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, defaultFetchParams, ); const tronResources = await getTronResources(acc, txs); const spendableBalance = acc.balance ? new BigNumber(acc.balance) : new BigNumber(0); const balance = computeBalanceBridge(acc); const parentTxs = txs.filter(isParentTx); const parentOperations: TronOperation[] = compact( parentTxs.map(tx => txInfoToOperation(accountId, address, tx)), ); const trc10Tokens = await getTrc10Tokens(acc); const trc20Tokens = await getTrc20Tokens(acc, currency.id); const { blacklistedTokenIds = [] } = syncConfig; const subAccounts: TokenAccount[] = []; for (const { key, tokenId, balance } of trc10Tokens.concat(trc20Tokens)) { const token = await getCryptoAssetsStore().findTokenById(tokenId); if (!token || blacklistedTokenIds.includes(tokenId)) continue; 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: TokenAccount = { 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 }; subAccounts.push(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: TronOperation[] = subAccounts .flatMap(s => s.operations) .filter(o => o.type === "OUT" && o.fee.isGreaterThan(0)) .map( (o): TronOperation => ({ ...o, accountId, value: o.fee, id: encodeOperationId(accountId, o.hash, "OUT"), extra: o.extra as TrongridExtraTxInfo, }), ); // 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, used: !isAccountEmpty({ tronResources }), }; }; export const postSync = (_initial: TronAccount, parent: TronAccount): TronAccount => { function evictRecentOpsIfPending(a: Account | TokenAccount) { 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: TokenAccount[], subAccounts2: TokenAccount[]) => { const existingIds = new Set(subAccounts1.map(subAccount => subAccount.id)); const filteredSubAccounts2: TokenAccount[] = subAccounts2 .filter(subAccount => !existingIds.has(subAccount.id)) .map(subAccount => ({ ...subAccount, balance: new BigNumber(0), spendableBalance: new BigNumber(0), })); return subAccounts1.concat(filteredSubAccounts2); }; export const sync = makeSync({ getAccountShape, postSync, });