UNPKG

@ledgerhq/coin-hedera

Version:
377 lines 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.listOperationsV2 = listOperationsV2; const state_1 = require("@ledgerhq/cryptoassets/state"); const accountId_1 = require("@ledgerhq/ledger-wallet-framework/account/accountId"); const operation_1 = require("@ledgerhq/ledger-wallet-framework/operation"); const live_env_1 = require("@ledgerhq/live-env"); const bignumber_js_1 = __importDefault(require("bignumber.js")); const constants_1 = require("../constants"); const api_1 = require("../network/api"); const hgraph_1 = require("../network/hgraph"); const utils_1 = require("../network/utils"); const utils_2 = require("./utils"); const txNameToCustomOperationType = { TOKENASSOCIATE: "ASSOCIATE_TOKEN", CONTRACTCALL: "CONTRACT_CALL", CRYPTOUPDATEACCOUNT: "UPDATE_ACCOUNT", }; function getCommonMirrorOperationData(rawTx, useEncodedHash, useSyntheticBlocks) { const date = new Date(Number.parseInt(rawTx.consensus_timestamp.split(".")[0], 10) * 1000); const hash = useEncodedHash ? (0, utils_2.base64ToUrlSafeBase64)(rawTx.transaction_hash) : rawTx.transaction_hash; const fee = new bignumber_js_1.default(rawTx.charged_tx_fee); const hasFailed = rawTx.result !== "SUCCESS"; const syntheticBlock = (0, utils_2.getSyntheticBlock)(rawTx.consensus_timestamp); const memo = (0, utils_2.getMemoFromBase64)(rawTx.memo_base64); const feesPayer = (0, utils_2.extractFeesPayer)(rawTx); const extra = { pagingToken: rawTx.consensus_timestamp, consensusTimestamp: rawTx.consensus_timestamp, transactionId: rawTx.transaction_id, feesPayer, ...(memo && { memo }), }; return { date, hash, fee, hasFailed, blockHeight: useSyntheticBlocks ? syntheticBlock.blockHeight : constants_1.HARDCODED_BLOCK_HEIGHT, blockHash: useSyntheticBlocks ? syntheticBlock.blockHash : null, extra, }; } function calculateStakingReward(rawTx, address) { return rawTx.staking_reward_transfers.reduce((acc, transfer) => { const transferAmount = new bignumber_js_1.default(transfer.amount); return transfer.account === address ? acc.plus(transferAmount) : acc; }, new bignumber_js_1.default(0)); } function createStakingRewardOperation({ stakingReward, address, ledgerAccountId, commonData, }) { if (stakingReward.lte(0)) { return null; } const { hash, date, blockHeight, blockHash } = commonData; const stakingRewardHash = (0, utils_2.createStakingRewardOperationHash)(hash); const stakingRewardType = "REWARD"; // offset timestamp by +1ms so that, when operations are sorted newest-first, this reward appears just before the operation that triggered it const stakingRewardTimestamp = new Date(date.getTime() + 1); return { id: (0, operation_1.encodeOperationId)(ledgerAccountId, stakingRewardHash, stakingRewardType), accountId: ledgerAccountId, type: stakingRewardType, value: stakingReward, recipients: [address], senders: [(0, live_env_1.getEnv)("HEDERA_STAKING_REWARD_ACCOUNT_ID")], hash: stakingRewardHash, fee: new bignumber_js_1.default(0), date: stakingRewardTimestamp, blockHeight, blockHash, extra: commonData.extra, }; } function getOperationTypeFromERC20Details({ transferType, senderEvmAddress, evmAddress, }) { if (transferType === "mint") return "IN"; if (transferType === "burn") return "OUT"; return senderEvmAddress.toLowerCase() === evmAddress.toLowerCase() ? "OUT" : "IN"; } async function processERC20TokenTransfer({ enrichedERC20Transfer, evmAddress, ledgerAccountId, commonData, }) { let coinOperation; const tokenOperations = []; for (const transfer of enrichedERC20Transfer.transfers) { const token = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(transfer.token_evm_address, "hedera"); if (!token) continue; const senderEvmAddress = transfer.sender_evm_address; const senderAddress = transfer.sender_account_id ? (0, utils_2.toEntityId)({ num: transfer.sender_account_id }) : transfer.sender_evm_address; const recipientAddress = transfer.receiver_account_id ? (0, utils_2.toEntityId)({ num: transfer.receiver_account_id }) : transfer.receiver_evm_address; // meaningful operation cannot be created without correct addresses, so we skip it if (!senderEvmAddress || !senderAddress || !recipientAddress) continue; const commonFields = { ...commonData, type: getOperationTypeFromERC20Details({ transferType: transfer.transfer_type, senderEvmAddress, evmAddress, }), contract: token.contractAddress, standard: "erc20", blockHeight: commonData.blockHeight, blockHash: commonData.blockHash, senders: [senderAddress], recipients: [recipientAddress], fee: new bignumber_js_1.default(enrichedERC20Transfer.mirrorTransaction.charged_tx_fee), value: new bignumber_js_1.default(transfer.amount), extra: { ...commonData.extra, gasConsumed: enrichedERC20Transfer.contractCallResult.gas_consumed, gasLimit: enrichedERC20Transfer.contractCallResult.gas_limit, gasUsed: enrichedERC20Transfer.contractCallResult.gas_used, }, }; const encodedTokenAccountId = (0, accountId_1.encodeTokenAccountId)(ledgerAccountId, token); const encodedOperationId = (0, operation_1.encodeOperationId)(encodedTokenAccountId, commonFields.hash, commonFields.type); const tokenOperation = { ...commonFields, id: encodedOperationId, accountId: encodedTokenAccountId, }; tokenOperations.push(tokenOperation); } // create FEES operation for outgoing ERC20 transfer const outgoingTransfer = tokenOperations.find(transfer => transfer.type === "OUT"); if (outgoingTransfer) { coinOperation = { ...commonData, id: (0, operation_1.encodeOperationId)(ledgerAccountId, commonData.hash, "FEES"), accountId: ledgerAccountId, type: "FEES", ...(outgoingTransfer.contract && { contract: outgoingTransfer.contract }), ...(outgoingTransfer.standard && { standard: outgoingTransfer.standard }), blockHeight: outgoingTransfer.blockHeight, blockHash: outgoingTransfer.blockHash, senders: outgoingTransfer.senders, recipients: outgoingTransfer.recipients, fee: outgoingTransfer.fee, value: outgoingTransfer.fee, extra: outgoingTransfer.extra, }; } return { coinOperation, tokenOperations, }; } async function processHTSTokenTransfers({ rawTx, address, currency, ledgerAccountId, commonData, }) { const tokenTransfers = rawTx.token_transfers ?? []; if (tokenTransfers.length === 0) return null; const tokenId = tokenTransfers[0].token_id; const token = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tokenId, currency.id); if (!token) return null; const encodedTokenId = (0, accountId_1.encodeTokenAccountId)(ledgerAccountId, token); const { type, value, senders, recipients } = (0, utils_1.parseTransfers)(tokenTransfers, address); const { hash, fee, date, blockHeight, blockHash, hasFailed } = commonData; const extra = { ...commonData.extra }; let coinOperation; // Add main FEES coin operation for send token transfer if (type === "OUT") { coinOperation = { id: (0, operation_1.encodeOperationId)(ledgerAccountId, hash, "FEES"), accountId: ledgerAccountId, type: "FEES", value: fee, recipients, senders, hash, fee, date, blockHeight, blockHash, hasFailed, extra, }; } const tokenOperation = { id: (0, operation_1.encodeOperationId)(encodedTokenId, hash, type), accountId: encodedTokenId, contract: token.contractAddress, standard: "hts", type, value, recipients, senders, hash, fee, date, blockHeight, blockHash, hasFailed, extra, }; return { coinOperation, tokenOperation, }; } function processCoinTransfers({ rawTx, address, ledgerAccountId, commonData, mirrorTokens, stakingReward, stakingAnalysis, }) { const coinOperations = []; const transfers = rawTx.transfers ?? []; if (transfers.length === 0) { return []; } const { type, value, senders, recipients } = (0, utils_1.parseTransfers)(transfers, address, stakingReward); const { hash, fee, date, 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_js_1.default(stakingAnalysis.stakedAmount.toString()); } // if recipients array is empty, add the node where the transaction was submitted as recipient if (recipients.length === 0 && rawTx.node) { recipients.push(rawTx.node); } // 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; } } coinOperations.push({ id: (0, operation_1.encodeOperationId)(ledgerAccountId, hash, operationType), accountId: ledgerAccountId, type: operationType, value, recipients, senders, hash, fee, date, blockHeight, blockHash, hasFailed, extra, }); return coinOperations; } async function processTransactionItem({ mergedTx, address, evmAddress, currency, ledgerAccountId, mirrorTokens, useEncodedHash, useSyntheticBlocks, }) { const newCoinOperations = []; const newTokenOperations = []; const mirrorTx = mergedTx.type === "mirror" ? mergedTx.data : mergedTx.data.mirrorTransaction; const commonData = getCommonMirrorOperationData(mirrorTx, useEncodedHash, useSyntheticBlocks); const stakingReward = calculateStakingReward(mirrorTx, address); const rewardOp = createStakingRewardOperation({ stakingReward, address, ledgerAccountId, commonData, }); if (rewardOp) newCoinOperations.push(rewardOp); const stakingAnalysis = mirrorTx.name === constants_1.HEDERA_TRANSACTION_NAMES.UpdateAccount ? await (0, utils_2.analyzeStakingOperation)(address, mirrorTx) : null; if (mergedTx.type === "mirror") { const htsTokenResult = await processHTSTokenTransfers({ rawTx: mirrorTx, address, currency, ledgerAccountId, commonData, }); if (htsTokenResult?.tokenOperation) newTokenOperations.push(htsTokenResult.tokenOperation); if (htsTokenResult?.coinOperation) newCoinOperations.push(htsTokenResult.coinOperation); if (!htsTokenResult) { const coinOps = processCoinTransfers({ rawTx: mirrorTx, address, ledgerAccountId, commonData, mirrorTokens, stakingReward, stakingAnalysis, }); newCoinOperations.push(...coinOps); } } else { const erc20TokenResult = await processERC20TokenTransfer({ enrichedERC20Transfer: mergedTx.data, evmAddress, ledgerAccountId, commonData, }); if (erc20TokenResult.coinOperation) newCoinOperations.push(erc20TokenResult.coinOperation); newTokenOperations.push(...erc20TokenResult.tokenOperations); } return { newCoinOperations, newTokenOperations }; } async function listOperationsV2({ currency, address, evmAddress, mirrorTokens, erc20Tokens, cursor, limit = 100, order = "desc", fetchAllPages, skipFeesForTokenOperations, useEncodedHash, useSyntheticBlocks, }) { const coinOperations = []; const tokenOperations = []; const ledgerAccountId = (0, accountId_1.encodeAccountId)({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: address, derivationMode: "hederaBip44", }); // fetch transactions from both sources in parallel const [mirrorTransactions, enrichedERC20Transfers, latestHgraphIndexedTimestampNs] = await Promise.all([ api_1.apiClient.getAccountTransactions({ address, order, limit, fetchAllPages, pagingToken: cursor ?? null, }), hgraph_1.hgraphClient .getERC20Transfers({ address, order, limit, fetchAllPages, tokenEvmAddresses: erc20Tokens.map(t => t.token.contractAddress.toLowerCase()), ...(cursor && { timestamp: cursor }), }) .then(erc20Transfers => (0, utils_1.enrichERC20Transfers)(erc20Transfers)), hgraph_1.hgraphClient.getLatestIndexedConsensusTimestamp(), ]); // merge transactions, ensuring no duplicates, correct ordering and pagination handling const mergeResult = (0, utils_2.mergeTransactionsFromDifferentSources)({ mirrorTransactions: mirrorTransactions.transactions, enrichedERC20Transfers, order, limit, latestHgraphIndexedTimestampNs, fetchAllPages, }); for (const mergedTx of mergeResult.merged) { const result = await processTransactionItem({ mergedTx, address, evmAddress, currency, ledgerAccountId, mirrorTokens, useEncodedHash, useSyntheticBlocks, }); coinOperations.push(...result.newCoinOperations); tokenOperations.push(...result.newTokenOperations); } return { tokenOperations, coinOperations: skipFeesForTokenOperations ? coinOperations.filter(op => op.type !== "FEES") : coinOperations, nextCursor: mergeResult.nextCursor, }; } //# sourceMappingURL=listOperations.v2.js.map