UNPKG

@ledgerhq/coin-hedera

Version:
224 lines 8.85 kB
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; import { encodeAccountId, encodeTokenAccountId, } from "@ledgerhq/ledger-wallet-framework/account/accountId"; import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation"; import { getEnv } from "@ledgerhq/live-env"; import BigNumber from "bignumber.js"; import { HEDERA_TRANSACTION_NAMES } from "../constants"; import { apiClient } from "../network/api"; import { parseTransfers } from "../network/utils"; import { analyzeStakingOperation, base64ToUrlSafeBase64, createStakingRewardOperationHash, extractFeesPayer, getMemoFromBase64, getSyntheticBlock, } from "./utils"; const txNameToCustomOperationType = { TOKENASSOCIATE: "ASSOCIATE_TOKEN", CONTRACTCALL: "CONTRACT_CALL", CRYPTOUPDATEACCOUNT: "UPDATE_ACCOUNT", }; function getCommonOperationData(rawTx, useEncodedHash, useSyntheticBlocks) { const timestamp = new Date(Number.parseInt(rawTx.consensus_timestamp.split(".")[0], 10) * 1000); const hash = useEncodedHash ? base64ToUrlSafeBase64(rawTx.transaction_hash) : rawTx.transaction_hash; const fee = new BigNumber(rawTx.charged_tx_fee); const hasFailed = rawTx.result !== "SUCCESS"; const syntheticBlock = getSyntheticBlock(rawTx.consensus_timestamp); const memo = getMemoFromBase64(rawTx.memo_base64); const feesPayer = extractFeesPayer(rawTx); const extra = { pagingToken: rawTx.consensus_timestamp, consensusTimestamp: rawTx.consensus_timestamp, transactionId: rawTx.transaction_id, feesPayer, ...(memo && { memo }), }; return { timestamp, hash, fee, hasFailed, blockHeight: useSyntheticBlocks ? syntheticBlock.blockHeight : 10, blockHash: useSyntheticBlocks ? syntheticBlock.blockHash : null, extra, }; } async function processTokenTransfers({ rawTx, address, currency, ledgerAccountId, commonData, skipFeesForTokenOperations, }) { const tokenTransfers = rawTx.token_transfers ?? []; if (tokenTransfers.length === 0) return null; const tokenId = tokenTransfers[0].token_id; const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(tokenId, currency.id); if (!token) return null; const encodedTokenId = encodeTokenAccountId(ledgerAccountId, token); const { type, value, senders, recipients } = parseTransfers(tokenTransfers, address); const { hash, fee, timestamp, blockHeight, blockHash, hasFailed } = commonData; const extra = { ...commonData.extra }; let coinOperation; // Add main FEES coin operation for send token transfer if (type === "OUT" && !skipFeesForTokenOperations) { coinOperation = { id: encodeOperationId(ledgerAccountId, hash, "FEES"), accountId: ledgerAccountId, type: "FEES", value: fee, recipients, senders, hash, fee, date: timestamp, blockHeight, blockHash, hasFailed, extra, }; } const tokenOperation = { id: encodeOperationId(encodedTokenId, hash, type), accountId: encodedTokenId, contract: token.contractAddress, standard: "hts", type, value, recipients, senders, hash, fee, date: timestamp, blockHeight, blockHash, hasFailed, extra, }; return { coinOperation, tokenOperation, }; } function processTransfers({ rawTx, address, ledgerAccountId, commonData, mirrorTokens, stakingAnalysis, }) { const coinOperations = []; const transfers = rawTx.transfers ?? []; if (transfers.length === 0) { return []; } const { type, value, senders, recipients } = parseTransfers(transfers, address); const { hash, fee, timestamp, blockHeight, blockHash, hasFailed } = commonData; const extra = { ...commonData.extra }; let operationType = txNameToCustomOperationType[rawTx.name] ?? type; // update operation type and extra fields if staking analysis is available if (stakingAnalysis) { operationType = stakingAnalysis.operationType; extra.previousStakingNodeId = stakingAnalysis.previousStakingNodeId; extra.targetStakingNodeId = stakingAnalysis.targetStakingNodeId; extra.stakedAmount = new BigNumber(stakingAnalysis.stakedAmount.toString()); } // each transfer may trigger staking reward claim const stakingReward = rawTx.staking_reward_transfers.reduce((acc, transfer) => { const transferAmount = new BigNumber(transfer.amount); if (transfer.account === address) { acc = acc.plus(transferAmount); } return acc; }, new BigNumber(0)); // try to enrich ASSOCIATE_TOKEN operation with extra.associatedTokenId // this value is used by custom OperationDetails components in Hedera family // accounts or contracts must first associate with an HTS token before they can receive or send that token; without association, token transfers fail if (operationType === "ASSOCIATE_TOKEN") { const relatedMirrorToken = mirrorTokens.find(t => { return t.created_timestamp === rawTx.consensus_timestamp; }); if (relatedMirrorToken) { extra.associatedTokenId = relatedMirrorToken.token_id; } } // add REWARD operation representing staking reward transfers if (stakingReward.gt(0)) { const stakingRewardHash = createStakingRewardOperationHash(hash); const stakingRewardType = "REWARD"; // offset timestamp by +1ms to ensure it appears just before the operation that triggered it const stakingRewardTimestamp = new Date(timestamp.getTime() + 1); coinOperations.push({ id: encodeOperationId(ledgerAccountId, stakingRewardHash, stakingRewardType), accountId: ledgerAccountId, type: stakingRewardType, value: stakingReward, recipients: [address], senders: [getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID")], hash: stakingRewardHash, fee: new BigNumber(0), date: stakingRewardTimestamp, blockHeight, blockHash, extra, }); } coinOperations.push({ id: encodeOperationId(ledgerAccountId, hash, operationType), accountId: ledgerAccountId, type: operationType, value, recipients, senders, hash, fee, date: timestamp, blockHeight, blockHash, hasFailed, extra, }); return coinOperations; } export async function listOperations({ currency, address, mirrorTokens, cursor, limit, order, fetchAllPages, skipFeesForTokenOperations, useEncodedHash, useSyntheticBlocks, }) { const coinOperations = []; const tokenOperations = []; const mirrorResult = await apiClient.getAccountTransactions({ address, pagingToken: cursor ?? null, order: order, limit: limit, fetchAllPages, }); const ledgerAccountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: address, derivationMode: "hederaBip44", }); for (const rawTx of mirrorResult.transactions) { const commonData = getCommonOperationData(rawTx, useEncodedHash, useSyntheticBlocks); // try to distinguish staking operations for CRYPTOUPDATEACCOUNT transactions const stakingAnalysis = rawTx.name === HEDERA_TRANSACTION_NAMES.UpdateAccount ? await analyzeStakingOperation(address, rawTx) : null; // process token transfers const tokenResult = await processTokenTransfers({ rawTx, address, currency, ledgerAccountId, commonData, skipFeesForTokenOperations, }); if (tokenResult?.coinOperation) coinOperations.push(tokenResult.coinOperation); if (tokenResult?.tokenOperation) tokenOperations.push(tokenResult.tokenOperation); // process regular transfers only if there were no token transfers if (!tokenResult) { const newCoinOperations = processTransfers({ rawTx, address, ledgerAccountId, commonData, mirrorTokens, stakingAnalysis, }); coinOperations.push(...newCoinOperations); } } return { coinOperations, tokenOperations, nextCursor: mirrorResult.nextCursor, }; } //# sourceMappingURL=listOperations.js.map