UNPKG

@ledgerhq/coin-hedera

Version:
264 lines (212 loc) 8.06 kB
import { AccountId, TransactionId } from "@hashgraph/sdk"; import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; import { getEnv } from "@ledgerhq/live-env"; import { TokenCurrency } from "@ledgerhq/types-cryptoassets"; import type { Operation, OperationType } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import type { HederaConfig } from "../config"; import { SUPPORTED_ERC20_TOKENS } from "../constants"; import { nanosToSeconds, toEntityId, toTimestamp } from "../logic/utils"; import type { HederaMirrorTokenTransfer, HederaMirrorCoinTransfer, HederaThirdwebTransaction, HederaThirdwebDecodedTransferParams, OperationERC20, HederaERC20TokenBalance, ERC20TokenTransfer, EnrichedERC20Transfer, } from "../types"; import { apiClient } from "./api"; import { hgraphClient } from "./hgraph"; export async function createTransactionId( accountId: string, config: HederaConfig, ): Promise<TransactionId> { if (!config.useNetworkTimestamp) { return TransactionId.generate(accountId); } try { const lastBlock = await apiClient.getLatestBlock(); const validStart = toTimestamp(lastBlock.timestamp.to ?? lastBlock.timestamp.from); return TransactionId.withValidStart(AccountId.fromString(accountId), validStart); } catch { return TransactionId.generate(accountId); } } function isValidRecipient(accountId: AccountId, recipients: string[]): boolean { if (accountId.shard.eq(0) && accountId.realm.eq(0)) { // account is a node, only add to list if we have none if (accountId.num.lt(100)) { return recipients.length === 0; } // account is a system account that is not a node, do NOT add if (accountId.num.lt(1000)) { return false; } } return true; } export function parseTransfers( mirrorTransfers: (HederaMirrorCoinTransfer | HederaMirrorTokenTransfer)[], address: string, stakingReward = new BigNumber(0), ): Pick<Operation, "type" | "value" | "senders" | "recipients"> { let value = new BigNumber(0); let type: OperationType = "NONE"; const senders: string[] = []; const recipients: string[] = []; const rewardPayerAddress = getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID"); for (const transfer of mirrorTransfers) { const amount = new BigNumber(transfer.amount); const accountId = AccountId.fromString(transfer.account); // staking reward is included in transfer, so it can be positive even if user sent less HBARs than the reward is const amountWithoutReward = transfer.account === address ? amount.minus(stakingReward) : amount; if (transfer.account === address) { value = amountWithoutReward.abs(); type = amountWithoutReward.isNegative() ? "OUT" : "IN"; } if (amountWithoutReward.isNegative()) { // exclude reward payer from senders list, because rewards are shown as separate operations const shouldIgnoreAddress = transfer.account === rewardPayerAddress && stakingReward.gt(0); if (shouldIgnoreAddress) { continue; } senders.push(transfer.account); } else if (isValidRecipient(accountId, recipients)) { recipients.push(transfer.account); } } // NOTE: earlier addresses are the "fee" addresses senders.reverse(); recipients.reverse(); return { type, value, senders, recipients, }; } // TODO: remove once migration to new API is complete export async function getERC20BalancesForAccount( evmAccountId: string, supportedTokenIds = SUPPORTED_ERC20_TOKENS.map(token => token.id), ): Promise<HederaERC20TokenBalance[]> { const availableTokens: TokenCurrency[] = []; for (const erc20TokenId of supportedTokenIds) { const calToken = await getCryptoAssetsStore().findTokenById(erc20TokenId); if (calToken) { availableTokens.push(calToken); } } const promises = availableTokens.map(async erc20token => { const balance = await apiClient.getERC20Balance(evmAccountId, erc20token.contractAddress); return { balance, token: erc20token, }; }); const balances = await Promise.all(promises); return balances; } export async function getERC20BalancesForAccountV2( address: string, ): Promise<HederaERC20TokenBalance[]> { const balances: HederaERC20TokenBalance[] = []; const rawBalances = await hgraphClient.getERC20Balances({ address }); for (const rawBalance of rawBalances) { const rawBalanceTokenId = toEntityId({ num: rawBalance.token_id }); const supportedToken = SUPPORTED_ERC20_TOKENS.find(token => { return token.tokenId === rawBalanceTokenId; }); if (!supportedToken) { continue; } const calToken = await getCryptoAssetsStore().findTokenById(supportedToken.id); if (!calToken) { continue; } balances.push({ token: calToken, balance: new BigNumber(rawBalance.balance), }); } return balances; } // TODO: remove once migration to new API is complete export const getERC20Operations = async ( latestERC20Transactions: HederaThirdwebTransaction[], ): Promise<OperationERC20[]> => { const latestERC20Operations: OperationERC20[] = []; for (const thirdwebTransaction of latestERC20Transactions) { const tokenId = thirdwebTransaction.address; const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(tokenId, "hedera"); if (!token) continue; const hash = thirdwebTransaction.transactionHash; const contractCallResult = await apiClient.getContractCallResult(hash); const mirrorTransaction = await apiClient.findTransactionByContractCall( contractCallResult.timestamp, contractCallResult.contract_id, ); if (!mirrorTransaction) continue; latestERC20Operations.push({ thirdwebTransaction, mirrorTransaction, contractCallResult, token, }); } return latestERC20Operations; }; // TODO: remove once migration to new API is complete export function parseThirdwebTransactionParams( transaction: HederaThirdwebTransaction, ): HederaThirdwebDecodedTransferParams | null { const { from, to, value } = transaction.decoded.params; if (typeof from !== "string" || typeof to !== "string" || typeof value !== "string") { return null; } return { from, to, value }; } /** * Enriches raw ERC20 transfers from Hgraph with additional data needed for operations: * - fetches contract call result containing gas metrics and block hash * - finds the corresponding Mirror Node transaction by consensus timestamp * * @param erc20Transfers - Raw ERC20 transfers from Hgraph API * @returns Array of enriched transfers with complete operation data, filtered to supported tokens only */ export const enrichERC20Transfers = async (erc20Transfers: ERC20TokenTransfer[]) => { const enrichedTransfers: EnrichedERC20Transfer[] = []; // with hgraph we can get two different transfers with the same transaction hash const groupedByTxHash = new Map<string, [ERC20TokenTransfer, ...ERC20TokenTransfer[]]>(); for (const transfer of erc20Transfers) { const group = groupedByTxHash.get(transfer.transaction_hash); if (!group) { groupedByTxHash.set(transfer.transaction_hash, [transfer]); continue; } group.push(transfer); } for (const [txHash, transfers] of groupedByTxHash.entries()) { const payerAddress = toEntityId({ num: transfers[0].payer_account_id }); const inaccurateConsensusTimestampNs = new BigNumber(transfers[0].consensus_timestamp); const inaccurateConsensusTimestamp = nanosToSeconds(inaccurateConsensusTimestampNs).toFixed(9); const [contractCallResult, mirrorTransaction] = await Promise.all([ apiClient.getContractCallResult(txHash), apiClient.findTransactionByContractCallV2({ payerAddress, timestamp: inaccurateConsensusTimestamp, }), ]); if (!mirrorTransaction) { continue; } enrichedTransfers.push({ transfers, contractCallResult, mirrorTransaction, }); } return enrichedTransfers; };