@ledgerhq/live-common 24.0.0-next.0 → 24.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/lib/env.d.ts +16 -6
  2. package/lib/env.d.ts.map +1 -1
  3. package/lib/env.js +11 -1
  4. package/lib/env.js.map +1 -1
  5. package/lib/env.react.d.ts +1 -1
  6. package/lib/env.react.d.ts.map +1 -1
  7. package/lib/errors.d.ts +0 -3
  8. package/lib/errors.d.ts.map +1 -1
  9. package/lib/errors.js +2 -3
  10. package/lib/errors.js.map +1 -1
  11. package/lib/exchange/index.d.ts +5 -0
  12. package/lib/exchange/index.d.ts.map +1 -1
  13. package/lib/exchange/index.js.map +1 -1
  14. package/lib/exchange/swap/checkQuote.d.ts +4 -0
  15. package/lib/exchange/swap/checkQuote.d.ts.map +1 -0
  16. package/lib/exchange/swap/checkQuote.js +80 -0
  17. package/lib/exchange/swap/checkQuote.js.map +1 -0
  18. package/lib/exchange/swap/checkQuote.test.d.ts +2 -0
  19. package/lib/exchange/swap/checkQuote.test.d.ts.map +1 -0
  20. package/lib/exchange/swap/checkQuote.test.js +103 -0
  21. package/lib/exchange/swap/checkQuote.test.js.map +1 -0
  22. package/lib/exchange/swap/completeExchange.d.ts.map +1 -1
  23. package/lib/exchange/swap/completeExchange.js +9 -9
  24. package/lib/exchange/swap/completeExchange.js.map +1 -1
  25. package/lib/exchange/swap/getExchangeRates.d.ts.map +1 -1
  26. package/lib/exchange/swap/getExchangeRates.js +3 -1
  27. package/lib/exchange/swap/getExchangeRates.js.map +1 -1
  28. package/lib/exchange/swap/getProviders.d.ts.map +1 -1
  29. package/lib/exchange/swap/getProviders.js +10 -4
  30. package/lib/exchange/swap/getProviders.js.map +1 -1
  31. package/lib/exchange/swap/getStatus.d.ts +1 -3
  32. package/lib/exchange/swap/getStatus.d.ts.map +1 -1
  33. package/lib/exchange/swap/getStatus.js +2 -30
  34. package/lib/exchange/swap/getStatus.js.map +1 -1
  35. package/lib/exchange/swap/hooks/useProviderRates.d.ts +3 -4
  36. package/lib/exchange/swap/hooks/useProviderRates.d.ts.map +1 -1
  37. package/lib/exchange/swap/hooks/useProviderRates.js +35 -5
  38. package/lib/exchange/swap/hooks/useProviderRates.js.map +1 -1
  39. package/lib/exchange/swap/hooks/useProviderRates.test.js +106 -1
  40. package/lib/exchange/swap/hooks/useProviderRates.test.js.map +1 -1
  41. package/lib/exchange/swap/hooks/useSwapTransaction.d.ts +5 -4
  42. package/lib/exchange/swap/hooks/useSwapTransaction.d.ts.map +1 -1
  43. package/lib/exchange/swap/hooks/useSwapTransaction.js +6 -6
  44. package/lib/exchange/swap/hooks/useSwapTransaction.js.map +1 -1
  45. package/lib/exchange/swap/hooks/useUpdateMaxAmount.d.ts +4 -3
  46. package/lib/exchange/swap/hooks/useUpdateMaxAmount.d.ts.map +1 -1
  47. package/lib/exchange/swap/hooks/useUpdateMaxAmount.js +8 -5
  48. package/lib/exchange/swap/hooks/useUpdateMaxAmount.js.map +1 -1
  49. package/lib/exchange/swap/hooks/useUpdateMaxAmount.test.js +35 -4
  50. package/lib/exchange/swap/hooks/useUpdateMaxAmount.test.js.map +1 -1
  51. package/lib/exchange/swap/index.d.ts +10 -8
  52. package/lib/exchange/swap/index.d.ts.map +1 -1
  53. package/lib/exchange/swap/index.js +43 -18
  54. package/lib/exchange/swap/index.js.map +1 -1
  55. package/lib/exchange/swap/index.test.d.ts +2 -0
  56. package/lib/exchange/swap/index.test.d.ts.map +1 -0
  57. package/lib/exchange/swap/index.test.js +30 -0
  58. package/lib/exchange/swap/index.test.js.map +1 -0
  59. package/lib/exchange/swap/initSwap.d.ts +1 -1
  60. package/lib/exchange/swap/initSwap.d.ts.map +1 -1
  61. package/lib/exchange/swap/initSwap.js +30 -25
  62. package/lib/exchange/swap/initSwap.js.map +1 -1
  63. package/lib/exchange/swap/mock.d.ts +5 -2
  64. package/lib/exchange/swap/mock.d.ts.map +1 -1
  65. package/lib/exchange/swap/mock.js +127 -6
  66. package/lib/exchange/swap/mock.js.map +1 -1
  67. package/lib/exchange/swap/postSwapState.d.ts +4 -0
  68. package/lib/exchange/swap/postSwapState.d.ts.map +1 -0
  69. package/lib/exchange/swap/postSwapState.js +121 -0
  70. package/lib/exchange/swap/postSwapState.js.map +1 -0
  71. package/lib/exchange/swap/types.d.ts +30 -2
  72. package/lib/exchange/swap/types.d.ts.map +1 -1
  73. package/lib/exchange/swap/updateAccountSwapStatus.d.ts.map +1 -1
  74. package/lib/exchange/swap/updateAccountSwapStatus.js +2 -2
  75. package/lib/exchange/swap/updateAccountSwapStatus.js.map +1 -1
  76. package/lib/exchange/swap/utils/index.d.ts +19 -2
  77. package/lib/exchange/swap/utils/index.d.ts.map +1 -1
  78. package/lib/exchange/swap/utils/index.js +96 -20
  79. package/lib/exchange/swap/utils/index.js.map +1 -1
  80. package/lib/exchange/swap/utils/index.test.js +95 -48
  81. package/lib/exchange/swap/utils/index.test.js.map +1 -1
  82. package/lib/hw/actions/initSwap.d.ts +4 -3
  83. package/lib/hw/actions/initSwap.d.ts.map +1 -1
  84. package/lib/hw/actions/initSwap.js +7 -5
  85. package/lib/hw/actions/initSwap.js.map +1 -1
  86. package/package.json +4 -1
  87. package/src/env.ts +12 -2
  88. package/src/errors.ts +0 -1
  89. package/src/exchange/index.ts +7 -0
  90. package/src/exchange/swap/checkQuote.test.ts +53 -0
  91. package/src/exchange/swap/checkQuote.ts +34 -0
  92. package/src/exchange/swap/completeExchange.ts +9 -11
  93. package/src/exchange/swap/getExchangeRates.ts +5 -3
  94. package/src/exchange/swap/getProviders.ts +12 -4
  95. package/src/exchange/swap/getStatus.ts +2 -12
  96. package/src/exchange/swap/hooks/useProviderRates.test.ts +103 -1
  97. package/src/exchange/swap/hooks/useProviderRates.ts +57 -9
  98. package/src/exchange/swap/hooks/useSwapTransaction.ts +14 -13
  99. package/src/exchange/swap/hooks/useUpdateMaxAmount.test.ts +31 -4
  100. package/src/exchange/swap/hooks/useUpdateMaxAmount.ts +13 -7
  101. package/src/exchange/swap/index.test.ts +36 -0
  102. package/src/exchange/swap/index.ts +51 -30
  103. package/src/exchange/swap/initSwap.ts +54 -40
  104. package/src/exchange/swap/mock.ts +128 -13
  105. package/src/exchange/swap/postSwapState.ts +61 -0
  106. package/src/exchange/swap/types.ts +68 -5
  107. package/src/exchange/swap/updateAccountSwapStatus.ts +4 -4
  108. package/src/exchange/swap/utils/index.test.ts +127 -63
  109. package/src/exchange/swap/utils/index.ts +128 -23
  110. package/src/hw/actions/initSwap.ts +20 -9
@@ -46,6 +46,13 @@ export type ExchangeProviderNameAndSignature = {
46
46
  nameAndPubkey: Buffer;
47
47
  signature: Buffer;
48
48
  };
49
+
50
+ export type SwapProviderConfig = ExchangeProviderNameAndSignature & {
51
+ curve: string;
52
+ needsKYC: boolean;
53
+ needsBearerToken: boolean;
54
+ };
55
+
49
56
  export const isExchangeSupportedByApp = (
50
57
  appName: string,
51
58
  appVersion: string
@@ -0,0 +1,53 @@
1
+ import axios from "axios";
2
+ import checkQuote from "./checkQuote";
3
+
4
+ jest.mock("axios");
5
+ const mockedAxios = jest.mocked(axios);
6
+
7
+ const provider = "changelly";
8
+ const quoteId = "RATE_ID";
9
+ const bearerToken = "BEARER_TOKEN";
10
+
11
+ describe("swap/checkQuote", () => {
12
+ test("should return response in case of valid rate", async () => {
13
+ const data = {
14
+ code: 1,
15
+ codeName: "RATE_VALID",
16
+ description: "KYC check passed",
17
+ };
18
+
19
+ const resp = {
20
+ data,
21
+ status: 200,
22
+ statusText: "",
23
+ headers: {},
24
+ config: {},
25
+ };
26
+
27
+ mockedAxios.mockResolvedValue(Promise.resolve(resp));
28
+ const res = await checkQuote({ provider, quoteId, bearerToken });
29
+
30
+ expect(res).toEqual(data);
31
+ });
32
+
33
+ test("should return response in case of invalid rate", async () => {
34
+ const data = {
35
+ code: 204,
36
+ codeName: "MFA_REQUIRED",
37
+ description: "MFA Required",
38
+ };
39
+
40
+ const resp = {
41
+ data,
42
+ status: 200,
43
+ statusText: "",
44
+ headers: {},
45
+ config: {},
46
+ };
47
+
48
+ mockedAxios.mockResolvedValue(Promise.resolve(resp));
49
+ const res = await checkQuote({ provider, quoteId, bearerToken });
50
+
51
+ expect(res).toEqual(data);
52
+ });
53
+ });
@@ -0,0 +1,34 @@
1
+ import { getEnv } from "../../env";
2
+ import network from "../../network";
3
+ import { getSwapAPIBaseURL } from "./";
4
+ import { mockCheckQuote } from "./mock";
5
+ import type { CheckQuote } from "./types";
6
+
7
+ const checkQuote: CheckQuote = async ({ provider, quoteId, bearerToken }) => {
8
+ if (getEnv("MOCK") || getEnv("MOCK_SWAP_CHECK_QUOTE")) {
9
+ return mockCheckQuote({ provider, quoteId, bearerToken });
10
+ }
11
+
12
+ const request = {
13
+ provider,
14
+ rateId: quoteId,
15
+ };
16
+
17
+ const { data } = await network({
18
+ method: "POST",
19
+ url: `${getSwapAPIBaseURL()}/rate/check`,
20
+ headers: {
21
+ Authorization: `Bearer ${bearerToken}`,
22
+ },
23
+ data: request,
24
+ });
25
+
26
+ /**
27
+ * TODO: we might want to handle various status and error here
28
+ * cf. https://github.com/LedgerHQ/swap/blob/master/modules/model/src/main/scala/co/ledger/swap/model/RateStatus.scala
29
+ */
30
+
31
+ return data;
32
+ };
33
+
34
+ export default checkQuote;
@@ -1,22 +1,20 @@
1
+ import { TransportStatusError, WrongDeviceForAccount } from "@ledgerhq/errors";
1
2
  import { log } from "@ledgerhq/logs";
2
3
  import { from, Observable } from "rxjs";
3
4
  import secp256k1 from "secp256k1";
4
- import { TransportStatusError, WrongDeviceForAccount } from "@ledgerhq/errors";
5
-
6
- import { delay } from "../../promise";
7
- import ExchangeTransport from "../hw-app-exchange/Exchange";
8
- import perFamily from "../../generated/exchange";
5
+ import { getCurrencyExchangeConfig } from "../";
9
6
  import { getAccountCurrency, getMainAccount } from "../../account";
10
7
  import { getAccountBridge } from "../../bridge";
11
8
  import { TransactionRefusedOnDevice } from "../../errors";
9
+ import perFamily from "../../generated/exchange";
12
10
  import { withDevice } from "../../hw/deviceAccess";
13
- import { getProviderNameAndSignature } from "./";
14
- import { getCurrencyExchangeConfig } from "../";
15
-
11
+ import { delay } from "../../promise";
12
+ import ExchangeTransport from "../hw-app-exchange/Exchange";
16
13
  import type {
17
14
  CompleteExchangeInputSwap,
18
15
  CompleteExchangeRequestEvent,
19
16
  } from "../platform/types";
17
+ import { getProviderConfig } from "./";
20
18
 
21
19
  const withDevicePromise = (deviceId, fn) =>
22
20
  withDevice(deviceId)((transport) => from(fn(transport))).toPromise();
@@ -45,7 +43,7 @@ const completeExchange = (
45
43
 
46
44
  const confirmExchange = async () => {
47
45
  await withDevicePromise(deviceId, async (transport) => {
48
- const providerNameAndSignature = getProviderNameAndSignature(provider);
46
+ const providerConfig = getProviderConfig(provider);
49
47
  const exchange = new ExchangeTransport(
50
48
  transport,
51
49
  exchangeType,
@@ -75,10 +73,10 @@ const completeExchange = (
75
73
  const errorsKeys = Object.keys(errors);
76
74
  if (errorsKeys.length > 0) throw errors[errorsKeys[0]]; // throw the first error
77
75
 
78
- await exchange.setPartnerKey(providerNameAndSignature.nameAndPubkey);
76
+ await exchange.setPartnerKey(providerConfig.nameAndPubkey);
79
77
  if (unsubscribed) return;
80
78
 
81
- await exchange.checkPartner(providerNameAndSignature.signature);
79
+ await exchange.checkPartner(providerConfig.signature);
82
80
  if (unsubscribed) return;
83
81
 
84
82
  await exchange.processTransaction(
@@ -13,9 +13,9 @@ import type {
13
13
  Transaction,
14
14
  Unit,
15
15
  } from "../../types";
16
- import { getSwapAPIBaseURL, getSwapAPIError } from "./";
16
+ import { getAvailableProviders, getSwapAPIBaseURL, getSwapAPIError } from "./";
17
17
  import { mockGetExchangeRates } from "./mock";
18
- import type { Exchange, GetExchangeRates } from "./types";
18
+ import type { CustomMinOrMaxError, Exchange, GetExchangeRates } from "./types";
19
19
 
20
20
  const getExchangeRates: GetExchangeRates = async (
21
21
  exchange: Exchange,
@@ -40,6 +40,7 @@ const getExchangeRates: GetExchangeRates = async (
40
40
  from,
41
41
  to,
42
42
  amountFrom: apiAmount.toString(),
43
+ providers: usesV3 ? getAvailableProviders() : undefined,
43
44
  };
44
45
  const res = await network({
45
46
  method: "POST",
@@ -122,7 +123,7 @@ const inferError = (
122
123
  errorCode?: number;
123
124
  errorMessage?: string;
124
125
  }
125
- ): Error | undefined => {
126
+ ): Error | CustomMinOrMaxError | undefined => {
126
127
  const tenPowMagnitude = new BigNumber(10).pow(unitFrom.magnitude);
127
128
  const { amountTo, minAmountFrom, maxAmountFrom, errorCode, errorMessage } =
128
129
  responseData;
@@ -157,6 +158,7 @@ const inferError = (
157
158
  showCode: true,
158
159
  }
159
160
  ),
161
+ amount: new BigNumber(amount),
160
162
  });
161
163
  }
162
164
  }
@@ -1,15 +1,23 @@
1
- import type { GetProviders } from "./types";
2
- import network from "../../network";
3
- import { getSwapAPIBaseURL } from "./";
1
+ import qs from "qs";
4
2
  import { getEnv } from "../../env";
5
- import { mockGetProviders } from "./mock";
6
3
  import { SwapNoAvailableProviders } from "../../errors";
4
+ import network from "../../network";
5
+ import { getAvailableProviders, getSwapAPIBaseURL } from "./";
6
+ import { mockGetProviders } from "./mock";
7
+ import type { GetProviders } from "./types";
7
8
 
8
9
  const getProviders: GetProviders = async () => {
9
10
  if (getEnv("MOCK")) return mockGetProviders();
11
+
12
+ // Rely on the api base to determine the version logic
13
+ const usesV3 = getSwapAPIBaseURL().endsWith("v3");
14
+
10
15
  const res = await network({
11
16
  method: "GET",
12
17
  url: `${getSwapAPIBaseURL()}/providers`,
18
+ params: usesV3 ? { whitelist: getAvailableProviders() } : undefined,
19
+ paramsSerializer: (params) =>
20
+ qs.stringify(params, { arrayFormat: "comma" }),
13
21
  });
14
22
 
15
23
  if (!res.data.length) {
@@ -1,18 +1,9 @@
1
+ import { getEnv } from "../../env";
1
2
  import network from "../../network";
2
3
  import { getSwapAPIBaseURL } from "./";
3
- import type { GetMultipleStatus, GetStatus } from "./types";
4
- import { getEnv } from "../../env";
5
4
  import { mockGetStatus } from "./mock";
6
- import { SwapUnknownSwapId } from "../../errors";
7
- export const getStatus: GetStatus = async (status) => {
8
- const updatedStatus = await getMultipleStatus([status]);
5
+ import type { GetMultipleStatus } from "./types";
9
6
 
10
- if (updatedStatus && updatedStatus.length) {
11
- return updatedStatus[0];
12
- }
13
-
14
- throw new SwapUnknownSwapId(undefined, { ...status });
15
- };
16
7
  export const getMultipleStatus: GetMultipleStatus = async (statusList) => {
17
8
  if (getEnv("MOCK")) return mockGetStatus(statusList);
18
9
  const res = await network({
@@ -22,4 +13,3 @@ export const getMultipleStatus: GetMultipleStatus = async (statusList) => {
22
13
  });
23
14
  return res.data;
24
15
  };
25
- export default getStatus;
@@ -1,8 +1,12 @@
1
- import { renderHook, act } from "@testing-library/react-hooks";
2
1
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
2
+ import { act, renderHook } from "@testing-library/react-hooks";
3
3
  import BigNumber from "bignumber.js";
4
4
  import { useProviderRates } from ".";
5
5
  import { getExchangeRates } from "..";
6
+ import {
7
+ SwapExchangeRateAmountTooHigh,
8
+ SwapExchangeRateAmountTooLow,
9
+ } from "../../../errors";
6
10
  import { genAccount } from "../../../mock/account";
7
11
  import { mockGetExchangeRates } from "../mock";
8
12
 
@@ -184,6 +188,104 @@ describe("useProviderRates", () => {
184
188
  expect(result.current.rates.value).toBeUndefined();
185
189
  });
186
190
 
191
+ it("should sort SwapExchangeRateAmountTooLow based on amount", async () => {
192
+ const swapExchangeRateAmountTooLowError1 = new SwapExchangeRateAmountTooLow(
193
+ undefined,
194
+ {
195
+ amount: new BigNumber(1),
196
+ }
197
+ );
198
+
199
+ const swapExchangeRateAmountTooLowError2 = new SwapExchangeRateAmountTooLow(
200
+ undefined,
201
+ {
202
+ amount: new BigNumber(2),
203
+ }
204
+ );
205
+
206
+ mockedRates[0].error = swapExchangeRateAmountTooLowError2;
207
+ mockedRates[1].error = swapExchangeRateAmountTooLowError1;
208
+
209
+ mockedGetExchangeRates.mockResolvedValue(mockedRates);
210
+ const { result, waitForNextUpdate } = renderHook(useProviderRates, {
211
+ initialProps: baseInitalProps,
212
+ });
213
+ await waitForNextUpdate({ timeout: 1000 });
214
+ expect(result.current.rates.status).toBe("error");
215
+ expect(result.current.rates.error).toBe(swapExchangeRateAmountTooLowError1);
216
+ expect(result.current.rates.value).toBeUndefined();
217
+ });
218
+
219
+ it("should sort SwapExchangeRateAmountTooHigh based on amount", async () => {
220
+ const swapExchangeRateAmountTooHighError1 =
221
+ new SwapExchangeRateAmountTooHigh(undefined, {
222
+ amount: new BigNumber(1),
223
+ });
224
+
225
+ const swapExchangeRateAmountTooHighError2 =
226
+ new SwapExchangeRateAmountTooHigh(undefined, {
227
+ amount: new BigNumber(2),
228
+ });
229
+
230
+ mockedRates[0].error = swapExchangeRateAmountTooHighError1;
231
+ mockedRates[1].error = swapExchangeRateAmountTooHighError2;
232
+
233
+ mockedGetExchangeRates.mockResolvedValue(mockedRates);
234
+ const { result, waitForNextUpdate } = renderHook(useProviderRates, {
235
+ initialProps: baseInitalProps,
236
+ });
237
+ await waitForNextUpdate({ timeout: 1000 });
238
+ expect(result.current.rates.status).toBe("error");
239
+ expect(result.current.rates.error).toBe(
240
+ swapExchangeRateAmountTooHighError2
241
+ );
242
+ expect(result.current.rates.value).toBeUndefined();
243
+ });
244
+
245
+ it("SwapExchangeRateAmountTooLow should take precedence over standard errors", async () => {
246
+ const swapExchangeRateAmountTooLowError = new SwapExchangeRateAmountTooLow(
247
+ undefined,
248
+ {
249
+ amount: new BigNumber(1),
250
+ }
251
+ );
252
+
253
+ const standardError = new Error("Error");
254
+
255
+ mockedRates[0].error = standardError;
256
+ mockedRates[1].error = swapExchangeRateAmountTooLowError;
257
+
258
+ mockedGetExchangeRates.mockResolvedValue(mockedRates);
259
+ const { result, waitForNextUpdate } = renderHook(useProviderRates, {
260
+ initialProps: baseInitalProps,
261
+ });
262
+ await waitForNextUpdate({ timeout: 1000 });
263
+ expect(result.current.rates.status).toBe("error");
264
+ expect(result.current.rates.error).toBe(swapExchangeRateAmountTooLowError);
265
+ expect(result.current.rates.value).toBeUndefined();
266
+ });
267
+
268
+ it("SwapExchangeRateAmountTooHigh should take precedence over standard errors", async () => {
269
+ const SwapExchangeRateAmountTooHighError =
270
+ new SwapExchangeRateAmountTooHigh(undefined, {
271
+ amount: new BigNumber(1),
272
+ });
273
+
274
+ const standardError = new Error("Error");
275
+
276
+ mockedRates[0].error = standardError;
277
+ mockedRates[1].error = SwapExchangeRateAmountTooHighError;
278
+
279
+ mockedGetExchangeRates.mockResolvedValue(mockedRates);
280
+ const { result, waitForNextUpdate } = renderHook(useProviderRates, {
281
+ initialProps: baseInitalProps,
282
+ });
283
+ await waitForNextUpdate({ timeout: 1000 });
284
+ expect(result.current.rates.status).toBe("error");
285
+ expect(result.current.rates.error).toBe(SwapExchangeRateAmountTooHighError);
286
+ expect(result.current.rates.value).toBeUndefined();
287
+ });
288
+
187
289
  it("should call 'onNoRates' if there are no rates returned by the server", async () => {
188
290
  const onNoRates = jest.fn(() => {});
189
291
  mockedGetExchangeRates.mockResolvedValue([]);
@@ -1,13 +1,12 @@
1
1
  import { useCallback, useEffect, useReducer, useState } from "react";
2
+ import { getExchangeRates } from "..";
3
+ import { Transaction } from "../../../generated/types";
4
+ import type { CustomMinOrMaxError, Exchange, ExchangeRate } from "../types";
2
5
  import {
3
6
  OnNoRatesCallback,
4
7
  SetExchangeRateCallback,
5
8
  SwapSelectorStateType,
6
9
  } from "./useSwapTransaction";
7
- import { getExchangeRates } from "..";
8
- import { Exchange, ExchangeRate } from "../types";
9
- import { pickExchangeRate } from "../utils";
10
- import { Transaction } from "../../../generated/types";
11
10
 
12
11
  export type RatesReducerState = {
13
12
  status?: string | null;
@@ -34,14 +33,12 @@ const ratesReducer = (state: RatesReducerState, action): RatesReducerState => {
34
33
  export const useProviderRates = ({
35
34
  fromState,
36
35
  toState,
37
- exchangeRate,
38
36
  transaction,
39
37
  onNoRates,
40
38
  setExchangeRate,
41
39
  }: {
42
40
  fromState: SwapSelectorStateType;
43
41
  toState: SwapSelectorStateType;
44
- exchangeRate?: ExchangeRate | null | undefined;
45
42
  transaction?: Transaction | null | undefined;
46
43
  onNoRates?: OnNoRatesCallback | null | undefined;
47
44
  setExchangeRate?: SetExchangeRateCallback | null | undefined;
@@ -82,23 +79,74 @@ export const useProviderRates = ({
82
79
  undefined,
83
80
  toCurrency
84
81
  );
82
+
85
83
  if (abort) return;
86
84
  if (rates.length === 0) {
87
85
  onNoRates && onNoRates({ fromState, toState });
88
86
  }
87
+
89
88
  // Discard bad provider rates
90
- let rateError: Error | null | undefined = null;
89
+ let rateError: Error | CustomMinOrMaxError | null | undefined = null;
91
90
  rates = rates.reduce<ExchangeRate[]>((acc, rate) => {
92
91
  rateError = rateError ?? rate.error;
92
+
93
+ /**
94
+ * If we have an error linked to the ammount, this error takes
95
+ * precedence over the other (like a "CurrencyNotSupportedError" one
96
+ * for example)
97
+ */
98
+ if (
99
+ rateError?.name !== rate.error?.name &&
100
+ (rate.error?.name === "SwapExchangeRateAmountTooLow" ||
101
+ rate.error?.name === "SwapExchangeRateAmountTooHigh")
102
+ ) {
103
+ rateError = rate.error;
104
+ }
105
+
106
+ if (
107
+ rateError?.name === rate.error?.name &&
108
+ (rate.error?.name === "SwapExchangeRateAmountTooLow" ||
109
+ rate.error?.name === "SwapExchangeRateAmountTooHigh")
110
+ ) {
111
+ /**
112
+ * Comparison pivot, depending on the order in which we want to sort errors
113
+ * If the order is ascending, the pivot is -1, otherwise it's 1
114
+ * Based on returns from https://mikemcl.github.io/bignumber.js/#cmp
115
+ */
116
+ const cmp =
117
+ rateError?.name === "SwapExchangeRateAmountTooLow" ? -1 : 1;
118
+
119
+ /**
120
+ * If the amount is too low, the user should put at least the
121
+ * minimum amount possible
122
+ * If the amount is too high, the user should put at most the
123
+ * maximum amount possible
124
+ */
125
+
126
+ rateError =
127
+ (rateError as CustomMinOrMaxError).amount.comparedTo(
128
+ (rate.error as CustomMinOrMaxError)?.amount
129
+ ) === cmp
130
+ ? rateError
131
+ : rate.error;
132
+ }
133
+
93
134
  return rate.error ? acc : [...acc, rate];
94
135
  }, []);
136
+
95
137
  if (rates.length === 0 && rateError) {
96
138
  // If all the rates are in error
97
139
  dispatchRates({ type: "error", payload: rateError });
98
140
  } else {
99
141
  dispatchRates({ type: "set", payload: rates });
100
- setExchangeRate &&
101
- pickExchangeRate(rates, exchangeRate, setExchangeRate);
142
+
143
+ /**
144
+ * Select the first rate returned by the API. Should be the prefered
145
+ * rate for the user. Rate ordering logic is handeled on backend side
146
+ */
147
+ const rate = rates?.length > 0 ? rates[0] : null;
148
+
149
+ setExchangeRate && setExchangeRate(rate);
102
150
  }
103
151
  } catch (error) {
104
152
  !abort && dispatchRates({ type: "error", payload: error });
@@ -1,24 +1,24 @@
1
- import { useMemo } from "react";
1
+ import { AmountRequired } from "@ledgerhq/errors";
2
2
  import { BigNumber } from "bignumber.js";
3
+ import { useMemo } from "react";
4
+ import {
5
+ RatesReducerState,
6
+ useFromState,
7
+ useProviderRates,
8
+ useToState,
9
+ } from ".";
3
10
  import useBridgeTransaction, {
4
11
  Result as UseBridgeTransactionReturnType,
5
12
  } from "../../../bridge/useBridgeTransaction";
6
13
  import type {
7
14
  Account,
15
+ CryptoCurrency,
8
16
  TokenAccount,
9
17
  TokenCurrency,
10
- CryptoCurrency,
11
18
  } from "../../../types";
12
19
  import { ExchangeRate } from "../types";
13
- import { AmountRequired } from "@ledgerhq/errors";
14
- import { useUpdateMaxAmount } from "./useUpdateMaxAmount";
15
- import {
16
- RatesReducerState,
17
- useFromState,
18
- useProviderRates,
19
- useToState,
20
- } from ".";
21
20
  import { useReverseAccounts } from "./useReverseAccounts";
21
+ import { useUpdateMaxAmount } from "./useUpdateMaxAmount";
22
22
 
23
23
  export type SwapSelectorStateType = {
24
24
  currency: null | undefined | TokenCurrency | CryptoCurrency;
@@ -65,6 +65,7 @@ export type OnNoRatesCallback = (arg: {
65
65
  export type SetExchangeRateCallback = (
66
66
  exchangeRate?: ExchangeRate | null
67
67
  ) => void;
68
+ export type SetIsSendMaxLoading = (boolean) => void;
68
69
 
69
70
  export const useFromAmountError = (
70
71
  errors: Record<string, Error | undefined>
@@ -82,16 +83,16 @@ export const useFromAmountError = (
82
83
 
83
84
  export const useSwapTransaction = ({
84
85
  accounts,
85
- exchangeRate,
86
86
  setExchangeRate,
87
+ setIsSendMaxLoading,
87
88
  defaultCurrency = selectorStateDefaultValues.currency,
88
89
  defaultAccount = selectorStateDefaultValues.account,
89
90
  defaultParentAccount = selectorStateDefaultValues.parentAccount,
90
91
  onNoRates,
91
92
  }: {
92
93
  accounts?: Account[];
93
- exchangeRate?: ExchangeRate;
94
94
  setExchangeRate?: SetExchangeRateCallback;
95
+ setIsSendMaxLoading?: SetIsSendMaxLoading;
95
96
  defaultCurrency?: SwapSelectorStateType["currency"];
96
97
  defaultAccount?: SwapSelectorStateType["account"];
97
98
  defaultParentAccount?: SwapSelectorStateType["parentAccount"];
@@ -136,12 +137,12 @@ export const useSwapTransaction = ({
136
137
  parentAccount: fromParentAccount,
137
138
  transaction,
138
139
  feesStrategy: transaction?.feesStrategy,
140
+ setIsSendMaxLoading,
139
141
  });
140
142
 
141
143
  const { rates, refetchRates } = useProviderRates({
142
144
  fromState,
143
145
  toState,
144
- exchangeRate,
145
146
  transaction,
146
147
  onNoRates,
147
148
  setExchangeRate,
@@ -1,10 +1,10 @@
1
- import { renderHook, act } from "@testing-library/react-hooks";
2
1
  import { getCryptoCurrencyById, getTokenById } from "@ledgerhq/cryptoassets";
3
- import { useUpdateMaxAmount, ZERO } from "./useUpdateMaxAmount";
4
- import { genAccount, genTokenAccount } from "../../../mock/account";
2
+ import { act, renderHook } from "@testing-library/react-hooks";
5
3
  import BigNumber from "bignumber.js";
6
- import ethBridge from "../../../families/ethereum/bridge/mock";
7
4
  import { checkAccountSupported } from "../../../account/support";
5
+ import ethBridge from "../../../families/ethereum/bridge/mock";
6
+ import { genAccount, genTokenAccount } from "../../../mock/account";
7
+ import { useUpdateMaxAmount, ZERO } from "./useUpdateMaxAmount";
8
8
 
9
9
  // Needs to be mocked since userSupportedCurrencies is initially empty.
10
10
  jest.mock("../../../account/support");
@@ -77,6 +77,33 @@ describe("updateAmountUsingMax", () => {
77
77
  expect(setFromAmount.mock.calls[0][0]).toBe(ZERO);
78
78
  });
79
79
 
80
+ it("should toggle setIsSendMaxLoading", async () => {
81
+ const setIsSendMaxLoading = jest.fn();
82
+
83
+ const amount = new BigNumber(0.5);
84
+ mockedEstimateMaxSpendable.mockResolvedValue(amount);
85
+ const { result } = renderHook(useUpdateMaxAmount, {
86
+ initialProps: { ...defaultProps, setIsSendMaxLoading },
87
+ });
88
+
89
+ act(() => result.current.toggleMax());
90
+ expect(setIsSendMaxLoading).toBeCalledTimes(1);
91
+ expect(setIsSendMaxLoading).toBeCalledWith(true);
92
+ setIsSendMaxLoading.mockClear();
93
+
94
+ // Lest resort solution, since waitFor and other helpers will not work here.
95
+ await wait();
96
+
97
+ expect(setIsSendMaxLoading).toBeCalledTimes(1);
98
+ expect(setIsSendMaxLoading).toBeCalledWith(false);
99
+ setIsSendMaxLoading.mockClear();
100
+
101
+ act(() => result.current.toggleMax());
102
+ expect(setIsSendMaxLoading).toBeCalledTimes(1);
103
+ expect(setIsSendMaxLoading).toBeCalledWith(false);
104
+ setIsSendMaxLoading.mockClear();
105
+ });
106
+
80
107
  it("should update the max amount whenever the dependencies change", async () => {
81
108
  const { result, rerender } = renderHook(useUpdateMaxAmount, {
82
109
  initialProps: defaultProps,
@@ -1,13 +1,13 @@
1
+ import BigNumber from "bignumber.js";
1
2
  import { useCallback, useEffect, useState } from "react";
2
3
  import { getAccountBridge } from "../../../bridge";
3
-
4
+ import { Transaction } from "../../../generated/types";
4
5
  import {
5
- SwapTransactionType,
6
+ SetIsSendMaxLoading,
6
7
  SwapDataType,
7
8
  SwapSelectorStateType,
9
+ SwapTransactionType,
8
10
  } from "./useSwapTransaction";
9
- import { Transaction } from "../../../generated/types";
10
- import BigNumber from "bignumber.js";
11
11
 
12
12
  export const ZERO = new BigNumber(0);
13
13
 
@@ -17,27 +17,31 @@ export const useUpdateMaxAmount = ({
17
17
  parentAccount,
18
18
  transaction,
19
19
  feesStrategy,
20
+ setIsSendMaxLoading,
20
21
  }: {
21
22
  setFromAmount: SwapTransactionType["setFromAmount"];
22
23
  account: SwapSelectorStateType["account"];
23
24
  parentAccount: SwapSelectorStateType["parentAccount"];
24
25
  transaction: SwapTransactionType["transaction"];
25
26
  feesStrategy: Transaction["feesStrategy"];
27
+ setIsSendMaxLoading?: SetIsSendMaxLoading;
26
28
  }): {
27
29
  isMaxEnabled: SwapDataType["isMaxEnabled"];
28
30
  toggleMax: SwapTransactionType["toggleMax"];
29
31
  } => {
30
- const [isMaxEnabled, setMax] = useState<SwapDataType["isMaxEnabled"]>(false);
32
+ const [isMaxEnabled, setIsMaxEnabled] =
33
+ useState<SwapDataType["isMaxEnabled"]>(false);
31
34
 
32
35
  const toggleMax: SwapTransactionType["toggleMax"] = useCallback(
33
36
  () =>
34
- setMax((previous) => {
37
+ setIsMaxEnabled((previous) => {
35
38
  if (previous) {
36
39
  setFromAmount(ZERO);
40
+ setIsSendMaxLoading?.(false);
37
41
  }
38
42
  return !previous;
39
43
  }),
40
- [setFromAmount]
44
+ [setFromAmount, setIsSendMaxLoading]
41
45
  );
42
46
 
43
47
  /* UPDATE from amount to the estimate max spendable on account
@@ -47,11 +51,13 @@ export const useUpdateMaxAmount = ({
47
51
  const updateAmountUsingMax = async () => {
48
52
  if (!account) return;
49
53
  const bridge = getAccountBridge(account, parentAccount);
54
+ setIsSendMaxLoading?.(true);
50
55
  const amount = await bridge.estimateMaxSpendable({
51
56
  account,
52
57
  parentAccount,
53
58
  transaction,
54
59
  });
60
+ setIsSendMaxLoading?.(false);
55
61
  setFromAmount(amount);
56
62
  };
57
63