UNPKG

@ledgerhq/live-common

Version:
188 lines (164 loc) • 6.07 kB
import type { Transaction } from "@ledgerhq/coin-bitcoin/types"; import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/formatCurrencyUnit"; import type { Account, AccountLike, FeeStrategy } from "@ledgerhq/types-live"; import { BigNumber } from "bignumber.js"; import { useMemo } from "react"; import type { Transaction as SendFlowTransaction, TransactionStatus } from "../../generated/types"; import { getUTXOStatus } from "./logic"; import { bitcoinPickingStrategy, type BitcoinAccount, type BitcoinOutput, type Transaction as BitcoinTransaction, type TransactionStatus as BitcoinTransactionStatus, } from "./types"; export const useFeesStrategy = (a: Account, t: Transaction): FeeStrategy[] => { const networkInfo = t.networkInfo; if (!networkInfo) return []; const strategies = networkInfo.feeItems.items .map(feeItem => { return { label: feeItem.speed, amount: feeItem.feePerByte, unit: a.currency.units[a.currency.units.length - 1], // Should be sat }; }) .reverse(); return strategies; }; function isBitcoinBasedAccount(account: AccountLike): account is BitcoinAccount { return "bitcoinResources" in account && account.bitcoinResources !== undefined; } function hasUtxoStrategy(tx: SendFlowTransaction): tx is BitcoinTransaction { return "utxoStrategy" in tx && tx.utxoStrategy != null; } function isBitcoinTransactionStatus(s: TransactionStatus): s is BitcoinTransactionStatus { return "txInputs" in s; } export type PickingStrategyOption = Readonly<{ value: number; labelKey: string; }>; export type UtxoRowDisplayData = Readonly<{ utxo: BitcoinOutput; titleLabel: string; formattedValue: string; excluded: boolean; exclusionReason: "pickPendingUtxo" | "userExclusion" | undefined; isUsedInTx: boolean; unconfirmed: boolean; isLastSelected: boolean; disabled: boolean; confirmations: number; }>; export type UseBitcoinUtxoDisplayDataParams = Readonly<{ account: AccountLike; transaction: SendFlowTransaction; status: TransactionStatus; locale: string; }>; export type BitcoinUtxoDisplayData = Readonly<{ pickingStrategyOptions: readonly PickingStrategyOption[]; pickingStrategyValue: number; totalExcludedUTXOS: number; totalSpent: BigNumber; utxoRows: readonly UtxoRowDisplayData[]; }>; // Object.keys returns string[]; cast needed for keyof typeof // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const pickingStrategyKeys = Object.keys( bitcoinPickingStrategy, ) as (keyof typeof bitcoinPickingStrategy)[]; const pickingStrategyOptions: readonly PickingStrategyOption[] = pickingStrategyKeys.map(key => ({ value: bitcoinPickingStrategy[key], labelKey: `bitcoin.pickingStrategyLabels.${String(key)}`, })); type UtxoStatus = ReturnType<typeof getUTXOStatus>; function utxoToRowDisplayData( utxo: BitcoinOutput, utxoStatus: UtxoStatus, context: Readonly<{ rowIndex: number; txInputs: BitcoinTransactionStatus["txInputs"]; totalExcludedUTXOS: number; utxosLength: number; blockHeight: number; accountUnit: BitcoinAccount["currency"]["units"][0]; locale: string; }>, ): UtxoRowDisplayData { const exclusionReason = utxoStatus.excluded ? utxoStatus.reason : undefined; const isUsedInTx = (context.txInputs ?? []).some( input => input.previousOutputIndex === utxo.outputIndex && input.previousTxHash === utxo.hash, ); const unconfirmed = exclusionReason === "pickPendingUtxo"; const isLastSelected = !utxoStatus.excluded && context.totalExcludedUTXOS + 1 === context.utxosLength; const disabled = unconfirmed || isLastSelected; const confirmations = utxo.blockHeight ? Math.max(0, context.blockHeight - utxo.blockHeight) : 0; const formattedValue = formatCurrencyUnit(context.accountUnit, utxo.value, { showCode: true, disableRounding: true, locale: context.locale, }); const address = utxo.address ?? ""; const addressLabel = address ? `${address.slice(0, 8)}...${address.slice(-4)}` : ""; const titleLabel = `#${context.rowIndex + 1} ${addressLabel}`.trim(); return { utxo, titleLabel, formattedValue, excluded: utxoStatus.excluded, exclusionReason, isUsedInTx, unconfirmed, isLastSelected, disabled, confirmations, }; } /** * Fetches all parameters needed to display Bitcoin UTXO rows and the picking strategy selector, * derived from account bitcoin resources, transaction utxoStrategy, and status. * Returns null when the account is not Bitcoin-based or the transaction has no utxoStrategy. */ export function useBitcoinUtxoDisplayData({ account, transaction, status, locale, }: UseBitcoinUtxoDisplayDataParams): BitcoinUtxoDisplayData | null { return useMemo(() => { if (!isBitcoinBasedAccount(account)) return null; if (!hasUtxoStrategy(transaction)) return null; if (!isBitcoinTransactionStatus(status)) return null; const bitcoinAccount = account; const bitcoinResources = bitcoinAccount.bitcoinResources; if (!bitcoinResources?.utxos?.length) return null; const accountUnit = bitcoinAccount.currency.units[0]; const { utxoStrategy } = transaction; const utxos = bitcoinResources.utxos; const blockHeight = bitcoinAccount.blockHeight ?? 0; const utxoStatuses = utxos.map(u => getUTXOStatus(u, utxoStrategy)); const totalExcludedUTXOS = utxoStatuses.filter(s => s.excluded).length; const txInputs = status.txInputs ?? []; const utxoRows: UtxoRowDisplayData[] = utxos.map((utxo, rowIndex) => utxoToRowDisplayData(utxo, utxoStatuses[rowIndex], { rowIndex, txInputs, totalExcludedUTXOS, utxosLength: utxos.length, blockHeight, accountUnit, locale, }), ); return { pickingStrategyOptions, pickingStrategyValue: utxoStrategy.strategy, totalExcludedUTXOS, totalSpent: status.totalSpent ?? new BigNumber(0), utxoRows, }; }, [account, locale, status, transaction]); }