@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
123 lines (104 loc) • 4.05 kB
text/typescript
import { Account } from "./enum/Account";
import { sanitizeError } from "./index";
import axios, { AxiosRequestConfig } from "axios";
// Target a sensible USD amount that works for most pairs
const FALLBACK_TARGET_USD = 50;
const COUNTERVALUES_URL = "https://countervalues.live.ledger.com/v3/spot/simple";
const SWAP_QUOTE_URL = "https://swap-stg.ledger-test.com/v5/quote";
/** Smallest amount sent to the quote API to discover minimum thresholds. */
const PROBE_AMOUNT = 0.0001;
const PROBE_NETWORK_FEES = 0.001;
const PROVIDERS_WHITELIST = "changelly_v2,exodus,thorswap,uniswap,cic_v2,nearintents,swapsxyz";
type QuoteErrorItem = {
parameter?: { minAmount?: string };
};
function isQuoteErrorItem(item: unknown): item is QuoteErrorItem {
return typeof item === "object" && item !== null && "parameter" in item;
}
/**
* Fetches the current USD price for a currency from the Ledger countervalues API
* and converts a target USD value into the equivalent crypto amount.
*/
export async function getAmountFromUSD(
currencyId: string,
targetUSD: number,
): Promise<number | null> {
try {
const { data } = await axios.get(COUNTERVALUES_URL, {
params: {
froms: currencyId,
to: "USD",
},
});
const price = data?.[currencyId];
if (!price || price <= 0) {
console.warn(`No USD price found for ${currencyId}`);
return null;
}
return targetUSD / price;
} catch (error: unknown) {
console.warn(`Failed to fetch countervalue for ${currencyId}:`, sanitizeError(error));
return null;
}
}
export async function getMinimumSwapAmount(
accountFrom: Account,
accountTo: Account,
providersWhitelist?: string[],
): Promise<number | null> {
try {
const addressFrom = accountFrom.address || accountFrom.parentAccount?.address;
if (!addressFrom) {
throw new Error("No address available from accounts when requesting minimum swap amount.");
}
const requestConfig: AxiosRequestConfig = {
method: "GET",
url: SWAP_QUOTE_URL,
params: {
from: accountFrom.currency.id,
to: accountTo.currency.id,
amountFrom: PROBE_AMOUNT,
addressFrom,
fiatForCounterValue: "USD",
slippage: 1,
networkFees: PROBE_NETWORK_FEES,
networkFeesCurrency: accountTo.currency.speculosApp.name.toLowerCase(),
displayLanguage: "en",
theme: "light",
"providers-whitelist": providersWhitelist?.join(",") ?? PROVIDERS_WHITELIST,
tradeType: "INPUT",
uniswapOrderType: "uniswapxv1",
},
headers: { accept: "application/json" },
};
const { data } = await axios(requestConfig);
if (!Array.isArray(data)) {
console.warn("Unexpected quote API response, falling back to countervalues");
return await getAmountFromUSD(accountFrom.currency.id, FALLBACK_TARGET_USD);
}
// Try to extract minAmount from AMOUNT_OFF_LIMITS errors
const minimumAmounts = data
.filter(isQuoteErrorItem)
.filter(item => item.parameter?.minAmount !== undefined)
.map(item => Number.parseFloat(item.parameter!.minAmount!))
.filter((amount: number) => !Number.isNaN(amount) && amount > 0);
if (minimumAmounts.length > 0) {
return Math.max(...minimumAmounts);
}
// No minAmount returned — compute a sensible fallback from countervalues
console.warn(
`No minAmount from quote API for ${accountFrom.currency.id} → ${accountTo.currency.id}, ` +
`computing fallback from countervalues (~$${FALLBACK_TARGET_USD} USD)`,
);
return await getAmountFromUSD(accountFrom.currency.id, FALLBACK_TARGET_USD);
} catch (error: unknown) {
const sanitizedError = sanitizeError(error);
console.warn("Error fetching swap minimum amount:", sanitizedError);
// Last resort: try to compute a sensible amount even if the quote call failed entirely
try {
return await getAmountFromUSD(accountFrom.currency.id, FALLBACK_TARGET_USD);
} catch {
return null;
}
}
}