import { getAccountCurrency, getFeesUnit } from "@ledgerhq/ledger-wallet-framework/account/index"; import { formatCurrencyUnit } from "@ledgerhq/coin-module-framework/currencies/index"; import { AmountRequired, FeeNotLoaded, NotEnoughBalanceSwap, NotEnoughGas, NotEnoughGasSwap, } from "@ledgerhq/errors"; import { Account } from "@ledgerhq/types-live"; import { useEffect, useMemo, useState } from "react"; import useBridgeTransaction, { Result } from "../../../bridge/useBridgeTransaction"; import { Transaction } from "../../../generated/types"; import { ExchangeRate, OnNoRatesCallback, SwapSelectorStateType, SwapTransactionType, } from "../types"; import { useFromState } from "./useFromState"; import { useReverseAccounts } from "./useReverseAccounts"; import { useToState } from "./useToState"; import { useUpdateMaxAmount } from "./useUpdateMaxAmount"; import { useProviderRates } from "./v5/useProviderRates"; export const selectorStateDefaultValues = { currency: undefined, account: undefined, parentAccount: undefined, amount: undefined, }; export type SetExchangeRateCallback = (exchangeRate?: ExchangeRate) => void; export const useFromAmountStatusMessage = ( { account, parentAccount, status, transaction }: Result, // The order of errors/warnings here will determine the precedence statusTypeToInclude: string[], sponsored?: boolean, ): Error | undefined => { const statusEntries = useMemo( () => statusTypeToInclude.map(statusType => (status.errors || status.warnings)?.[statusType]), [status.errors, status.warnings, statusTypeToInclude], ); const currency = useMemo(() => { if (parentAccount) { return getAccountCurrency(parentAccount); } if (account) { return getAccountCurrency(account); } return undefined; }, [account, parentAccount]); const estimatedFees = useMemo(() => { return status.estimatedFees; }, [status]); return useMemo(() => { // don't return an error/warning if we have no transaction or if transaction.amount <= 0 if (transaction?.amount.lte(0)) return undefined; const [relevantStatus] = statusEntries .filter(maybeError => maybeError instanceof Error) .filter(errorOrWarning => !(errorOrWarning instanceof AmountRequired)); const isRelevantStatus = (relevantStatus as Error) instanceof NotEnoughGas; // Skip gas validation for sponsored transactions since gas fees are covered by sponsor if (isRelevantStatus && currency && estimatedFees && !sponsored) { const query = new URLSearchParams({ // get account id first and set it equal to account. // if parent account exists then overwrite the former. ...(account?.id ? { account: account.id } : {}), ...(parentAccount?.id ? { account: parentAccount.id } : {}), }); return new NotEnoughGasSwap(undefined, { fees: formatCurrencyUnit(getFeesUnit(currency), estimatedFees), ticker: currency.ticker, cryptoName: currency.name, links: [`ledgerlive://buy?${query.toString()}`], }); } // convert to swap variation of error to display correct message to frontend. if (relevantStatus instanceof FeeNotLoaded) { return new NotEnoughBalanceSwap(); } return relevantStatus; }, [ statusEntries, currency, estimatedFees, transaction?.amount, account?.id, parentAccount?.id, sponsored, ]); }; type UseSwapTransactionProps = { accounts?: Account[]; setExchangeRate?: SetExchangeRateCallback; defaultCurrency?: SwapSelectorStateType["currency"]; defaultAccount?: SwapSelectorStateType["account"]; defaultParentAccount?: SwapSelectorStateType["parentAccount"]; onNoRates?: OnNoRatesCallback; excludeFixedRates?: boolean; refreshRate?: number; allowRefresh?: boolean; isEnabled?: boolean; sponsored?: boolean; }; export const useSwapTransaction = ({ accounts, setExchangeRate, defaultCurrency = selectorStateDefaultValues.currency, defaultAccount = selectorStateDefaultValues.account, defaultParentAccount = selectorStateDefaultValues.parentAccount, onNoRates, excludeFixedRates, refreshRate, allowRefresh, isEnabled, sponsored, }: UseSwapTransactionProps): SwapTransactionType => { const bridgeTransaction = useBridgeTransaction(() => ({ account: defaultAccount, parentAccount: defaultParentAccount, })); const { fromState, setFromAccount, setFromAmount } = useFromState({ accounts, defaultCurrency, defaultAccount, defaultParentAccount, bridgeTransaction, }); const { toState, setToAccount, setToAmount, setToCurrency, targetAccounts } = useToState({ accounts, fromCurrencyAccount: fromState.account, }); const { account: fromAccount, parentAccount: fromParentAccount, currency: fromCurrency, } = fromState; const { account: toAccount } = toState; const fromAmountError = useFromAmountStatusMessage( bridgeTransaction, ["gasPrice", "amount", "gasLimit"], sponsored, ); const { isSwapReversable, reverseSwap } = useReverseAccounts({ accounts, fromAccount, toAccount, fromParentAccount, fromCurrency, setFromAccount, setToAccount, }); const { isMaxEnabled, toggleMax, isMaxLoading } = useUpdateMaxAmount({ setFromAmount, account: fromAccount, parentAccount: fromParentAccount, bridge: bridgeTransaction, }); const { rates, refetchRates, updateSelectedRate, countdown } = useProviderRates({ fromState, toState, onNoRates, setExchangeRate, countdown: refreshRate, allowRefresh, isEnabled, }); const [maxAmountLowerThanBallanceError, setMaxAmountLowerThanBallanceError] = useState< Error | undefined >(undefined); useEffect(() => { const timer = setTimeout( () => { // libs/coin-modules/coin-evm/src/prepareTransaction.ts L47 // returns 0 if the balance - fees is less than 0 const error = isMaxEnabled && !isMaxLoading && fromState.amount?.eq(0) ? new NotEnoughBalanceSwap() : undefined; setMaxAmountLowerThanBallanceError(error); }, isMaxEnabled ? 500 : 0, ); // Cleanup the timeout if the component unmounts or the dependencies change return () => clearTimeout(timer); }, [isMaxEnabled, isMaxLoading, fromState]); return { ...bridgeTransaction, swap: { to: toState, from: fromState, isMaxEnabled, isMaxLoading, isSwapReversable, rates: rates.value && excludeFixedRates ? { ...rates, value: rates.value.filter(v => v.tradeMethod !== "fixed"), } : rates, countdown, refetchRates, updateSelectedRate, targetAccounts, }, setFromAmount, toggleMax, fromAmountError: maxAmountLowerThanBallanceError || fromAmountError, setToAccount, setToCurrency, setFromAccount, setToAmount, reverseSwap, }; };