@ledgerhq/live-countervalues-react 0.2.44-nightly.0 → 0.2.44-nightly.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.
package/src/index.tsx CHANGED
@@ -1,25 +1,14 @@
1
- import { BigNumber } from "bignumber.js";
2
- import React, {
3
- createContext,
4
- useMemo,
5
- useContext,
6
- useEffect,
7
- useReducer,
8
- useState,
9
- useCallback,
10
- ReactElement,
11
- } from "react";
12
1
  import { getAccountCurrency } from "@ledgerhq/coin-framework/account/helpers";
2
+ import api from "@ledgerhq/live-countervalues/api/index";
13
3
  import {
14
- initialState,
15
4
  calculate,
16
- loadCountervalues,
17
5
  exportCountervalues,
18
6
  importCountervalues,
19
7
  inferTrackingPairForAccounts,
8
+ initialState,
9
+ loadCountervalues,
20
10
  trackingPairForTopCoins,
21
11
  } from "@ledgerhq/live-countervalues/logic";
22
- import api from "@ledgerhq/live-countervalues/api/index";
23
12
  import type {
24
13
  CounterValuesState,
25
14
  CounterValuesStateRaw,
@@ -28,8 +17,28 @@ import type {
28
17
  } from "@ledgerhq/live-countervalues/types";
29
18
  import { useDebounce } from "@ledgerhq/live-hooks/useDebounce";
30
19
  import { log } from "@ledgerhq/logs";
31
- import type { Account, AccountLike } from "@ledgerhq/types-live";
32
20
  import type { Currency, Unit } from "@ledgerhq/types-cryptoassets";
21
+ import type { Account, AccountLike } from "@ledgerhq/types-live";
22
+ import { BigNumber } from "bignumber.js";
23
+ import React, {
24
+ ReactElement,
25
+ createContext,
26
+ useCallback,
27
+ useContext,
28
+ useEffect,
29
+ useMemo,
30
+ useReducer,
31
+ useState,
32
+ } from "react";
33
+
34
+ /** Bridge enabling platform-specific persistence of market-cap ids. */
35
+ export interface CountervaluesMarketcapBridge {
36
+ useIds(): string[];
37
+ useLastUpdated(): number | undefined;
38
+ setLoading(loading: boolean): void;
39
+ setIds(ids: string[]): void;
40
+ setError(message: string): void;
41
+ }
33
42
 
34
43
  // Polling is the control object you get from the high level <PollingConsumer>{ polling => ...
35
44
  export type Polling = {
@@ -48,6 +57,7 @@ export type Polling = {
48
57
  // if the last polling failed, there will be an error
49
58
  error: Error | null | undefined;
50
59
  };
60
+
51
61
  export type Props = {
52
62
  children: React.ReactNode;
53
63
  userSettings: CountervaluesSettings;
@@ -78,7 +88,6 @@ const CountervaluesUserSettingsContext = createContext<CountervaluesSettings>({
78
88
  });
79
89
 
80
90
  const CountervaluesContext = createContext<CounterValuesState>(initialState);
81
-
82
91
  const CountervaluesMarketcapIdsContext = createContext<string[]>([]);
83
92
 
84
93
  function trackingPairsHash(a: TrackingPair[]) {
@@ -90,80 +99,56 @@ function trackingPairsHash(a: TrackingPair[]) {
90
99
 
91
100
  const marketcapRefresh = 30 * 60000;
92
101
  const marketcapRefreshOnError = 60000;
93
- const initialIds: string[] = [];
94
- /**
95
- * Internal only. fetch the marketcap and keep it in sync.
96
- * the data is shared through a context, you can useMarketcapIds to get it.
97
- */
98
- function useMarketcap() {
99
- const [ids, setIds] = useState<string[]>(initialIds);
100
- const [fetchNonce, setFetchNonce] = useState(0);
102
+
103
+ /** Provides market-cap ids via the supplied bridge. */
104
+ export function CountervaluesMarketcapProvider({
105
+ children,
106
+ bridge,
107
+ }: {
108
+ children: React.ReactNode;
109
+ bridge: CountervaluesMarketcapBridge;
110
+ }): ReactElement {
111
+ const ids = bridge.useIds();
112
+ const lastUpdated = bridge.useLastUpdated();
113
+ const [, forceUpdate] = useReducer(x => x + 1, 0);
101
114
 
102
115
  useEffect(() => {
103
- let timeout: NodeJS.Timeout | null = null;
104
- api.fetchIdsSortedByMarketcap().then(
105
- ids => {
106
- setIds(ids);
107
- timeout = setTimeout(() => setFetchNonce(n => n + 1), marketcapRefresh);
108
- },
109
- error => {
110
- log("countervalues", "error fetching marketcap ids " + error);
111
- timeout = setTimeout(() => setFetchNonce(n => n + 1), marketcapRefreshOnError);
112
- },
113
- );
116
+ let timeout: ReturnType<typeof setTimeout> | null = null;
117
+ const now = Date.now();
118
+
119
+ if (!lastUpdated || now - lastUpdated > marketcapRefresh) {
120
+ bridge.setLoading(true);
121
+ api.fetchIdsSortedByMarketcap().then(
122
+ fetchedIds => {
123
+ bridge.setIds(fetchedIds);
124
+ timeout = setTimeout(() => forceUpdate(), marketcapRefresh);
125
+ },
126
+ error => {
127
+ log("countervalues", "error fetching marketcap ids " + error);
128
+ bridge.setError(error.message);
129
+ timeout = setTimeout(() => forceUpdate(), marketcapRefreshOnError);
130
+ },
131
+ );
132
+ } else {
133
+ timeout = setTimeout(() => forceUpdate(), marketcapRefresh - (now - lastUpdated));
134
+ }
135
+
114
136
  return () => {
115
- if (timeout) {
116
- clearTimeout(timeout);
117
- }
137
+ if (timeout) clearTimeout(timeout);
118
138
  };
119
- }, [fetchNonce]);
139
+ }, [lastUpdated, bridge]);
120
140
 
121
- return ids;
122
- }
123
-
124
- // infer the tracking pairs for the top coins that the portfolio needs to display itself
125
- // if startDate is undefined, the feature is disabled
126
- export function useTrackingPairsForTopCoins(
127
- marketcapIds: string[],
128
- countervalue: Currency,
129
- size: number,
130
- startDate: Date | undefined,
131
- ) {
132
- const dateTimestamp = startDate?.getTime();
133
- return useMemo(
134
- () =>
135
- dateTimestamp
136
- ? trackingPairForTopCoins(marketcapIds, size, countervalue, new Date(dateTimestamp))
137
- : [],
138
- [marketcapIds, countervalue, dateTimestamp, size],
139
- );
140
- }
141
-
142
- export function useTrackingPairForAccounts(
143
- accounts: Account[],
144
- countervalue: Currency,
145
- ): TrackingPair[] {
146
- // first we cache the tracking pairs with its hash
147
- const c = useMemo(() => {
148
- const pairs = inferTrackingPairForAccounts(accounts, countervalue);
149
- return { pairs, hash: trackingPairsHash(pairs) };
150
- }, [accounts, countervalue]);
151
- // we only want to return the pairs when the hash changes
152
- // to not recalculate pairs as fast as accounts resynchronizes
153
- // eslint-disable-next-line react-hooks/exhaustive-deps
154
- return useMemo(() => c.pairs, [c.hash]);
155
- }
156
-
157
- export function CountervaluesMarketcap({ children }: { children: React.ReactNode }): ReactElement {
158
- const marketcapIds = useMarketcap();
159
141
  return (
160
- <CountervaluesMarketcapIdsContext.Provider value={marketcapIds}>
142
+ <CountervaluesMarketcapIdsContext.Provider value={ids}>
161
143
  {children}
162
144
  </CountervaluesMarketcapIdsContext.Provider>
163
145
  );
164
146
  }
165
147
 
166
- export function Countervalues({
148
+ /**
149
+ * Root countervalues provider (polling + calculation).
150
+ */
151
+ export function CountervaluesProvider({
167
152
  children,
168
153
  userSettings,
169
154
  pollInitDelay = 3 * 1000,
@@ -174,18 +159,20 @@ export function Countervalues({
174
159
  const debouncedUserSettings = useDebounce(userSettings, debounceDelay);
175
160
  const [{ state, pending, error }, dispatch] = useReducer(fetchReducer, initialFetchState);
176
161
 
162
+ // TODO this is always using the initial value, doesn't react to changes.
177
163
  const marketcapIds = useContext(CountervaluesMarketcapIdsContext);
178
-
179
164
  const { marketCapBatchingAfterRank } = userSettings;
180
- const batchStrategySolver = useMemo(() => {
181
- return {
165
+
166
+ const batchStrategySolver = useMemo(
167
+ () => ({
182
168
  shouldBatchCurrencyFrom: (currency: Currency) => {
183
169
  if (currency.type === "FiatCurrency") return false;
184
170
  const i = marketcapIds.indexOf(currency.id);
185
171
  return i === -1 || i > marketCapBatchingAfterRank;
186
172
  },
187
- };
188
- }, [marketCapBatchingAfterRank, marketcapIds]);
173
+ }),
174
+ [marketCapBatchingAfterRank, marketcapIds],
175
+ );
189
176
 
190
177
  // flag used to trigger a loadCountervalues
191
178
  const [triggerLoad, setTriggerLoad] = useState(false);
@@ -198,28 +185,15 @@ export function Countervalues({
198
185
  useEffect(() => {
199
186
  if (pending || !triggerLoad) return;
200
187
  setTriggerLoad(false);
201
- dispatch({
202
- type: "pending",
203
- });
204
-
188
+ dispatch({ type: "pending" });
205
189
  loadCountervalues(
206
190
  state,
207
191
  userSettings,
208
192
  batchStrategySolver,
209
193
  userSettings.granularitiesRates,
210
194
  ).then(
211
- state => {
212
- dispatch({
213
- type: "success",
214
- payload: state,
215
- });
216
- },
217
- error => {
218
- dispatch({
219
- type: "error",
220
- payload: error,
221
- });
222
- },
195
+ newState => dispatch({ type: "success", payload: newState }),
196
+ e => dispatch({ type: "error", payload: e }),
223
197
  );
224
198
  }, [pending, state, userSettings, triggerLoad, batchStrategySolver]);
225
199
 
@@ -229,31 +203,25 @@ export function Countervalues({
229
203
  dispatch({
230
204
  type: "setCounterValueState",
231
205
  payload: importCountervalues(savedState, userSettings),
232
- }); // eslint-disable-next-line react-hooks/exhaustive-deps
233
- }, [savedState]);
206
+ });
207
+ }, [savedState, userSettings]);
234
208
 
235
209
  // manage the auto polling loop and the interface for user land to trigger a reload
236
210
  const [isPolling, setIsPolling] = useState(true);
237
211
  useEffect(() => {
238
212
  if (!isPolling) return;
239
- let pollingTimeout: NodeJS.Timeout;
240
-
213
+ let pollingTimeout: ReturnType<typeof setTimeout>;
241
214
  function pollingLoop() {
242
215
  setTriggerLoad(true);
243
216
  pollingTimeout = setTimeout(pollingLoop, autopollInterval);
244
217
  }
245
-
246
218
  pollingTimeout = setTimeout(pollingLoop, pollInitDelay);
247
219
  return () => clearTimeout(pollingTimeout);
248
220
  }, [autopollInterval, pollInitDelay, isPolling]);
249
221
 
250
222
  const polling = useMemo<Polling>(
251
223
  () => ({
252
- wipe: () => {
253
- dispatch({
254
- type: "wipe",
255
- });
256
- },
224
+ wipe: () => dispatch({ type: "wipe" }),
257
225
  poll: () => setTriggerLoad(true),
258
226
  start: () => setIsPolling(true),
259
227
  stop: () => setIsPolling(false),
@@ -273,65 +241,42 @@ export function Countervalues({
273
241
  }
274
242
 
275
243
  type Action =
276
- | {
277
- type: "success";
278
- payload: CounterValuesState;
279
- }
280
- | {
281
- type: "error";
282
- payload: Error;
283
- }
284
- | {
285
- type: "pending";
286
- }
287
- | {
288
- type: "wipe";
289
- }
290
- | {
291
- type: "setCounterValueState";
292
- payload: CounterValuesState;
293
- };
244
+ | { type: "success"; payload: CounterValuesState }
245
+ | { type: "error"; payload: Error }
246
+ | { type: "pending" }
247
+ | { type: "wipe" }
248
+ | { type: "setCounterValueState"; payload: CounterValuesState };
294
249
 
295
- type FetchState = {
296
- state: CounterValuesState;
297
- pending: boolean;
298
- error?: Error;
299
- };
300
- const initialFetchState: FetchState = {
301
- state: initialState,
302
- pending: false,
303
- };
250
+ type FetchState = { state: CounterValuesState; pending: boolean; error?: Error };
251
+ const initialFetchState: FetchState = { state: initialState, pending: false };
304
252
 
305
253
  function fetchReducer(state: FetchState, action: Action): FetchState {
306
254
  switch (action.type) {
307
255
  case "success":
308
- return {
309
- state: action.payload,
310
- pending: false,
311
- error: undefined,
312
- };
313
-
256
+ return { state: action.payload, pending: false, error: undefined };
314
257
  case "error":
315
258
  return { ...state, pending: false, error: action.payload };
316
-
317
259
  case "pending":
318
260
  return { ...state, pending: true, error: undefined };
319
-
320
261
  case "wipe":
321
- return {
322
- state: initialState,
323
- pending: false,
324
- error: undefined,
325
- };
326
-
262
+ return { state: initialState, pending: false, error: undefined };
327
263
  case "setCounterValueState":
328
264
  return { ...state, state: action.payload };
329
-
330
265
  default:
331
266
  return state;
332
267
  }
333
268
  }
334
269
 
270
+ /** Returns market-cap ids. */
271
+ export function useMarketcapIds(): string[] {
272
+ return useContext(CountervaluesMarketcapIdsContext);
273
+ }
274
+
275
+ /** Returns the full countervalues state. */
276
+ export function useCountervaluesState(): CounterValuesState {
277
+ return useContext(CountervaluesContext);
278
+ }
279
+
335
280
  // allows consumer to access the countervalues polling control object
336
281
  export function useCountervaluesPolling(): Polling {
337
282
  return useContext(CountervaluesPollingContext);
@@ -342,19 +287,9 @@ export function useCountervaluesUserSettingsContext(): CountervaluesSettings {
342
287
  return useContext(CountervaluesUserSettingsContext);
343
288
  }
344
289
 
345
- // allows consumer to access the countervalues state
346
- export function useCountervaluesState(): CounterValuesState {
347
- return useContext(CountervaluesContext);
348
- }
349
-
350
- // allows consumer to access the coins ids sorted by marketcap. It's basically all the coins that the API supports.
351
- export function useMarketcapIds(): string[] {
352
- return useContext(CountervaluesMarketcapIdsContext);
353
- }
354
-
355
290
  // provides an export of the countervalues state
356
291
  export function useCountervaluesExport(): CounterValuesStateRaw {
357
- const state = useContext(CountervaluesContext);
292
+ const state = useCountervaluesState();
358
293
  return useMemo(() => exportCountervalues(state), [state]);
359
294
  }
360
295
 
@@ -379,7 +314,7 @@ export function useCalculateCountervalueCallback({
379
314
  }): (from: Currency, value: BigNumber) => BigNumber | null | undefined {
380
315
  const state = useCountervaluesState();
381
316
  return useCallback(
382
- (from: Currency, value: BigNumber): BigNumber | null | undefined => {
317
+ (from: Currency, value: BigNumber) => {
383
318
  const countervalue = calculate(state, {
384
319
  value: value.toNumber(),
385
320
  from,
@@ -392,6 +327,7 @@ export function useCalculateCountervalueCallback({
392
327
  );
393
328
  }
394
329
 
330
+ /** Helper for send-flow: returns fiat amount and reverse calculation. */
395
331
  export function useSendAmount({
396
332
  account,
397
333
  fiatCurrency,
@@ -416,22 +352,49 @@ export function useSendAmount({
416
352
  const fiatUnit = fiatCurrency.units[0];
417
353
  const state = useCountervaluesState();
418
354
  const calculateCryptoAmount = useCallback(
419
- (fiatAmount: BigNumber) => {
420
- const cryptoAmount = new BigNumber(
355
+ (fiatAmount: BigNumber) =>
356
+ new BigNumber(
421
357
  calculate(state, {
422
358
  from: cryptoCurrency,
423
359
  to: fiatCurrency,
424
360
  value: fiatAmount.toNumber(),
425
361
  reverse: true,
426
362
  }) ?? 0,
427
- );
428
- return cryptoAmount;
429
- },
363
+ ),
430
364
  [state, cryptoCurrency, fiatCurrency],
431
365
  );
432
- return {
433
- fiatAmount,
434
- fiatUnit,
435
- calculateCryptoAmount,
436
- };
366
+ return { fiatAmount, fiatUnit, calculateCryptoAmount };
367
+ }
368
+
369
+ // infer the tracking pairs for the top coins that the portfolio needs to display itself
370
+ // if startDate is undefined, the feature is disabled
371
+ export function useTrackingPairsForTopCoins(
372
+ marketcapIds: string[],
373
+ countervalue: Currency,
374
+ size: number,
375
+ startDate: Date | undefined,
376
+ ) {
377
+ const dateTimestamp = startDate?.getTime();
378
+ return useMemo(
379
+ () =>
380
+ dateTimestamp
381
+ ? trackingPairForTopCoins(marketcapIds, size, countervalue, new Date(dateTimestamp))
382
+ : [],
383
+ [marketcapIds, countervalue, dateTimestamp, size],
384
+ );
385
+ }
386
+
387
+ export function useTrackingPairForAccounts(
388
+ accounts: Account[],
389
+ countervalue: Currency,
390
+ ): TrackingPair[] {
391
+ // first we cache the tracking pairs with its hash
392
+ const c = useMemo(() => {
393
+ const pairs = inferTrackingPairForAccounts(accounts, countervalue);
394
+ return { pairs, hash: trackingPairsHash(pairs) };
395
+ }, [accounts, countervalue]);
396
+ // we only want to return the pairs when the hash changes
397
+ // to not recalculate pairs as fast as accounts resynchronizes
398
+ // eslint-disable-next-line react-hooks/exhaustive-deps
399
+ return useMemo(() => c.pairs, [c.hash]);
437
400
  }