import network from "@ledgerhq/live-network"; import { DEFAULT_SWAP_TIMEOUT_MS } from "../../const/timeout"; import axios from "axios"; import { LedgerAPI4xx } from "@ledgerhq/errors"; import { ExchangeRate, ExchangeRateErrorDefault, ExchangeRateResponseRaw } from "../../types"; import { Unit } from "@ledgerhq/live-app-sdk"; import { SwapGenericAPIError } from "../../../../errors"; import { enrichRatesResponse } from "../../utils/enrichRatesResponse"; import { isIntegrationTestEnv } from "../../utils/isIntegrationTestEnv"; import { fetchRatesMock } from "./__mocks__/fetchRates.mocks"; import { getSwapAPIBaseURL, getSwapUserIP } from "../.."; type Props = { providers: Array; currencyFrom?: string; toCurrencyId?: string; fromCurrencyAmount: string; unitTo: Unit; unitFrom: Unit; }; export const throwRateError = (response: ExchangeRate[]) => { // ranked errors so incases where all rates error we return // the highest priority error to the user. // lowest number is the highest rank. // highest number is lowest rank. const errorsRank = { SwapExchangeRateAmountTooLowOrTooHigh: 1, SwapExchangeRateAmountTooLow: 2, SwapExchangeRateAmountTooHigh: 3, }; const filterLimitResponse = response.filter(rate => { const name = rate.error && rate.error["name"]; return name && errorsRank[name]; }); if (!filterLimitResponse.length) throw new SwapGenericAPIError(); // get the highest ranked error and throw it. const initError = filterLimitResponse[0].error; const error = filterLimitResponse.reduce((acc, curr) => { const currError = curr.error; if (!acc || !currError || !acc["name"] || !currError["name"]) return acc; const currErrorRank: number = errorsRank[currError["name"]]; const accErrorRank: number = errorsRank[acc["name"]]; if (currErrorRank <= accErrorRank) { if (currErrorRank === 1) return currError; const currErrorLimit = currError["amount"]; const accErrorLimit = acc["amount"]; // Get smallest amount supported if ( currErrorRank === 2 && currErrorLimit && currErrorLimit.isLessThan(accErrorLimit || Infinity) ) { return curr.error; } // Get highest amount supported if ( currErrorRank === 3 && currErrorLimit && currErrorLimit.isGreaterThan(accErrorLimit || -Infinity) ) { return curr.error; } } return acc; }, initError); throw error; }; export async function fetchRates({ providers, currencyFrom, toCurrencyId, unitTo, unitFrom, fromCurrencyAmount, }: Props): Promise { if (isIntegrationTestEnv()) { return Promise.resolve( enrichRatesResponse(fetchRatesMock(fromCurrencyAmount, currencyFrom), unitTo, unitFrom), ); } const url = new URL(`${getSwapAPIBaseURL()}/rate`); const requestBody = { from: currencyFrom, to: toCurrencyId, amountFrom: fromCurrencyAmount, // not sure why amountFrom thinks it can be undefined here providers: providers, }; const headers = getSwapUserIP(); try { const { data } = await network({ method: "POST", url: url.toString(), timeout: DEFAULT_SWAP_TIMEOUT_MS, data: requestBody, ...(headers !== undefined ? { headers } : {}), }); const filteredData = data.filter( response => ![300, 304, 306, 308].includes((response as ExchangeRateErrorDefault)?.errorCode), ); // remove backend only errors const enrichedResponse = enrichRatesResponse(filteredData, unitTo, unitFrom); const allErrored = enrichedResponse.every(res => !!res.error); if (allErrored) { throwRateError(enrichedResponse); } // if some of the rates are successful then return those. return enrichedResponse .filter(res => !res.error) .sort((a, b) => b.toAmount.minus(a.toAmount).toNumber()); } catch (e: unknown) { if (axios.isAxiosError(e)) { if (e.code === "ECONNABORTED") { // TODO: LIVE-8901 (handle request timeout) } } if (e instanceof LedgerAPI4xx) { // TODO: LIVE-8901 (handle 4xx) } throw e; } }