@ledgerhq/live-countervalues 0.1.0
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/.eslintrc.js +33 -0
- package/.turbo/turbo-build.log +4 -0
- package/.unimportedrc.json +4 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.txt +21 -0
- package/jest-global-setup.js +9 -0
- package/jest.config.js +14 -0
- package/lib/api/api.d.ts +4 -0
- package/lib/api/api.d.ts.map +1 -0
- package/lib/api/api.js +82 -0
- package/lib/api/api.js.map +1 -0
- package/lib/api/api.mock.d.ts +4 -0
- package/lib/api/api.mock.d.ts.map +1 -0
- package/lib/api/api.mock.js +103 -0
- package/lib/api/api.mock.js.map +1 -0
- package/lib/api/index.d.ts +4 -0
- package/lib/api/index.d.ts.map +1 -0
- package/lib/api/index.js +19 -0
- package/lib/api/index.js.map +1 -0
- package/lib/helpers.d.ts +31 -0
- package/lib/helpers.d.ts.map +1 -0
- package/lib/helpers.js +77 -0
- package/lib/helpers.js.map +1 -0
- package/lib/logic.d.ts +40 -0
- package/lib/logic.d.ts.map +1 -0
- package/lib/logic.integration.test.d.ts +2 -0
- package/lib/logic.integration.test.d.ts.map +1 -0
- package/lib/logic.integration.test.js +191 -0
- package/lib/logic.integration.test.js.map +1 -0
- package/lib/logic.js +422 -0
- package/lib/logic.js.map +1 -0
- package/lib/logic.test.d.ts +2 -0
- package/lib/logic.test.d.ts.map +1 -0
- package/lib/logic.test.js +29 -0
- package/lib/logic.test.js.map +1 -0
- package/lib/mock.d.ts +4 -0
- package/lib/mock.d.ts.map +1 -0
- package/lib/mock.js +10 -0
- package/lib/mock.js.map +1 -0
- package/lib/mock.test.d.ts +2 -0
- package/lib/mock.test.d.ts.map +1 -0
- package/lib/mock.test.js +210 -0
- package/lib/mock.test.js.map +1 -0
- package/lib/types.d.ts +47 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +4 -0
- package/lib/types.js.map +1 -0
- package/lib-es/api/api.d.ts +4 -0
- package/lib-es/api/api.d.ts.map +1 -0
- package/lib-es/api/api.js +77 -0
- package/lib-es/api/api.js.map +1 -0
- package/lib-es/api/api.mock.d.ts +4 -0
- package/lib-es/api/api.mock.d.ts.map +1 -0
- package/lib-es/api/api.mock.js +98 -0
- package/lib-es/api/api.mock.js.map +1 -0
- package/lib-es/api/index.d.ts +4 -0
- package/lib-es/api/index.d.ts.map +1 -0
- package/lib-es/api/index.js +14 -0
- package/lib-es/api/index.js.map +1 -0
- package/lib-es/helpers.d.ts +31 -0
- package/lib-es/helpers.d.ts.map +1 -0
- package/lib-es/helpers.js +67 -0
- package/lib-es/helpers.js.map +1 -0
- package/lib-es/logic.d.ts +40 -0
- package/lib-es/logic.d.ts.map +1 -0
- package/lib-es/logic.integration.test.d.ts +2 -0
- package/lib-es/logic.integration.test.d.ts.map +1 -0
- package/lib-es/logic.integration.test.js +186 -0
- package/lib-es/logic.integration.test.js.map +1 -0
- package/lib-es/logic.js +406 -0
- package/lib-es/logic.js.map +1 -0
- package/lib-es/logic.test.d.ts +2 -0
- package/lib-es/logic.test.d.ts.map +1 -0
- package/lib-es/logic.test.js +27 -0
- package/lib-es/logic.test.js.map +1 -0
- package/lib-es/mock.d.ts +4 -0
- package/lib-es/mock.d.ts.map +1 -0
- package/lib-es/mock.js +6 -0
- package/lib-es/mock.js.map +1 -0
- package/lib-es/mock.test.d.ts +2 -0
- package/lib-es/mock.test.d.ts.map +1 -0
- package/lib-es/mock.test.js +205 -0
- package/lib-es/mock.test.js.map +1 -0
- package/lib-es/types.d.ts +47 -0
- package/lib-es/types.d.ts.map +1 -0
- package/lib-es/types.js +3 -0
- package/lib-es/types.js.map +1 -0
- package/package.json +81 -0
- package/src/api/api.mock.ts +117 -0
- package/src/api/api.ts +79 -0
- package/src/api/index.ts +19 -0
- package/src/helpers.ts +82 -0
- package/src/logic.integration.test.ts +209 -0
- package/src/logic.test.ts +30 -0
- package/src/logic.ts +533 -0
- package/src/mock.test.ts +231 -0
- package/src/mock.ts +8 -0
- package/src/types.ts +70 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { CounterValuesAPI, RateGranularity } from "../types";
|
|
2
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
3
|
+
import { getBTCValues, BTCtoUSD, referenceSnapshotDate } from "../mock";
|
|
4
|
+
import { formatPerGranularity } from "../helpers";
|
|
5
|
+
import Prando from "prando";
|
|
6
|
+
import { findCurrencyByTicker } from "@ledgerhq/coin-framework/currencies/index";
|
|
7
|
+
|
|
8
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function btcTrend(t: number) {
|
|
11
|
+
const daysSinceGenesis = (t - 1230937200000) / DAY;
|
|
12
|
+
return Math.pow(daysSinceGenesis / 693, 5.526);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const randomCache: Record<string, number> = {};
|
|
16
|
+
|
|
17
|
+
function fromToRandom(id: string) {
|
|
18
|
+
if (randomCache[id]) return randomCache[id];
|
|
19
|
+
return (randomCache[id] = new Prando(getEnv("MOCK") + id).next());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function temporalFactor(from: string, to: string, maybeDate: Date | undefined) {
|
|
23
|
+
const t = (maybeDate || new Date()).getTime();
|
|
24
|
+
const r = fromToRandom(from); // make it varies between rates...
|
|
25
|
+
|
|
26
|
+
const wave1 = Math.cos(r * 0.5 + t / (200 * DAY * (0.5 + 0.5 * r)));
|
|
27
|
+
// long term wave
|
|
28
|
+
const wave2 = Math.sin(r + t / (30 * DAY)); // short term wave
|
|
29
|
+
|
|
30
|
+
const wave3 = // random market perturbation
|
|
31
|
+
Math.max(0, Math.sin(t / (66 * DAY))) *
|
|
32
|
+
Math.cos(wave2 + Math.cos(r) + t / (3 * DAY * (1 - 0.1 * r)));
|
|
33
|
+
|
|
34
|
+
// This is essentially randomness!
|
|
35
|
+
if (maybeDate && Math.cos(7 * r + t * 0.1) > 0.9 + 0.1 * r) {
|
|
36
|
+
return 0; // intentionally set a GAP into the data
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res =
|
|
40
|
+
(0.2 - 0.2 * r * r) * wave1 +
|
|
41
|
+
(0.1 + 0.05 * Math.sin(r)) * wave2 +
|
|
42
|
+
0.05 * wave3 +
|
|
43
|
+
btcTrend(t) / btcTrend(referenceSnapshotDate.getTime());
|
|
44
|
+
return Math.max(0, res);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rate(from: string, to: string, date?: Date): number | undefined {
|
|
48
|
+
const asBTC = getBTCValues()[from];
|
|
49
|
+
if (!asBTC) return;
|
|
50
|
+
|
|
51
|
+
if (to === "BTC") {
|
|
52
|
+
return asBTC * temporalFactor(from, to, date);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (to === "USD") {
|
|
56
|
+
return asBTC * BTCtoUSD * temporalFactor(from, to, date);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (from === "BTC") {
|
|
60
|
+
const r = rate(to, from, date);
|
|
61
|
+
if (!r) return;
|
|
62
|
+
return 1 / r;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const btcTO = rate("BTC", to, date);
|
|
66
|
+
|
|
67
|
+
if (btcTO) {
|
|
68
|
+
return asBTC * btcTO * temporalFactor(from, to, date);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const increment = {
|
|
73
|
+
daily: DAY,
|
|
74
|
+
hourly: 60 * 60 * 1000,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function getIds(): string[] {
|
|
78
|
+
const ids: string[] = [];
|
|
79
|
+
for (const k in getBTCValues()) {
|
|
80
|
+
const c = findCurrencyByTicker(k);
|
|
81
|
+
if (c && (c.type == "CryptoCurrency" || c.type == "TokenCurrency")) {
|
|
82
|
+
ids.push(c.id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ids;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getDates(granularity: RateGranularity, start: Date): Date[] {
|
|
89
|
+
const array: Date[] = [];
|
|
90
|
+
const f = formatPerGranularity[granularity];
|
|
91
|
+
const incr = increment[granularity];
|
|
92
|
+
const initial = new Date(f(start || new Date())).getTime();
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
|
|
95
|
+
for (let t = initial; t < now; t += incr) {
|
|
96
|
+
array.push(new Date(t));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return array;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const api: CounterValuesAPI = {
|
|
103
|
+
fetchHistorical: (granularity, { from, to, startDate }) => {
|
|
104
|
+
const r: Record<string, number> = {};
|
|
105
|
+
const f = formatPerGranularity[granularity];
|
|
106
|
+
getDates(granularity, startDate).forEach(date => {
|
|
107
|
+
const v = rate(from.ticker, to.ticker, date);
|
|
108
|
+
if (v) {
|
|
109
|
+
r[f(date)] = v;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return Promise.resolve(r);
|
|
113
|
+
},
|
|
114
|
+
fetchLatest: pairs => Promise.resolve(pairs.map(({ from, to }) => rate(from.ticker, to.ticker))),
|
|
115
|
+
fetchIdsSortedByMarketcap: () => Promise.resolve(getIds()),
|
|
116
|
+
};
|
|
117
|
+
export default api;
|
package/src/api/api.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import network from "@ledgerhq/live-network/network";
|
|
2
|
+
import URL from "url";
|
|
3
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
4
|
+
import { promiseAllBatched } from "@ledgerhq/live-promise";
|
|
5
|
+
import { formatPerGranularity, inferCurrencyAPIID } from "../helpers";
|
|
6
|
+
import type { CounterValuesAPI, TrackingPair } from "../types";
|
|
7
|
+
import type { Currency } from "@ledgerhq/types-cryptoassets";
|
|
8
|
+
|
|
9
|
+
const baseURL = () => getEnv("LEDGER_COUNTERVALUES_API");
|
|
10
|
+
|
|
11
|
+
const LATEST_CHUNK = 50;
|
|
12
|
+
|
|
13
|
+
const api: CounterValuesAPI = {
|
|
14
|
+
fetchHistorical: async (granularity, { from, to, startDate }) => {
|
|
15
|
+
const format = formatPerGranularity[granularity];
|
|
16
|
+
const url = URL.format({
|
|
17
|
+
pathname: `${baseURL()}/v3/historical/${granularity}/simple`,
|
|
18
|
+
query: {
|
|
19
|
+
from: inferCurrencyAPIID(from),
|
|
20
|
+
to: inferCurrencyAPIID(to),
|
|
21
|
+
start: format(startDate),
|
|
22
|
+
end: format(new Date()),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const { data } = await network({ method: "GET", url });
|
|
26
|
+
return data;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
fetchLatest: async (pairs: TrackingPair[]): Promise<number[]> => {
|
|
30
|
+
if (pairs.length === 0) return [];
|
|
31
|
+
|
|
32
|
+
// we group the pairs into chunks (of max LATEST_CHUNK) of the same "to" currency
|
|
33
|
+
// the batches preserve the ordering of "pairs" so the output returns the result in same orders
|
|
34
|
+
// we essentially assume that pairs' to's field are not changing / are sorted
|
|
35
|
+
const batches: Array<[Currency[], Currency]> = []; // array of [froms, to]
|
|
36
|
+
const first = pairs[0];
|
|
37
|
+
let batch: [Currency[], Currency] = [[first.from], first.to];
|
|
38
|
+
for (let i = 1; i < pairs.length; i++) {
|
|
39
|
+
const pair = pairs[i];
|
|
40
|
+
if (pair.to !== batch[1] || batch[0].length >= LATEST_CHUNK) {
|
|
41
|
+
batches.push(batch);
|
|
42
|
+
batch = [[pair.from], pair.to];
|
|
43
|
+
} else {
|
|
44
|
+
batch[0].push(pair.from);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
batches.push(batch);
|
|
48
|
+
|
|
49
|
+
const all = await promiseAllBatched(4, batches, async ([froms, to]): Promise<number[]> => {
|
|
50
|
+
const fromIds = froms.map(inferCurrencyAPIID);
|
|
51
|
+
const url = URL.format({
|
|
52
|
+
pathname: `${baseURL()}/v3/spot/simple`,
|
|
53
|
+
query: {
|
|
54
|
+
to: inferCurrencyAPIID(to),
|
|
55
|
+
froms: fromIds.join(","),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { data } = await network({ method: "GET", url });
|
|
60
|
+
|
|
61
|
+
// backend returns an object with keys being the froms
|
|
62
|
+
return fromIds.map(id => data[id] || 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const data = all.flat();
|
|
66
|
+
|
|
67
|
+
return data;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
fetchIdsSortedByMarketcap: async () => {
|
|
71
|
+
const { data } = await network({
|
|
72
|
+
method: "GET",
|
|
73
|
+
url: `${baseURL()}/v3/currencies/supported`,
|
|
74
|
+
});
|
|
75
|
+
return data;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default api;
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
2
|
+
import type { CounterValuesAPI } from "../types";
|
|
3
|
+
import prodAPI from "./api";
|
|
4
|
+
import mockAPI from "./api.mock";
|
|
5
|
+
|
|
6
|
+
const api: CounterValuesAPI = {
|
|
7
|
+
fetchHistorical: (granularity, pair) =>
|
|
8
|
+
getEnv("MOCK_COUNTERVALUES")
|
|
9
|
+
? mockAPI.fetchHistorical(granularity, pair)
|
|
10
|
+
: prodAPI.fetchHistorical(granularity, pair),
|
|
11
|
+
fetchLatest: pairs =>
|
|
12
|
+
getEnv("MOCK_COUNTERVALUES") ? mockAPI.fetchLatest(pairs) : prodAPI.fetchLatest(pairs),
|
|
13
|
+
fetchIdsSortedByMarketcap: () =>
|
|
14
|
+
getEnv("MOCK_COUNTERVALUES")
|
|
15
|
+
? mockAPI.fetchIdsSortedByMarketcap()
|
|
16
|
+
: prodAPI.fetchIdsSortedByMarketcap(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default api;
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Currency } from "@ledgerhq/types-cryptoassets";
|
|
2
|
+
import type { RateGranularity } from "./types";
|
|
3
|
+
|
|
4
|
+
export const inferCurrencyAPIID = (currency: Currency): string => {
|
|
5
|
+
switch (currency.type) {
|
|
6
|
+
case "FiatCurrency": {
|
|
7
|
+
return currency.ticker;
|
|
8
|
+
}
|
|
9
|
+
case "CryptoCurrency":
|
|
10
|
+
case "TokenCurrency": {
|
|
11
|
+
return currency.id;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const HOUR = 60 * 60 * 1000;
|
|
17
|
+
const DAY = 24 * HOUR;
|
|
18
|
+
|
|
19
|
+
export const incrementPerGranularity: Record<RateGranularity, number> = {
|
|
20
|
+
daily: DAY,
|
|
21
|
+
hourly: HOUR,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const datapointLimits: Record<RateGranularity, number> = {
|
|
25
|
+
daily: 9999 * DAY,
|
|
26
|
+
hourly: 7 * DAY, // we fetch at MOST a week of hourly. after that there are too much data...
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* efficient implementation of YYYY-MM-DD formatter
|
|
31
|
+
* @memberof countervalue
|
|
32
|
+
*/
|
|
33
|
+
export const formatCounterValueDay = (d: Date): string => d.toISOString().slice(0, 10);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* efficient implementation of YYYY-MM-DDTHH formatter
|
|
37
|
+
* @memberof countervalue
|
|
38
|
+
*/
|
|
39
|
+
export const formatCounterValueHour = (d: Date): string => d.toISOString().slice(0, 13);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* full version of formatCounterValue*
|
|
43
|
+
*/
|
|
44
|
+
export const formatCounterValueHashes = (d: Date): { iso: string; day: string; hour: string } => {
|
|
45
|
+
const iso = d.toISOString();
|
|
46
|
+
return {
|
|
47
|
+
iso,
|
|
48
|
+
day: iso.slice(0, 10),
|
|
49
|
+
hour: iso.slice(0, 13),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const parseFormattedDate = (str: string): Date => {
|
|
54
|
+
let full = str;
|
|
55
|
+
|
|
56
|
+
switch (str.length) {
|
|
57
|
+
case 10:
|
|
58
|
+
full += "T00:00";
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case 13:
|
|
62
|
+
full += ":00";
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
full += ":00.000Z";
|
|
67
|
+
return new Date(full);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const formatPerGranularity: Record<RateGranularity, (arg0: Date) => string> = {
|
|
71
|
+
daily: formatCounterValueDay,
|
|
72
|
+
hourly: formatCounterValueHour,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// a hash function used to identify a pair of currencies, internal use only (not used for the API)
|
|
76
|
+
export function pairId({ from, to }: { from: Currency; to: Currency }): string {
|
|
77
|
+
return `${inferCurrencyAPIID(to)} ${inferCurrencyAPIID(from)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function magFromTo(from: Currency, to: Currency): number {
|
|
81
|
+
return 10 ** (to.units[0].magnitude - from.units[0].magnitude);
|
|
82
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initialState,
|
|
3
|
+
loadCountervalues,
|
|
4
|
+
calculate,
|
|
5
|
+
exportCountervalues,
|
|
6
|
+
importCountervalues,
|
|
7
|
+
} from "./logic";
|
|
8
|
+
import { findCurrencyByTicker } from "@ledgerhq/coin-framework/currencies/index";
|
|
9
|
+
import {
|
|
10
|
+
getFiatCurrencyByTicker,
|
|
11
|
+
getCryptoCurrencyById,
|
|
12
|
+
getTokenById,
|
|
13
|
+
} from "@ledgerhq/cryptoassets";
|
|
14
|
+
import { getBTCValues } from "./mock";
|
|
15
|
+
import { Currency } from "@ledgerhq/types-cryptoassets";
|
|
16
|
+
import Prando from "prando";
|
|
17
|
+
import api from "./api";
|
|
18
|
+
import { setEnv } from "@ledgerhq/live-env";
|
|
19
|
+
|
|
20
|
+
const value = "ll-ci/0.0.0";
|
|
21
|
+
setEnv("LEDGER_CLIENT_VERSION", value);
|
|
22
|
+
|
|
23
|
+
const ethereum = getCryptoCurrencyById("ethereum");
|
|
24
|
+
const bitcoin = getCryptoCurrencyById("bitcoin");
|
|
25
|
+
const usd = getFiatCurrencyByTicker("USD");
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
|
|
28
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
jest.setTimeout(60000);
|
|
31
|
+
|
|
32
|
+
describe("API sanity", () => {
|
|
33
|
+
test("recent days have rate for BTC USD", async () => {
|
|
34
|
+
const state = await loadCountervalues(initialState, {
|
|
35
|
+
trackingPairs: [
|
|
36
|
+
{
|
|
37
|
+
from: bitcoin,
|
|
38
|
+
to: usd,
|
|
39
|
+
startDate: new Date(now - 200 * DAY),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
autofillGaps: false,
|
|
43
|
+
disableAutoRecoverErrors: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < 7; i++) {
|
|
47
|
+
const value = calculate(state, {
|
|
48
|
+
date: new Date(now - i * DAY),
|
|
49
|
+
disableRounding: true,
|
|
50
|
+
from: bitcoin,
|
|
51
|
+
to: usd,
|
|
52
|
+
value: 1000000,
|
|
53
|
+
});
|
|
54
|
+
expect(value).toBeDefined();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("recent days have different rates for BTC USD", async () => {
|
|
58
|
+
const state = await loadCountervalues(initialState, {
|
|
59
|
+
trackingPairs: [
|
|
60
|
+
{
|
|
61
|
+
from: bitcoin,
|
|
62
|
+
to: usd,
|
|
63
|
+
startDate: new Date(now - 200 * DAY),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
autofillGaps: true,
|
|
67
|
+
disableAutoRecoverErrors: true,
|
|
68
|
+
});
|
|
69
|
+
const currentValue = calculate(state, {
|
|
70
|
+
disableRounding: true,
|
|
71
|
+
from: bitcoin,
|
|
72
|
+
to: usd,
|
|
73
|
+
value: 1000000,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
for (let i = 1; i < 7; i++) {
|
|
77
|
+
const value = calculate(state, {
|
|
78
|
+
date: new Date(now - i * DAY),
|
|
79
|
+
disableRounding: true,
|
|
80
|
+
from: bitcoin,
|
|
81
|
+
to: usd,
|
|
82
|
+
value: 1000000,
|
|
83
|
+
});
|
|
84
|
+
expect(value).not.toEqual(currentValue);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("extreme cases", () => {
|
|
90
|
+
const universe = Object.keys(getBTCValues())
|
|
91
|
+
.filter(t => t !== "USD")
|
|
92
|
+
.map(findCurrencyByTicker)
|
|
93
|
+
.filter(Boolean) as Currency[];
|
|
94
|
+
universe.sort((a, b) => a.ticker.localeCompare(b.ticker));
|
|
95
|
+
const prando = new Prando("orderingrng");
|
|
96
|
+
const sampleCount = Math.min(120, universe.length);
|
|
97
|
+
const i = prando.nextInt(0, universe.length - sampleCount);
|
|
98
|
+
const currencies = universe.slice(i, i + sampleCount);
|
|
99
|
+
|
|
100
|
+
test("all tickers against USD", async () => {
|
|
101
|
+
const state = await loadCountervalues(initialState, {
|
|
102
|
+
trackingPairs: currencies.map(from => ({
|
|
103
|
+
from,
|
|
104
|
+
to: usd,
|
|
105
|
+
startDate: new Date(),
|
|
106
|
+
})),
|
|
107
|
+
autofillGaps: true,
|
|
108
|
+
disableAutoRecoverErrors: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const currenciesWithCVs = currencies
|
|
112
|
+
.map(from =>
|
|
113
|
+
calculate(state, {
|
|
114
|
+
date: new Date(),
|
|
115
|
+
from,
|
|
116
|
+
to: usd,
|
|
117
|
+
value: 1000000,
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
.filter(v => v && v > 0);
|
|
121
|
+
|
|
122
|
+
expect(currenciesWithCVs.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("all tickers against BTC", async () => {
|
|
126
|
+
const state = await loadCountervalues(initialState, {
|
|
127
|
+
trackingPairs: currencies.map(from => ({
|
|
128
|
+
from,
|
|
129
|
+
to: bitcoin,
|
|
130
|
+
startDate: new Date(),
|
|
131
|
+
})),
|
|
132
|
+
autofillGaps: true,
|
|
133
|
+
disableAutoRecoverErrors: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const currenciesWithCVs = currencies
|
|
137
|
+
.map(from =>
|
|
138
|
+
calculate(state, {
|
|
139
|
+
date: new Date(),
|
|
140
|
+
from,
|
|
141
|
+
to: bitcoin,
|
|
142
|
+
value: 1000000,
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
.filter(v => v && v > 0);
|
|
146
|
+
|
|
147
|
+
expect(currenciesWithCVs.length).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("WETH rules", () => {
|
|
152
|
+
// this test is created to confirm the recent removal of weth/eth specific management is still functional in v3 context
|
|
153
|
+
test("ethereum WETH have countervalues", async () => {
|
|
154
|
+
const weth = getTokenById("ethereum/erc20/weth");
|
|
155
|
+
const state = await loadCountervalues(initialState, {
|
|
156
|
+
trackingPairs: [
|
|
157
|
+
{
|
|
158
|
+
from: weth,
|
|
159
|
+
to: usd,
|
|
160
|
+
startDate: new Date(now - 10 * DAY),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
autofillGaps: true,
|
|
164
|
+
disableAutoRecoverErrors: true,
|
|
165
|
+
});
|
|
166
|
+
const value = calculate(state, {
|
|
167
|
+
disableRounding: true,
|
|
168
|
+
from: weth,
|
|
169
|
+
to: usd,
|
|
170
|
+
value: 1000000,
|
|
171
|
+
});
|
|
172
|
+
expect(value).toBeGreaterThan(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("fetchIdsSortedByMarketcap", async () => {
|
|
177
|
+
const ids = await api.fetchIdsSortedByMarketcap();
|
|
178
|
+
expect(ids).toContain("bitcoin");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("export and import it back", async () => {
|
|
182
|
+
const settings = {
|
|
183
|
+
trackingPairs: [
|
|
184
|
+
{
|
|
185
|
+
from: bitcoin,
|
|
186
|
+
to: usd,
|
|
187
|
+
startDate: new Date(now - 10 * DAY),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
from: ethereum,
|
|
191
|
+
to: usd,
|
|
192
|
+
startDate: new Date(now - 100 * DAY),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
autofillGaps: true,
|
|
196
|
+
disableAutoRecoverErrors: true,
|
|
197
|
+
};
|
|
198
|
+
const state = await loadCountervalues(initialState, settings);
|
|
199
|
+
const exported = exportCountervalues(state);
|
|
200
|
+
const imported = importCountervalues(exported, settings);
|
|
201
|
+
expect(imported).toEqual(state);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("API specific unit tests", () => {
|
|
205
|
+
test("fetchLatest with empty pairs", async () => {
|
|
206
|
+
const rates = await api.fetchLatest([]);
|
|
207
|
+
expect(rates).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { genAccount } from "@ledgerhq/coin-framework/mocks/account";
|
|
2
|
+
import { getCryptoCurrencyById, getFiatCurrencyByTicker } from "@ledgerhq/cryptoassets";
|
|
3
|
+
import { inferTrackingPairForAccounts } from "./logic";
|
|
4
|
+
|
|
5
|
+
describe("inferTrackingPairForAccounts", () => {
|
|
6
|
+
const accounts = Array(20)
|
|
7
|
+
.fill(null)
|
|
8
|
+
.map((_, i) => genAccount("test" + i));
|
|
9
|
+
const usd = getFiatCurrencyByTicker("USD");
|
|
10
|
+
|
|
11
|
+
test("trackingPairs have a deterministic order regardless of accounts order", () => {
|
|
12
|
+
const trackingPairs = inferTrackingPairForAccounts(accounts, usd);
|
|
13
|
+
const accounts2 = accounts.slice(10).concat(accounts.slice(0, 10));
|
|
14
|
+
const trackingPairs2 = inferTrackingPairForAccounts(accounts2, usd);
|
|
15
|
+
expect(trackingPairs).toEqual(trackingPairs2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("trackingPairs with same from and to are filtered out", () => {
|
|
19
|
+
const first = genAccount("test1", { currency: getCryptoCurrencyById("bitcoin") });
|
|
20
|
+
const trackingPairs = inferTrackingPairForAccounts([first], first.currency);
|
|
21
|
+
expect(trackingPairs).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("trackingPairs with 2 accounts of same coin yield one tracking pair", () => {
|
|
25
|
+
const first = genAccount("test1", { currency: getCryptoCurrencyById("bitcoin") });
|
|
26
|
+
const second = genAccount("test2", { currency: getCryptoCurrencyById("bitcoin") });
|
|
27
|
+
const trackingPairs = inferTrackingPairForAccounts([first, second], usd);
|
|
28
|
+
expect(trackingPairs.length).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
});
|