@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
135 lines (119 loc) • 4.21 kB
text/typescript
import network from "@ledgerhq/live-network/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<string>;
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<ExchangeRate[]> {
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<ExchangeRateResponseRaw[]>({
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;
}
}