@ledgerhq/coin-tron
Version:
Ledger Tron Coin integration
223 lines (195 loc) • 6.78 kB
text/typescript
import type {
Block,
BlockInfo,
BlockOperation,
BlockTransaction,
} from "@ledgerhq/coin-module-framework/api/index";
import { log } from "@ledgerhq/logs";
import BigNumber from "bignumber.js";
import {
getBlock as networkGetBlock,
getBlockWithTransactions,
getTransactionInfoByBlockNum,
} from "../network";
import { encode58Check } from "../network/format";
import { inferAssetInfo } from "../network/trongrid/trongrid-adapters";
import type { BlockTransactionAPI, TransactionInfoByBlockNumAPI } from "../network/types";
import { abiDecodeTrc20Transfer } from "../network/utils";
import type { TrongridTxInfo, TrongridTxType } from "../types";
type BlockTxInfo = TrongridTxInfo;
export async function getBlockInfo(height: number): Promise<BlockInfo> {
if (!Number.isSafeInteger(height) || height <= 0) {
throw new Error(`Invalid block height: ${height}`);
}
const block = await networkGetBlock(height);
return {
height: block.height,
hash: block.hash,
time: block.time ?? new Date(0),
};
}
export async function getBlock(height: number): Promise<Block> {
if (!Number.isSafeInteger(height) || height <= 0) {
throw new Error(`Invalid block height: ${height}`);
}
const [data, txInfos] = await Promise.all([
getBlockWithTransactions(height),
getTransactionInfoByBlockNum(height).catch(error => {
log("tron/getBlock", "Failed to fetch transaction info, falling back to ret fees", {
height,
error,
});
return [];
}),
]);
const header = data.block_header.raw_data;
const blockTimestamp = header.timestamp ?? 0;
const info: BlockInfo = {
height: header.number ?? height,
hash: data.blockID,
time: blockTimestamp ? new Date(blockTimestamp) : new Date(0),
};
if (header.parentHash && info.height > 1) {
info.parent = { height: info.height - 1, hash: header.parentHash };
}
const rawTxs = data.transactions ?? [];
const txInfoById = buildTxInfoMap(txInfos);
const transactions: BlockTransaction[] = rawTxs
.map(tx => toBlockTransaction(tx, blockTimestamp, info.height, txInfoById))
.filter((tx): tx is BlockTransaction => tx !== null);
return { info, transactions };
}
function buildTxInfoMap(
txInfos: TransactionInfoByBlockNumAPI[],
): Map<string, TransactionInfoByBlockNumAPI> {
return new Map(txInfos.map(tx => [tx.id, tx]));
}
function toBlockTransaction(
tx: BlockTransactionAPI,
blockTimestamp: number,
blockHeight: number,
txInfoById: Map<string, TransactionInfoByBlockNumAPI>,
): BlockTransaction | null {
const txInfo = formatBlockTransaction(tx, blockTimestamp, blockHeight);
if (!txInfo) return null;
const txDetail = txInfoById.get(tx.txID);
const fee = txDetail?.fee ?? tx.ret?.[0]?.fee ?? 0;
return {
hash: txInfo.txID,
failed: txInfo.hasFailed,
fees: BigInt(fee),
feesPayer: txInfo.from,
operations: txInfo.hasFailed ? [] : toBlockOperations(txInfo),
};
}
function formatBlockTransaction(
tx: BlockTransactionAPI,
blockTimestamp: number,
blockHeight: number,
): BlockTxInfo | null {
try {
const contract = tx.raw_data.contract[0];
if (!contract) return null;
const type = contract.type as TrongridTxType;
const params = contract.parameter.value;
const ownerAddress = params.owner_address;
if (!ownerAddress) return null;
const from = encode58Check(ownerAddress);
const contractRet = tx.ret?.[0]?.contractRet ?? "SUCCESS";
const hasFailed = contractRet !== "SUCCESS";
const isTrc20 = type === "TriggerSmartContract" && params.contract_address;
const isTrc10 = type === "TransferAssetContract";
const tokenType = isTrc10 ? "trc10" : isTrc20 ? "trc20" : undefined;
let to: string | undefined;
let value: BigNumber;
if (isTrc20 && params.data) {
const decoded = abiDecodeTrc20Transfer(params.data);
if (decoded) {
to = encode58Check(decoded.to);
value = decoded.amount;
} else {
value = new BigNumber(0);
}
} else {
to = params.to_address ? encode58Check(params.to_address) : undefined;
value = params.amount ? new BigNumber(params.amount) : new BigNumber(0);
}
const tokenId = isTrc10
? decodeHexAssetName(params.asset_name)
: isTrc20 && params.contract_address
? encode58Check(params.contract_address)
: undefined;
return {
txID: tx.txID,
date: new Date(blockTimestamp),
type,
tokenId,
tokenType,
tokenAddress:
isTrc20 && params.contract_address ? encode58Check(params.contract_address) : undefined,
from,
to,
value,
blockHeight,
hasFailed,
};
} catch (error) {
log("tron/getBlock", "formatBlockTransaction error", {
txId: tx.txID,
error,
});
return null;
}
}
function toBlockOperations(txInfo: BlockTxInfo): BlockOperation[] {
if (isTransfer(txInfo) && txInfo.to && txInfo.value && !txInfo.value.isZero()) {
const asset = inferAssetInfo(txInfo);
const value = txInfo.value;
if (value.isNaN() || !value.isFinite()) {
return [{ type: "other", operationType: "NONE", contractType: txInfo.type }];
}
const amount = BigInt(value.integerValue().toFixed(0));
return [
{ type: "transfer", address: txInfo.from, peer: txInfo.to, asset, amount: -amount },
{ type: "transfer", address: txInfo.to, peer: txInfo.from, asset, amount },
];
}
const operationType = getOperationType(txInfo.type);
return [{ type: "other", operationType: operationType, contractType: txInfo.type }];
}
function isTransfer(txInfo: TrongridTxInfo): boolean {
return (
txInfo.type === "TransferContract" ||
txInfo.type === "TransferAssetContract" ||
(txInfo.type === "TriggerSmartContract" && txInfo.tokenType === "trc20")
);
}
function getOperationType(contractType: string): string {
switch (contractType) {
case "ContractApproval":
return "APPROVE";
case "ExchangeTransactionContract":
return "OUT";
case "VoteWitnessContract":
return "VOTE";
case "WithdrawBalanceContract":
return "REWARD";
case "FreezeBalanceContract":
case "FreezeBalanceV2Contract":
return "FREEZE";
case "UnfreezeBalanceV2Contract":
return "UNFREEZE";
case "WithdrawExpireUnfreezeContract":
return "WITHDRAW_EXPIRE_UNFREEZE";
case "UnDelegateResourceContract":
return "UNDELEGATE_RESOURCE";
case "UnfreezeBalanceContract":
return "LEGACY_UNFREEZE";
default:
return "NONE";
}
}
function decodeHexAssetName(hexAssetName: string | undefined): string | undefined {
if (!hexAssetName) return undefined;
return Buffer.from(hexAssetName, "hex").toString("utf8");
}