Skip to content

Commit

Permalink
Feat/LIVE-12031 fetch CDN for swap providers (#6929)
Browse files Browse the repository at this point in the history
* 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

* wip

* bugfix: correctly get isRegistration

* chore: clean

* chore: lint

* chore: changeset

* chore: tests

* mock tests

---------

Co-authored-by: Louis PAQUET <[email protected]>
  • Loading branch information
CremaFR and lpaquet-ledger committed May 24, 2024
1 parent 6512191 commit 0bb6b76
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .changeset/seven-olives-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ledger-live-desktop": patch
"@ledgerhq/live-common": patch
---

fetch CDN for swap providers data
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { Text } from "@ledgerhq/react-ui";
import Box from "~/renderer/components/Box";
import FormattedVal from "~/renderer/components/FormattedVal";
import { ExchangeRate, SwapSelectorStateType } from "@ledgerhq/live-common/exchange/swap/types";
import {
getProviderName,
isRegistrationRequired,
} from "@ledgerhq/live-common/exchange/swap/utils/index";
import { getProviderName } from "@ledgerhq/live-common/exchange/swap/utils/index";
import Price from "~/renderer/components/Price";
import CounterValue from "~/renderer/components/CounterValue";
import { Trans } from "react-i18next";
Expand All @@ -20,6 +17,7 @@ export type Props = {
selected?: boolean | null;
fromCurrency?: SwapSelectorStateType["currency"];
toCurrency?: SwapSelectorStateType["currency"];
isRegistrationRequired: boolean;
};

const SecondaryText = styled(Text)`
Expand All @@ -29,7 +27,14 @@ const StyledCounterValue = styled(CounterValue)`
color: ${p => p.theme.colors.neutral.c70};
`;

function SwapRate({ value, selected, onSelect, fromCurrency, toCurrency }: Props) {
function SwapRate({
value,
selected,
onSelect,
fromCurrency,
toCurrency,
isRegistrationRequired,
}: Props) {
const { toAmount: amount, provider } = value;
return (
<Rate
Expand All @@ -41,7 +46,7 @@ function SwapRate({ value, selected, onSelect, fromCurrency, toCurrency }: Props
subtitle={
<Trans
i18nKey={
isRegistrationRequired(value.provider)
isRegistrationRequired
? "swap2.form.rates.registration"
: "swap2.form.rates.noRegistration"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import IconInfoCircle from "~/renderer/icons/InfoCircle";
import { filterRates } from "./filterRates";
import { getFeesUnit } from "@ledgerhq/live-common/account/index";
import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index";
import { isRegistrationRequired } from "@ledgerhq/live-common/exchange/swap/utils/index";

type Props = {
fromCurrency: SwapSelectorStateType["currency"];
Expand Down Expand Up @@ -59,6 +60,9 @@ export default function ProviderRate({
const dispatch = useDispatch();
const [filter, setFilter] = useState<string[]>([]);
const [defaultPartner, setDefaultPartner] = useState<string | null>(null);
const [isRegistrationRequiredMap, setIsRegistrationRequiredMap] = useState<{
[x: string]: boolean;
}>({});
const selectedRate = useSelector(rateSelector);
const filteredRates = useMemo(() => filterRates(rates, filter), [rates, filter]);
const providers = useMemo(() => [...new Set(rates?.map(rate => rate.provider) ?? [])], [rates]);
Expand All @@ -67,6 +71,22 @@ export default function ProviderRate({
? rates.map(({ toAmount }) => formatCurrencyUnit(getFeesUnit(toCurrency), toAmount))
: [];
}, [toCurrency, rates]);
useEffect(() => {
if (providers) {
const fetchlol = async () => {
const results = await Promise.all(
providers.map(async provider => {
const isRequired = await isRegistrationRequired(provider);
return { [provider]: isRequired };
}),
);

const resultsMap = results.reduce((acc, result) => ({ ...acc, ...result }), {});
setIsRegistrationRequiredMap(resultsMap);
};
fetchlol();
}
}, [providers]);
const updateRate = useCallback(
(rate: ExchangeRate) => {
const value = rate.rate ?? rate.provider;
Expand Down Expand Up @@ -242,6 +262,7 @@ export default function ProviderRate({
onSelect={updateRate}
fromCurrency={fromCurrency}
toCurrency={toCurrency}
isRegistrationRequired={isRegistrationRequiredMap[rate.provider]}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,34 @@ export const getRatesMock = () => {
},
]);
};

// services-api-mocks/getProvidersCDNData.mock.js
export const getProvidersCDNDataMock = () =>
JSON.stringify({
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,
},
});
16 changes: 16 additions & 0 deletions apps/ledger-live-desktop/tests/specs/services/swap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getBitcoinToDogecoinRatesMock,
getBitcoinToEthereumRatesMock,
getEthereumToTetherRatesMock,
getProvidersCDNDataMock,
} from "./services-api-mocks/getRates.mock";

test.use({
Expand Down Expand Up @@ -63,6 +64,11 @@ test.describe.parallel("Swap", () => {
});
});

await page.route("https://cdn.live.ledger.com/swap-providers/data.json", async route => {
const mockProvidersResponse = getProvidersCDNDataMock();
await route.fulfill({ headers: { teststatus: "mocked" }, body: mockProvidersResponse });
});

await test.step("Navigate to swap via account page", async () => {
await layout.goToAccounts();
await accountsPage.navigateToAccountByName(ethereumAccountName);
Expand Down Expand Up @@ -110,6 +116,11 @@ test.describe.parallel("Swap", () => {
await route.fulfill({ headers: { teststatus: "mocked" }, body: mockRatesResponse });
});

await page.route("https://cdn.live.ledger.com/swap-providers/data.json", async route => {
const mockProvidersResponse = getProvidersCDNDataMock();
await route.fulfill({ headers: { teststatus: "mocked" }, body: mockProvidersResponse });
});

await test.step("Generate ETH to USDT quotes", async () => {
await swapPage.navigate();
await swapPage.reverseSwapPair();
Expand Down Expand Up @@ -184,6 +195,11 @@ test.describe.parallel("Swap", () => {
});
});

await page.route("https://cdn.live.ledger.com/swap-providers/data.json", async route => {
const mockProvidersResponse = getProvidersCDNDataMock();
await route.fulfill({ headers: { teststatus: "mocked" }, body: mockProvidersResponse });
});

await test.step("Open Swap Page", async () => {
await swapPage.navigate();
await swapPage.waitForSwapFormToLoad();
Expand Down
87 changes: 59 additions & 28 deletions libs/ledger-live-common/src/exchange/providers/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export type SwapProviderConfig = {

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

const swapAdditionData: Record<string, AdditionalProviderConfig> = {
Expand Down Expand Up @@ -40,7 +44,7 @@ const swapAdditionData: Record<string, AdditionalProviderConfig> = {
},
};

const swapProviders: Record<string, ProviderConfig> = {
const swapProviders: Record<string, ProviderConfig & AdditionalProviderConfig> = {
changelly: {
name: "Changelly",
publicKey: {
Expand Down Expand Up @@ -105,8 +109,10 @@ const swapProviders: Record<string, ProviderConfig> = {
},
};

export const getSwapProvider = async (providerName: string): Promise<ProviderConfig> => {
const res = await getProvidersData();
export const getSwapProvider = async (
providerName: string,
): Promise<ProviderConfig & AdditionalProviderConfig> => {
const res = await fetchAndMergeProviderData();

if (!res[providerName.toLowerCase()]) {
throw new Error(`Unknown partner ${providerName}`);
Expand All @@ -115,27 +121,20 @@ export const getSwapProvider = async (providerName: string): Promise<ProviderCon
return res[providerName.toLowerCase()];
};

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

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

return transformedObject;
return transformed;
}

export const getProvidersData = async () => {
Expand All @@ -145,22 +144,54 @@ export const getProvidersData = async () => {
"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);
return providersData;
} catch {
return swapProviders;
}
};

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

if (!res) {
throw new Error(`Unknown partner ${providerName}`);
export const getProvidersCDNData = async () => {
try {
const providersData = await (
await fetch("https://cdn.live.ledger.com/swap-providers/data.json")
).json();
return providersData;
} catch {
return swapAdditionData;
}
};

export const fetchAndMergeProviderData = async () => {
try {
const [providersData, providersExtraData] = await Promise.all([
getProvidersData(),
getProvidersCDNData(),
]);

// Transform and merge fetched data
const transformedProvidersData = transformData(providersData);
const finalProvidersData = mergeProviderData(transformedProvidersData, providersExtraData);

return res;
return finalProvidersData;
} catch (error) {
console.error("Error fetching or processing provider data:", error);
const transformedProvidersData = transformData(swapProviders);
const finalProvidersData = mergeProviderData(transformedProvidersData, swapAdditionData);
return finalProvidersData;
}
};

function mergeProviderData(baseData, additionalData) {
const mergedData = { ...baseData };
Object.keys(additionalData).forEach(key => {
mergedData[key] = {
...mergedData[key],
...additionalData[key],
};
});
return mergedData;
}

export const getAvailableProviders = (): string[] => {
if (isIntegrationTestEnv()) {
return Object.keys(swapProviders).filter(p => p !== "changelly");
Expand Down
12 changes: 6 additions & 6 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 { getProvidersAdditionalData } from "../providers/swap";
import { getSwapProvider } from "../providers/swap";

const getExchangeRates: GetExchangeRates = async ({
exchange,
Expand Down Expand Up @@ -52,7 +52,7 @@ const getExchangeRates: GetExchangeRates = async ({
data: request,
});

const rates = res.data.map(responseData => {
const rates = res.data.map(async responseData => {
const {
rate: maybeRate,
payoutNetworkFees: maybePayoutNetworkFees,
Expand All @@ -66,7 +66,7 @@ const getExchangeRates: GetExchangeRates = async ({
expirationTime,
} = responseData;

const error = inferError(apiAmount, unitFrom, responseData);
const error = await inferError(apiAmount, unitFrom, responseData);
if (error) {
return {
provider,
Expand Down Expand Up @@ -123,7 +123,7 @@ const getExchangeRates: GetExchangeRates = async ({
return rates;
};

const inferError = (
const inferError = async (
apiAmount: BigNumber,
unitFrom: Unit,
responseData: {
Expand All @@ -135,11 +135,11 @@ const inferError = (
status?: string;
provider: string;
},
): Error | CustomMinOrMaxError | undefined => {
): Promise<Error | CustomMinOrMaxError | undefined> => {
const tenPowMagnitude = new BigNumber(10).pow(unitFrom.magnitude);
const { amountTo, minAmountFrom, maxAmountFrom, errorCode, errorMessage, provider, status } =
responseData;
const isDex = getProvidersAdditionalData(provider).type === "DEX";
const isDex = (await getSwapProvider(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
4 changes: 2 additions & 2 deletions libs/ledger-live-common/src/exchange/swap/utils/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ describe("swap/utils/getAvailableAccountsById", () => {
});

describe("swap/utils/isRegistrationRequired", () => {
test("should return registration is not required for changelly", () => {
test("should return registration is not required for changelly", async () => {
const expectedResult = false;

const result = isRegistrationRequired("changelly");
const result = await isRegistrationRequired("changelly");

expect(result).toBe(expectedResult);
});
Expand Down
6 changes: 3 additions & 3 deletions libs/ledger-live-common/src/exchange/swap/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
import { Account, AccountLike, SubAccount } from "@ledgerhq/types-live";
import { getAccountCurrency, makeEmptyTokenAccount } from "../../../account";
import { getProvidersAdditionalData } from "../../providers/swap";
import { getSwapProvider } from "../../providers/swap";

export const FILTER = {
centralised: "centralised",
Expand Down Expand Up @@ -61,8 +61,8 @@ export const getAvailableAccountsById = (
.filter(acc => getAccountCurrency(acc)?.id === id && !acc.disabled)
.sort((a, b) => b.balance.minus(a.balance).toNumber());

export const isRegistrationRequired = (provider: string): boolean => {
const { needsBearerToken, needsKYC } = getProvidersAdditionalData(provider);
export const isRegistrationRequired = async (provider: string): Promise<boolean> => {
const { needsBearerToken, needsKYC } = await getSwapProvider(provider);
return needsBearerToken || needsKYC;
};

Expand Down

0 comments on commit 0bb6b76

Please sign in to comment.