@ledgerhq/coin-tron
Version:
Ledger Tron Coin integration
247 lines (221 loc) • 8.79 kB
text/typescript
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,
});