@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
136 lines (118 loc) • 4.24 kB
text/typescript
import type {
AssetInfo,
Block,
BlockOperation,
BlockTransaction,
} from "@ledgerhq/coin-module-framework/api/types";
import { FINALITY_MS, HEDERA_TRANSACTION_NAMES } from "../constants";
import { apiClient } from "../network/api";
import type {
HederaMirrorCoinTransfer,
HederaMirrorTokenTransfer,
HederaMirrorTransaction,
} from "../types";
import { getBlockInfo } from "./getBlockInfo";
import {
extractFeesPayer,
getMemoFromBase64,
analyzeStakingOperation,
getDateRangeFromBlockHeight,
} from "./utils";
function toHederaAsset(
mirrorTransfer: HederaMirrorCoinTransfer | HederaMirrorTokenTransfer,
): AssetInfo {
if ("token_id" in mirrorTransfer) {
return {
type: "hts",
assetReference: mirrorTransfer.token_id,
};
}
return { type: "native" };
}
function toBlockOperation(
payerAccount: string,
chargedFee: number,
mirrorTransfer: HederaMirrorCoinTransfer | HederaMirrorTokenTransfer,
): BlockOperation {
const isTokenTransfer = "token_id" in mirrorTransfer;
const address = mirrorTransfer.account;
const asset = toHederaAsset(mirrorTransfer);
let amount = BigInt(mirrorTransfer.amount);
// exclude fee from payer's operation amount (fees are accounted for separately, so operations must not represent fees)
if (payerAccount === address && !isTokenTransfer) {
amount += BigInt(chargedFee);
}
return {
type: "transfer",
address,
asset,
amount,
};
}
function createStakingRewardOperations(tx: HederaMirrorTransaction): BlockOperation[] {
return tx.staking_reward_transfers.map(rewardTransfer => ({
type: "transfer",
address: rewardTransfer.account,
asset: { type: "native" },
amount: BigInt(rewardTransfer.amount),
}));
}
export async function getBlock(height: number): Promise<Block> {
const { start, end } = getDateRangeFromBlockHeight(height);
// block data should be immutable: do not allow querying blocks on non-finalized time range
if (end.getTime() > new Date().getTime() - FINALITY_MS)
throw new Error(`Block ${height} is not available yet`);
const blockInfo = await getBlockInfo(height);
const transactions = await apiClient.getTransactionsByTimestampRange({
startTimestamp: `gte:${start.getTime() / 1000}`,
endTimestamp: `lt:${end.getTime() / 1000}`,
});
// 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(
transactions
.filter(tx => tx.name === HEDERA_TRANSACTION_NAMES.UpdateAccount)
.map(async tx => {
const payerAccount = extractFeesPayer(tx);
const analysis = await analyzeStakingOperation(payerAccount, tx);
return [tx.transaction_hash, analysis] as const;
}),
);
const stakingAnalysisMap = new Map(stakingAnalyses);
const blockTransactions: BlockTransaction[] = transactions.map(tx => {
const payerAccount = extractFeesPayer(tx);
const stakingAnalysis = stakingAnalysisMap.get(tx.transaction_hash);
let operations: BlockOperation[];
if (stakingAnalysis) {
operations = [
{
type: "other",
operationType: stakingAnalysis.operationType,
stakedNodeId: stakingAnalysis.targetStakingNodeId,
previousStakedNodeId: stakingAnalysis.previousStakingNodeId,
stakedAmount: stakingAnalysis.stakedAmount,
},
];
} else {
const allTransfers = [...tx.transfers, ...tx.token_transfers];
operations = allTransfers.map(transfer =>
toBlockOperation(payerAccount, tx.charged_tx_fee, transfer),
);
}
// add staking reward operations if present (can occur on any transaction type)
const rewardOperations = createStakingRewardOperations(tx);
operations.push(...rewardOperations);
return {
hash: tx.transaction_hash,
failed: tx.result !== "SUCCESS",
operations,
fees: BigInt(tx.charged_tx_fee),
feesPayer: payerAccount,
details: { memo: getMemoFromBase64(tx.memo_base64) },
};
});
return {
info: blockInfo,
transactions: blockTransactions,
};
}