UNPKG

@ledgerhq/coin-hedera

Version:
190 lines 7.79 kB
import { FINALITY_MS, HEDERA_TRANSACTION_NAMES } from "../constants"; import { apiClient } from "../network/api"; import { hgraphClient } from "../network/hgraph"; import { enrichERC20Transfers } from "../network/utils"; import { getBlockInfo } from "./getBlockInfo"; import { getMemoFromBase64, analyzeStakingOperation, getDateRangeFromBlockHeight, mergeTransactionsFromDifferentSources, toEntityId, extractFeesPayer, millisToSeconds, secondsToNanos, } from "./utils"; function isStakingTransactionType(item) { return item.type === "mirror" && item.data.name === HEDERA_TRANSACTION_NAMES.UpdateAccount; } function getMirrorTransaction(item) { return item.type === "mirror" ? item.data : item.data.mirrorTransaction; } function createBlockOperationFromCoinTransfer({ payerAccount, chargedFee, transfer, rewardTransfers, }) { const address = transfer.account; const reward = rewardTransfers.find(r => r.account === address); const asset = { type: "native", }; // adjust the transfer amount: // - exclude fee from payer's operation amount (fees are accounted for separately, so operations must not represent fees) // - subtract staking rewards from the amount as they are represented as separate operations const feeAdjustment = payerAccount === address ? BigInt(chargedFee) : BigInt(0); const rewardAdjustment = BigInt(reward?.amount ?? 0); const amount = BigInt(transfer.amount) + feeAdjustment - rewardAdjustment; return { type: "transfer", address, asset, amount, }; } function createBlockOperationFromHTSTokenTransfer({ transfer, }) { const amount = BigInt(transfer.amount); const address = transfer.account; const asset = { type: "hts", assetReference: transfer.token_id, }; return { type: "transfer", address, asset, amount, }; } function createBlockOperationFromERC20TokenTransfer({ transfer, }) { const amount = BigInt(transfer.amount); const recipient = transfer.receiver_account_id ? toEntityId({ num: transfer.receiver_account_id }) : transfer.receiver_evm_address; const sender = transfer.sender_account_id ? toEntityId({ num: transfer.sender_account_id }) : transfer.sender_evm_address; const asset = { type: "erc20", assetReference: transfer.token_evm_address, }; // if we don't have either sender or recipient info, we cannot create a meaningful operation, so we skip it if (!sender || !recipient) { return []; } return [ { type: "transfer", address: recipient, asset, amount, }, { type: "transfer", address: sender, asset, amount: -amount, }, ]; } function createStakingRewardOperations(tx) { return tx.staking_reward_transfers.map(rewardTransfer => ({ type: "transfer", address: rewardTransfer.account, asset: { type: "native" }, amount: BigInt(rewardTransfer.amount), })); } export async function getBlockV2(height) { const { start, end } = getDateRangeFromBlockHeight(height); // block data should be immutable: do not allow querying blocks on non-finalized time range if (end.getTime() > Date.now() - FINALITY_MS) { throw new Error(`Block ${height} is not available yet`); } const latestHgraphIndexedTimestampNs = await hgraphClient.getLatestIndexedConsensusTimestamp(); const startSeconds = millisToSeconds(start.getTime()); const endSeconds = millisToSeconds(end.getTime()); const endNanos = secondsToNanos(endSeconds); const limit = 100; const order = "desc"; // do not allow querying blocks if hgraph is not fully synced up to the end of the block time range if (latestHgraphIndexedTimestampNs.lt(endNanos)) { throw new Error(`Block ${height} has no ERC20 synced yet (${latestHgraphIndexedTimestampNs})`); } const [blockInfo, mirrorTransactions, enrichedERC20Transfers] = await Promise.all([ getBlockInfo(height), apiClient.getTransactionsByTimestampRange({ startTimestamp: `gte:${startSeconds}`, endTimestamp: `lt:${endSeconds}`, limit, order, }), hgraphClient .getERC20TransfersByTimestampRange({ startTimestamp: startSeconds.toFixed(9), endTimestamp: endSeconds.toFixed(9), limit, order, }) .then(erc20Transfers => enrichERC20Transfers(erc20Transfers)), ]); const mergeResult = mergeTransactionsFromDifferentSources({ mirrorTransactions, enrichedERC20Transfers, order, limit, latestHgraphIndexedTimestampNs, fetchAllPages: true, }); // analyze CRYPTOUPDATEACCOUNT transactions to distinguish staking operations from regular account updates. // this creates a map of transaction_hash -> StakingAnalysis to avoid repeated lookups. const stakingAnalyses = await Promise.all(mergeResult.merged.filter(isStakingTransactionType).map(async (item) => { const payerAccount = extractFeesPayer(item.data); const analysis = await analyzeStakingOperation(payerAccount, item.data); return [item.data.transaction_hash, analysis]; })); const stakingAnalysisMap = new Map(stakingAnalyses); const blockTransactions = mergeResult.merged.map(item => { const mirrorTx = getMirrorTransaction(item); const payerAccount = extractFeesPayer(mirrorTx); const stakingAnalysis = stakingAnalysisMap.get(mirrorTx.transaction_hash); let operations; if (stakingAnalysis) { operations = [ { type: "other", operationType: stakingAnalysis.operationType, stakedNodeId: stakingAnalysis.targetStakingNodeId, previousStakedNodeId: stakingAnalysis.previousStakingNodeId, stakedAmount: stakingAnalysis.stakedAmount, }, ]; } else { const allTransfers = [ ...mirrorTx.transfers, ...mirrorTx.token_transfers, ...(item.type === "erc20" ? item.data.transfers : []), ]; operations = allTransfers.flatMap(transfer => { if ("token_evm_address" in transfer) { return createBlockOperationFromERC20TokenTransfer({ transfer }); } else if ("token_id" in transfer) { return createBlockOperationFromHTSTokenTransfer({ transfer }); } else { return createBlockOperationFromCoinTransfer({ payerAccount, transfer, chargedFee: mirrorTx.charged_tx_fee, rewardTransfers: mirrorTx.staking_reward_transfers, }); } }); } // add staking reward operations if present (can occur on any transaction type) const rewardOperations = createStakingRewardOperations(mirrorTx); operations.push(...rewardOperations); return { hash: mirrorTx.transaction_hash, failed: mirrorTx.result !== "SUCCESS", operations, fees: BigInt(mirrorTx.charged_tx_fee), feesPayer: payerAccount, details: { memo: getMemoFromBase64(mirrorTx.memo_base64) }, }; }); return { info: blockInfo, transactions: blockTransactions, }; } //# sourceMappingURL=getBlock.v2.js.map