Skip to content

Commit

Permalink
feat(LIVE-9638): change provider info from static object to API call (#…
Browse files Browse the repository at this point in the history
…6853)

* feat(LIVE-9638): change provider info from static object to API call

* feat(LIVE-9638): tests files and changeset

* feat(LIVE-9638): change tests for provider fetch

* feat(LIVE-9638): mock test change

* feat(LIVE-9638): remove useless cache and static swap providers

* feat(LIVE-9638): add feature flag provider management
  • Loading branch information
lpaquet-ledger committed May 21, 2024
1 parent 9085673 commit 2fc0865
Show file tree
Hide file tree
Showing 21 changed files with 130 additions and 92 deletions.
7 changes: 7 additions & 0 deletions .changeset/cool-pots-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"ledger-live-desktop": patch
"live-mobile": patch
"@ledgerhq/live-common": patch
---

Replace static provider info to API
11 changes: 9 additions & 2 deletions apps/ledger-live-mobile/src/screens/Swap/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function SwapForm({
const exchangeRate = useSelector(rateSelector);
// mobile specific
const [confirmed, setConfirmed] = useState(false);
const [swapAcceptedProviders, setSwapAcceptedProviders] = useState<string[]>([]);

const setExchangeRate = useCallback(
(rate?: ExchangeRate) => {
Expand Down Expand Up @@ -314,7 +315,13 @@ export function SwapForm({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);

const swapAcceptedProviders = getAvailableProviders();
useEffect(() => {
const fetchProviders = async () => {
setSwapAcceptedProviders(await getAvailableProviders());
};
fetchProviders();
}, []);

const termsAccepted = (swapAcceptedProviders || []).includes(provider ?? "");
const [deviceMeta, setDeviceMeta] = useState<DeviceMeta>();

Expand All @@ -323,7 +330,7 @@ export function SwapForm({
return <Connect provider={provider} setResult={setDeviceMeta} />;
}

if (getAvailableProviders().length) {
if (swapAcceptedProviders.length) {
return (
<KeyboardAwareScrollView testID="exchange-scrollView">
<Flex flex={1} justifyContent="space-between" padding={6}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const startExchange = (input: StartExchangeInput): Observable<ExchangeRequestEve
if (provider) {
switch (exchangeType) {
case ExchangeTypes.Swap: {
const providerConfig = getSwapProvider(provider);
const providerConfig = await getSwapProvider(provider);
if (providerConfig.type !== "CEX") {
throw new Error(`Unsupported provider type ${providerConfig.type}`);
}
Expand Down
80 changes: 78 additions & 2 deletions libs/ledger-live-common/src/exchange/providers/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,38 @@ export type SwapProviderConfig = {

type CEXProviderConfig = ExchangeProviderNameAndSignature & SwapProviderConfig & { type: "CEX" };
type DEXProviderConfig = SwapProviderConfig & { type: "DEX" };
type AdditionalProviderConfig = SwapProviderConfig & { type: "DEX" | "CEX" } & { version?: number };
export type ProviderConfig = CEXProviderConfig | DEXProviderConfig;

const swapAdditionData: Record<string, AdditionalProviderConfig> = {
changelly: {
needsKYC: false,
needsBearerToken: false,
type: "CEX",
},
cic: {
needsKYC: false,
needsBearerToken: false,
type: "CEX",
},
moonpay: {
needsKYC: true,
needsBearerToken: false,
type: "CEX",
version: 2,
},
oneinch: {
type: "DEX",
needsKYC: false,
needsBearerToken: false,
},
paraswap: {
type: "DEX",
needsKYC: false,
needsBearerToken: false,
},
};

const swapProviders: Record<string, ProviderConfig> = {
changelly: {
name: "Changelly",
Expand Down Expand Up @@ -75,8 +105,54 @@ const swapProviders: Record<string, ProviderConfig> = {
},
};

export const getSwapProvider = (providerName: string): ProviderConfig => {
const res = swapProviders[providerName.toLowerCase()];
export const getSwapProvider = async (providerName: string): Promise<ProviderConfig> => {
const res = getProvidersData()[providerName.toLowerCase()];

if (!res) {
throw new Error(`Unknown partner ${providerName}`);
}

return res;
};

function transformData(inputArray) {
const transformedObject = {};

inputArray.forEach(item => {
const key = item.name.toLowerCase();
transformedObject[key] = {
name: item.name,
publicKey: {
curve: item.public_key_curve,
data: Buffer.from(item.public_key, "hex"),
},
signature: item.signature,
...(swapProviders[key] && {
needsKYC: swapProviders[key].needsKYC,
needsBearerToken: swapProviders[key].needsBearerToken,
type: swapProviders[key].type,
}),
};
});

return transformedObject;
}

export const getProvidersData = async () => {
try {
const providersData = await (
await fetch(
"https://crypto-assets-service.api.aws.prd.ldg-tech.com/v1/partners?output=name,payload_signature_computed_format,signature,public_key,public_key_curve",
)
).json();
return transformData(providersData);
} catch {
return swapProviders;
}
};

export const getProvidersAdditionalData = (providerName: string): AdditionalProviderConfig => {
const res = swapAdditionData[providerName.toLowerCase()];

if (!res) {
throw new Error(`Unknown partner ${providerName}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ describe("fetchCurrencyAll", () => {
data: fetchCurrencyAllMock,
}));

const result = await fetchCurrencyAll({
providers: ["oneinch"],
});
const result = await fetchCurrencyAll({});

expect(result).toStrictEqual(flattenV5CurrenciesAll(fetchCurrencyAllMock));
expect(network as jest.Mock).toHaveBeenCalledWith({
method: "GET",
timeout: DEFAULT_SWAP_TIMEOUT_MS,
url: "https://swap.ledger.com/v5/currencies/all?providers-whitelist=oneinch&additional-coins-flag=false",
url: "https://swap.ledger.com/v5/currencies/all?providers-whitelist=changelly%2Ccic%2Cmoonpay%2Coneinch%2Cparaswap&additional-coins-flag=false",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ describe("fetchCurrencyFrom", () => {
}));

const result = await fetchCurrencyFrom({
providers: ["changelly", "cic", "oneinch"],
currencyTo: "bitcoin",
});

expect(result).toStrictEqual(flattenV5CurrenciesToAndFrom(fetchCurrencyFromMock));
expect(network as jest.Mock).toHaveBeenCalledWith({
method: "GET",
timeout: DEFAULT_SWAP_TIMEOUT_MS,
url: "https://swap.ledger.com/v5/currencies/from?providers-whitelist=changelly%2Ccic%2Coneinch&additional-coins-flag=false&currency-to=bitcoin",
url: "https://swap.ledger.com/v5/currencies/from?providers-whitelist=changelly%2Ccic%2Cmoonpay%2Coneinch%2Cparaswap&additional-coins-flag=false&currency-to=bitcoin",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ describe("fetchCurrencyFrom", () => {
});

const result = await fetchCurrencyTo({
providers: ["changelly", "cic", "oneinch"],
currencyFromId: "bitcoin",
});

expect(result).toStrictEqual(flattenV5CurrenciesToAndFrom(fetchCurrencyToMock));
expect(mockNetwork).toHaveBeenCalledWith({
method: "GET",
timeout: DEFAULT_SWAP_TIMEOUT_MS,
url: "https://swap.ledger.com/v5/currencies/to?providers-whitelist=changelly%2Ccic%2Coneinch&additional-coins-flag=false&currency-from=bitcoin",
url: "https://swap.ledger.com/v5/currencies/to?providers-whitelist=changelly%2Ccic%2Cmoonpay%2Coneinch%2Cparaswap&additional-coins-flag=false&currency-from=bitcoin",
});
});

Expand All @@ -34,7 +33,6 @@ describe("fetchCurrencyFrom", () => {

try {
await fetchCurrencyTo({
providers: ["changelly", "cic", "oneinch"],
currencyFromId: "bitcoin",
});
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { fetchCurrencyAllMock } from "./__mocks__/fetchCurrencyAll.mocks";
import { ResponseData as ResponseDataTo } from "./fetchCurrencyTo";
import { ResponseData as ResponseDataFrom } from "./fetchCurrencyFrom";
import { flattenV5CurrenciesAll } from "../../utils/flattenV5CurrenciesAll";
import { getSwapAPIBaseURL } from "../..";
import { getAvailableProviders, getSwapAPIBaseURL } from "../..";
import { getEnv } from "@ledgerhq/live-env";

type Props = {
providers: Array<string>;
additionalCoinsFlag?: boolean;
};

Expand All @@ -19,10 +18,11 @@ export type ResponseDataAll = {
to: ResponseDataTo["currencyGroups"];
};

export async function fetchCurrencyAll({ providers, additionalCoinsFlag = false }: Props) {
export async function fetchCurrencyAll({ additionalCoinsFlag = false }: Props) {
if (getEnv("MOCK") || getEnv("PLAYWRIGHT_RUN"))
return Promise.resolve(flattenV5CurrenciesAll(fetchCurrencyAllMock));

const providers = getAvailableProviders();
const url = new URL(`${getSwapAPIBaseURL()}/currencies/all`);
url.searchParams.append("providers-whitelist", providers.join(","));
url.searchParams.append("additional-coins-flag", additionalCoinsFlag.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import network from "@ledgerhq/live-network/network";
import { DEFAULT_SWAP_TIMEOUT_MS } from "../../const/timeout";
import { flattenV5CurrenciesToAndFrom } from "../../utils/flattenV5CurrenciesToAndFrom";
import { fetchCurrencyFromMock } from "./__mocks__/fetchCurrencyFrom.mocks";
import { getSwapAPIBaseURL } from "../..";
import { getAvailableProviders, getSwapAPIBaseURL } from "../..";
import { getEnv } from "@ledgerhq/live-env";

type Props = {
providers: Array<string>;
currencyTo?: string;
additionalCoinsFlag?: boolean;
};
Expand All @@ -18,17 +17,15 @@ export type ResponseData = {
}>;
};

export async function fetchCurrencyFrom({
providers,
currencyTo,
additionalCoinsFlag = false,
}: Props) {
export async function fetchCurrencyFrom({ currencyTo, additionalCoinsFlag = false }: Props) {
if (getEnv("MOCK") || getEnv("PLAYWRIGHT_RUN"))
return flattenV5CurrenciesToAndFrom(fetchCurrencyFromMock);

const url = new URL(`${getSwapAPIBaseURL()}/currencies/from`);
const providers = getAvailableProviders();
url.searchParams.append("providers-whitelist", providers.join(","));
url.searchParams.append("additional-coins-flag", additionalCoinsFlag.toString());

if (currencyTo) {
url.searchParams.append("currency-to", currencyTo);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { DEFAULT_SWAP_TIMEOUT_MS } from "../../const/timeout";
import axios from "axios";
import { LedgerAPI4xx } from "@ledgerhq/errors";
import { flattenV5CurrenciesToAndFrom } from "../../utils/flattenV5CurrenciesToAndFrom";
import { getSwapAPIBaseURL } from "../..";
import { getAvailableProviders, getSwapAPIBaseURL } from "../..";

type Props = {
currencyFromId?: string;
providers: string[];
additionalCoinsFlag?: boolean;
};

Expand All @@ -21,16 +20,13 @@ type CurrencyGroup = {
supportedCurrencies: string[];
};

export async function fetchCurrencyTo({
currencyFromId,
providers,
additionalCoinsFlag = false,
}: Props) {
export async function fetchCurrencyTo({ currencyFromId, additionalCoinsFlag = false }: Props) {
if (isIntegrationTestEnv())
return Promise.resolve(flattenV5CurrenciesToAndFrom(fetchCurrencyToMock));

const url = new URL(`${getSwapAPIBaseURL()}/currencies/to`);

const providers = getAvailableProviders();
url.searchParams.append("providers-whitelist", providers.join(","));
url.searchParams.append("additional-coins-flag", additionalCoinsFlag.toString());
url.searchParams.append("currency-from", currencyFromId!);
Expand Down
12 changes: 8 additions & 4 deletions libs/ledger-live-common/src/exchange/swap/api/v5/fetchRates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { SwapGenericAPIError } from "../../../../errors";
import { enrichRatesResponse } from "../../utils/enrichRatesResponse";
import { isIntegrationTestEnv } from "../../utils/isIntegrationTestEnv";
import { fetchRatesMock } from "./__mocks__/fetchRates.mocks";
import { getSwapAPIBaseURL } from "../..";
import { getAvailableProviders, getSwapAPIBaseURL } from "../..";

type Props = {
providers: Array<string>;
removeProviders: Array<string>;
currencyFrom?: string;
toCurrencyId?: string;
fromCurrencyAmount: string;
Expand Down Expand Up @@ -76,7 +76,7 @@ export const throwRateError = (response: ExchangeRate[]) => {
};

export async function fetchRates({
providers,
removeProviders,
currencyFrom,
toCurrencyId,
unitTo,
Expand All @@ -90,11 +90,15 @@ export async function fetchRates({
}

const url = new URL(`${getSwapAPIBaseURL()}/rate`);
const providers = await getAvailableProviders();
removeProviders.forEach(provider => {
providers.splice(providers.indexOf(provider), 1);
});
const requestBody = {
from: currencyFrom,
to: toCurrencyId,
amountFrom: fromCurrencyAmount, // not sure why amountFrom thinks it can be undefined here
providers,
providers: providers,
};

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const completeExchange = (

const confirmExchange = async () => {
await withDevicePromise(deviceId, async transport => {
const providerConfig = getSwapProvider(provider);
const providerConfig = await getSwapProvider(provider);
if (providerConfig.type !== "CEX") {
throw new Error(`Unsupported provider type ${providerConfig.type}`);
}
Expand Down
4 changes: 2 additions & 2 deletions libs/ledger-live-common/src/exchange/swap/getExchangeRates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getSwapAPIBaseURL, getSwapAPIError } from "./";
import { mockGetExchangeRates } from "./mock";
import type { CustomMinOrMaxError, GetExchangeRates } from "./types";
import { isIntegrationTestEnv } from "./utils/isIntegrationTestEnv";
import { getSwapProvider } from "../providers";
import { getProvidersAdditionalData } from "../providers/swap";

const getExchangeRates: GetExchangeRates = async ({
exchange,
Expand Down Expand Up @@ -139,7 +139,7 @@ const inferError = (
const tenPowMagnitude = new BigNumber(10).pow(unitFrom.magnitude);
const { amountTo, minAmountFrom, maxAmountFrom, errorCode, errorMessage, provider, status } =
responseData;
const isDex = getSwapProvider(provider).type === "DEX";
const isDex = getProvidersAdditionalData(provider).type === "DEX";

// DEX quotes are out of limits error. We do not know if it is a low or high limit, neither the amount.
if ((!minAmountFrom || !maxAmountFrom) && status === "error" && errorCode !== 300 && isDex) {
Expand Down
Loading

0 comments on commit 2fc0865

Please sign in to comment.