diff --git a/.changeset/beige-starfishes-guess.md b/.changeset/beige-starfishes-guess.md new file mode 100644 index 00000000000..ba39c08d754 --- /dev/null +++ b/.changeset/beige-starfishes-guess.md @@ -0,0 +1,12 @@ +--- +"@ledgerhq/cryptoassets": patch +"@ledgerhq/live-countervalues-react": patch +"ledger-live-desktop": patch +"live-mobile": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-countervalues": patch +"@ledgerhq/coin-framework": patch +"@ledgerhq/live-cli": patch +--- + +LL's preferred countervalues will now have all the possible fiats that our CVS api supports. diff --git a/.changeset/green-bags-hunt.md b/.changeset/green-bags-hunt.md new file mode 100644 index 00000000000..7296c4bbe42 --- /dev/null +++ b/.changeset/green-bags-hunt.md @@ -0,0 +1,8 @@ +--- +"@ledgerhq/live-countervalues-react": patch +"ledger-live-desktop": patch +"@ledgerhq/live-countervalues": patch +"@ledgerhq/coin-framework": patch +--- + +Countervalues API: upgrade to v3 for fetching supported fiats diff --git a/.changeset/swift-penguins-act.md b/.changeset/swift-penguins-act.md new file mode 100644 index 00000000000..2e1c7e161bf --- /dev/null +++ b/.changeset/swift-penguins-act.md @@ -0,0 +1,10 @@ +--- +"@ledgerhq/live-countervalues-react": patch +"ledger-live-desktop": patch +"live-mobile": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-countervalues": patch +"@ledgerhq/live-cli": patch +--- + +Countervalues performance evolutions. (8min -> 1min refresh rate, more efficient http calls caching,..) diff --git a/apps/cli/src/commands/blockchain/botTransfer.ts b/apps/cli/src/commands/blockchain/botTransfer.ts index 6bf42296290..7ed3d19c58d 100644 --- a/apps/cli/src/commands/blockchain/botTransfer.ts +++ b/apps/cli/src/commands/blockchain/botTransfer.ts @@ -163,6 +163,8 @@ export default { const countervaluesState = await loadCountervalues(initialState, { trackingPairs: inferTrackingPairForAccounts(accounts, countervalue), autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); await promiseAllBatched(CONCURRENT, accounts, async account => { diff --git a/apps/cli/src/commands/live/countervalues.ts b/apps/cli/src/commands/live/countervalues.ts index 68fae564fbf..ae0491c1e9e 100644 --- a/apps/cli/src/commands/live/countervalues.ts +++ b/apps/cli/src/commands/live/countervalues.ts @@ -170,6 +170,8 @@ export default { })), ), autofillGaps: !opts.disableAutofillGaps, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); // eslint-disable-next-line no-console if (opts.verbose) console.log(cvs); diff --git a/apps/cli/src/commands/live/portfolio.ts b/apps/cli/src/commands/live/portfolio.ts index 3e613e9b6b0..a0b0264074b 100644 --- a/apps/cli/src/commands/live/portfolio.ts +++ b/apps/cli/src/commands/live/portfolio.ts @@ -62,6 +62,8 @@ export default { loadCountervalues(initialState, { trackingPairs: inferTrackingPairForAccounts(accounts, countervalue as Currency), autofillGaps: !opts.disableAutofillGaps, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }), ).pipe( map(state => { diff --git a/apps/ledger-live-desktop/src/renderer/actions/general.ts b/apps/ledger-live-desktop/src/renderer/actions/general.ts index b222b564b37..2f86a5d51d5 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/general.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/general.ts @@ -26,6 +26,7 @@ import { import { resolveTrackingPairs } from "@ledgerhq/live-countervalues/logic"; import { useExtraSessionTrackingPair } from "./deprecated/ondemand-countervalues"; import { useMarketPerformanceTrackingPairs } from "./marketperformance"; +import { LiveConfig } from "@ledgerhq/live-config/LiveConfig"; // provide redux states via custom hook wrapper @@ -130,6 +131,10 @@ export function useCalculateCountervaluesUserSettings(): CountervaluesSettings { () => ({ trackingPairs, autofillGaps: true, + refreshRate: LiveConfig.getValueByKey("countervalues_refreshRate"), + marketCapBatchingAfterRank: LiveConfig.getValueByKey( + "countervalues_marketCapBatchingAfterRank", + ), }), [trackingPairs], ); diff --git a/apps/ledger-live-desktop/tests/specs/market/market.spec.ts b/apps/ledger-live-desktop/tests/specs/market/market.spec.ts index 22b58d73db4..cc8aea8ad63 100644 --- a/apps/ledger-live-desktop/tests/specs/market/market.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/market/market.spec.ts @@ -52,71 +52,71 @@ test("Market", async ({ page }) => { await expect.soft(page).toHaveScreenshot("market-page-no-scrollbar.png", maskItems); }); - await page.route(`${getEnv("LEDGER_COUNTERVALUES_API")}/v2/supported-to`, async route => { + await page.route(`${getEnv("LEDGER_COUNTERVALUES_API")}/v3/supported/fiat`, async route => { route.fulfill({ headers: { teststatus: "mocked" }, body: JSON.stringify([ - "aed", - "ars", - "aud", - "bch", - "bdt", - "bhd", - "bits", - "bmd", - "bnb", - "brl", - "btc", - "cad", - "chf", - "clp", - "cny", - "czk", - "dkk", - "dot", - "eos", - "eth", - "eur", - "gbp", - "hkd", - "huf", - "idr", - "ils", - "inr", - "jpy", - "krw", - "kwd", - "link", - "lkr", - "ltc", - "mmk", - "mxn", - "myr", - "ngn", - "nok", - "nzd", - "php", - "pkr", - "pln", - "rub", - "sar", - "sats", - "sek", - "sgd", - "thb", - "try", - "twd", - "uah", - "usd", - "vef", - "vnd", - "xag", - "xau", - "xdr", - "xlm", - "xrp", - "yfi", - "zar", + "AED", + "ARS", + "AUD", + "BCH", + "BDT", + "BHD", + "BITS", + "BMD", + "BNB", + "BRL", + "BTC", + "CAD", + "CHF", + "CLP", + "CNY", + "CZK", + "DKK", + "DOT", + "EOS", + "ETH", + "EUR", + "GBP", + "HKD", + "HUF", + "IDR", + "ILS", + "INR", + "JPY", + "KRW", + "KWD", + "LINK", + "LKR", + "LTC", + "MMK", + "MXN", + "MYR", + "NGN", + "NOK", + "NZD", + "PHP", + "PKR", + "PLN", + "RUB", + "SAR", + "SATS", + "SEK", + "SGD", + "THB", + "TRY", + "TWD", + "UAH", + "USD", + "VEF", + "VND", + "XAG", + "XAU", + "XDR", + "XLM", + "XRP", + "YFI", + "ZAR", ]), }); }); diff --git a/apps/ledger-live-mobile/src/actions/general.ts b/apps/ledger-live-mobile/src/actions/general.ts index a6aaa6193af..5f871f00f5e 100644 --- a/apps/ledger-live-mobile/src/actions/general.ts +++ b/apps/ledger-live-mobile/src/actions/general.ts @@ -18,6 +18,7 @@ import { accountsSelector } from "../reducers/accounts"; import { counterValueCurrencySelector, orderAccountsSelector } from "../reducers/settings"; import { clearBridgeCache } from "../bridge/cache"; import { flushAll } from "../components/DBSave"; +import { LiveConfig } from "@ledgerhq/live-config/LiveConfig"; const extraSessionTrackingPairsChanges: BehaviorSubject = new BehaviorSubject< TrackingPair[] @@ -102,6 +103,10 @@ export function useUserSettings() { () => ({ trackingPairs, autofillGaps: true, + refreshRate: LiveConfig.getValueByKey("countervalues_refreshRate"), + marketCapBatchingAfterRank: LiveConfig.getValueByKey( + "countervalues_marketCapBatchingAfterRank", + ), }), [trackingPairs], ); diff --git a/libs/coin-framework/src/currencies/support.ts b/libs/coin-framework/src/currencies/support.ts index 78e2aa93a10..8b23caa59c5 100644 --- a/libs/coin-framework/src/currencies/support.ts +++ b/libs/coin-framework/src/currencies/support.ts @@ -2,6 +2,7 @@ import { getFiatCurrencyByTicker, getCryptoCurrencyById, hasCryptoCurrencyId, + hasFiatCurrencyTicker, } from "@ledgerhq/cryptoassets"; import { CryptoCurrency, CryptoCurrencyId, FiatCurrency } from "@ledgerhq/types-cryptoassets"; import { getEnv } from "@ledgerhq/live-env"; @@ -56,23 +57,20 @@ async function initializeUserSupportedFiats() { let supportedTokens = [] as string[]; if (remoteSupportedTokens.length !== 0) { - remoteSupportedTokens.forEach(id => { - const token = id.toUpperCase(); - if (locallySupportedFiats.includes(token)) { + remoteSupportedTokens.forEach(token => { + if (hasFiatCurrencyTicker(token)) { supportedTokens.push(token); } }); } else { supportedTokens = locallySupportedFiats; } - userSupportedFiats = supportedTokens.map(id => { - return getFiatCurrencyByTicker(id); - }); + userSupportedFiats = supportedTokens.map(getFiatCurrencyByTicker); } export async function fetchSupportedFiatsTokens(): Promise { try { - const response = await fetch(`${getEnv("LEDGER_COUNTERVALUES_API")}/v2/supported-to`, { + const response = await fetch(`${getEnv("LEDGER_COUNTERVALUES_API")}/v3/supported/fiat`, { method: "GET", headers: { accept: "application/json", diff --git a/libs/ledger-live-common/src/__tests__/csvExport.ts b/libs/ledger-live-common/src/__tests__/csvExport.ts index d5768ee5dcd..308c956edbb 100644 --- a/libs/ledger-live-common/src/__tests__/csvExport.ts +++ b/libs/ledger-live-common/src/__tests__/csvExport.ts @@ -19,6 +19,8 @@ test("export CSV", async () => { startDate: new Date(Date.now() - 200 * 24 * 60 * 60 * 1000), })), autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); const accounts = currencies.map(currency => genAccount(`${currency.id}_export`, { diff --git a/libs/ledger-live-common/src/bot/index.ts b/libs/ledger-live-common/src/bot/index.ts index af07e2f38bc..0332dff88bf 100644 --- a/libs/ledger-live-common/src/bot/index.ts +++ b/libs/ledger-live-common/src/bot/index.ts @@ -198,6 +198,8 @@ export async function bot({ disabled, filter }: Arg = {}): Promise { const countervaluesState = await loadCountervalues(initialState, { trackingPairs: inferTrackingPairForAccounts(allAccountsAfter, usd), autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }).catch(e => { if (process.env.CI) console.error(e); countervaluesError = e; diff --git a/libs/ledger-live-common/src/config/sharedConfig.ts b/libs/ledger-live-common/src/config/sharedConfig.ts index 0ab124c99b0..5d865a9e81c 100644 --- a/libs/ledger-live-common/src/config/sharedConfig.ts +++ b/libs/ledger-live-common/src/config/sharedConfig.ts @@ -22,11 +22,23 @@ import { tronConfig } from "../families/tron/config"; import { vechainConfig } from "../families/vechain/config"; import { appConfig } from "../apps/config"; +const countervaluesConfig: ConfigSchema = { + countervalues_refreshRate: { + type: "number", + default: 60 * 1000, + }, + countervalues_marketCapBatchingAfterRank: { + type: "number", + default: 20, + }, +}; + const liveCommonConfig: ConfigSchema = { ...appConfig, }; export const liveConfig: ConfigSchema = { + ...countervaluesConfig, ...liveCommonConfig, ...algorandConfig, ...bitcoinConfig, diff --git a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap index 979244c8bd2..1541c472a3e 100644 --- a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap +++ b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap @@ -118,7 +118,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/beam", "ethereum/erc20/bora", "wanchain", - "ethereum/erc20/metalpay", "ethereum/erc20/rune", "bsc/bep20/inpulsex", "ethereum/erc20/acuteanglecoin", @@ -242,7 +241,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "kin", "ethereum/erc20/pinakion", "bsc/bep20/polis", - "ethereum/erc20/aurora", "ethereum/erc20/ampleforth", "ethereum/erc20/suterusu", "ethereum/erc20/hxro", @@ -302,7 +300,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/tellor_tributes", "ethereum/erc20/robotina_token", "qrl", - "ethereum/erc20/token_of_power", "ethereum/erc20/s4fe", "ethereum/erc20/persians", "ethereum/erc20/everycoin", @@ -753,7 +750,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/orb_wizz_council", "ethereum/erc20/syncfab_manufacturing", "ethereum/erc20/niobium", - "ethereum/erc20/bob", "ethereum/erc20/hashcoin", "ethereum/erc20/xceltoken_plus", "ethereum/erc20/adbank", @@ -808,7 +804,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/sentinel_chain", "ethereum/erc20/localcoinswap_cryptoshare", "bsc/bep20/qubit_token", - "ethereum/erc20/autonio_old", "ethereum/erc20/zilla", "ethereum/erc20/vodi_x", "ethereum/erc20/coinlancer", @@ -917,7 +912,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/datawallet", "ethereum/erc20/dether", "ethereum/erc20/max_token", - "ethereum/erc20/basis_dollar", "ethereum/erc20/jarvis_coins", "ethereum/erc20/rex", "ethereum/erc20/cryptocarbon", @@ -1460,7 +1454,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/decentralized_accessible_content_chain", "ethereum/erc20/tudatoken", "ethereum/erc20/btc_volatility_index", - "ethereum/erc20/wetoken", "ethereum/erc20/hunt_token", "ethereum/erc20/noiz", "ethereum/erc20/sonocoin", @@ -1526,7 +1519,6 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/gomics", "ethereum/erc20/oath_token", "ethereum/erc20/vera_e468", - "ethereum/erc20/knowyourdev", "ethereum/erc20/roboai_coin_r2r", "ethereum/erc20/asac_coin", "ethereum/erc20/fileshare", @@ -2116,6 +2108,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/aurei", "ethereum/erc20/aureus_nummus_gold", "ethereum/erc20/auric_network", + "ethereum/erc20/aurora", "ethereum/erc20/aurora(_near)", "ethereum/erc20/aurox_token", "ethereum/erc20/auruscoin_old", @@ -2134,6 +2127,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/autoglyphs", "ethereum/erc20/automata", "ethereum/erc20/autonio", + "ethereum/erc20/autonio_old", "ethereum/erc20/autonolas", "ethereum/erc20/autumn_token", "ethereum/erc20/autz_token", @@ -2271,6 +2265,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/base_protocol", "ethereum/erc20/basedpepe", "ethereum/erc20/basid", + "ethereum/erc20/basis_dollar", "ethereum/erc20/basis_dollar_share", "ethereum/erc20/basketcoin", "ethereum/erc20/basketdao_defi_index", @@ -2535,6 +2530,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/bns_finance", "ethereum/erc20/bns_token", "ethereum/erc20/bnt_smart_token_relay", + "ethereum/erc20/bob", "ethereum/erc20/bob_s_repair", "ethereum/erc20/bobacat", "ethereum/erc20/bobc", @@ -5070,6 +5066,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/klub_coin", "ethereum/erc20/kmcc", "ethereum/erc20/knit_finance", + "ethereum/erc20/knowyourdev", "ethereum/erc20/knoxstertoken_old", "ethereum/erc20/kodachi_token", "ethereum/erc20/koi_auction_token", @@ -5523,6 +5520,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/metagaming_guild", "ethereum/erc20/metahorse_unity_token", "ethereum/erc20/metal_blockchain_token", + "ethereum/erc20/metalpay", "ethereum/erc20/metalswap", "ethereum/erc20/metamonkeyai", "ethereum/erc20/metamundo_token", @@ -7691,6 +7689,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/tokemaksushilppool", "ethereum/erc20/tokemon", "ethereum/erc20/token_1337", + "ethereum/erc20/token_of_power", "ethereum/erc20/token_play", "ethereum/erc20/tokenasset", "ethereum/erc20/tokenbot", @@ -8216,6 +8215,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/werewolf_coin", "ethereum/erc20/wesingcoin", "ethereum/erc20/weth_yvault", + "ethereum/erc20/wetoken", "ethereum/erc20/weway_token", "ethereum/erc20/wewe", "ethereum/erc20/wexo", diff --git a/libs/ledgerjs/packages/cryptoassets/src/fiats.ts b/libs/ledgerjs/packages/cryptoassets/src/fiats.ts index 6e2c7e4543f..5086888a74d 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/fiats.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/fiats.ts @@ -22,54 +22,168 @@ function fiat(name, ticker, defaultSymbol, defaultMagnitude): FiatCurrency { const byTicker: Record = { AED: fiat("Emirati Dirham", "AED", "د.إ.", 2), + AFN: fiat("Afghan Afghani", "AFN", "؋", 2), + ALL: fiat("Albanian Lek", "ALL", "Lek", 2), + AMD: fiat("Armenian Dram", "AMD", "֏", 2), + ANG: fiat("Netherlands Antillean Guilder", "ANG", "ƒ", 2), + AOA: fiat("Angolan Kwanza", "AOA", "Kz", 2), + ARS: fiat("Argentine Peso", "ARS", "$", 2), AUD: fiat("Australian Dollar", "AUD", "AU$", 2), + AWG: fiat("Aruban Florin", "AWG", "ƒ", 2), + AZN: fiat("Azerbaijani Manat", "AZN", "₼", 2), + BAM: fiat("Bosnia-Herzegovina Convertible Mark", "BAM", "КМ", 2), + BBD: fiat("Barbadian Dollar", "BBD", "$", 2), + BDT: fiat("Bangladeshi Taka", "BDT", "৳", 0), BGN: fiat("Bulgarian Lev", "BGN", "лв.", 2), BHD: fiat("Bahraini Dinar", "BHD", "د.ب.", 3), + BIF: fiat("Burundian Franc", "BIF", "FBu", 0), + BMD: fiat("Bermudian Dollar", "BMD", "$", 2), + BND: fiat("Brunei Dollar", "BND", "$", 0), + BOB: fiat("Bolivian Boliviano", "BOB", "Bs", 2), BRL: fiat("Brazilian Real", "BRL", "R$", 2), + BSD: fiat("Bahamian Dollar", "BSD", "$", 2), + BTC: fiat("Bitcoin", "BTC", "Ƀ", 8), + BTN: fiat("Bhutanese Ngultrum", "BTN", "Nu.", 1), + BWP: fiat("Botswana Pula", "BWP", "P", 2), + BYN: fiat("Belarusian Ruble", "BYN", "р.", 2), + BYR: fiat("Belarusian Ruble (pre-2016)", "BYR", "р.", 2), + BZD: fiat("Belize Dollar", "BZD", "BZ$", 2), CAD: fiat("Canadian Dollar", "CAD", "CA$", 2), + CDF: fiat("Congolese Franc", "CDF", "FC", 2), CHF: fiat("Swiss Franc", "CHF", "CHF", 2), - CLP: fiat("Chilean Peso", "CLP", "CLP$", 2), - CNY: fiat("Yuan or Chinese Renminbi", "CNY", "¥", 2), + CLP: fiat("Chilean Peso", "CLP", "CLP$", 0), + CNY: fiat("Chinese Yuan Renminbi", "CNY", "¥", 2), + COP: fiat("Colombian Peso", "COP", "$", 2), CRC: fiat("Costa Rican Colón", "CRC", "₡", 2), + CUC: fiat("Cuban Convertible Peso", "CUC", "CUC", 2), + CUP: fiat("Cuban Peso", "CUP", "$MN", 2), + CVE: fiat("Cape Verdean Escudo", "CVE", "$", 2), CZK: fiat("Czech Koruna", "CZK", "Kč", 2), + DJF: fiat("Djiboutian Franc", "DJF", "Fdj", 0), DKK: fiat("Danish Krone", "DKK", "kr.", 2), + DOP: fiat("Dominican Peso", "DOP", "RD$", 2), + DZD: fiat("Algerian Dinar", "DZD", "د.ج.‏", 2), + EGP: fiat("Egyptian Pound", "EGP", "ج.م.‏", 2), + ERN: fiat("Eritrean Nakfa", "ERN", "Nfk", 2), + ETB: fiat("Ethiopian Birr", "ETB", "ETB", 2), EUR: fiat("Euro", "EUR", "€", 2), + FJD: fiat("Fijian Dollar", "FJD", "$", 2), + FKP: fiat("Falkland Islands Pound", "FKP", "£", 2), GBP: fiat("British Pound", "GBP", "£", 2), + GEL: fiat("Georgian Lari", "GEL", "GEL", 2), GHS: fiat("Ghanaian Cedi", "GHS", "₵", 2), + GIP: fiat("Gibraltar Pound", "GIP", "£", 2), + GMD: fiat("Gambian Dalasi", "GMD", "D", 2), + GNF: fiat("Guinean Franc", "GNF", "FG", 0), + GTQ: fiat("Guatemalan Quetzal", "GTQ", "Q", 2), + GYD: fiat("Guyanese Dollar", "GYD", "$", 2), HKD: fiat("Hong Kong Dollar", "HKD", "HK$", 2), + HNL: fiat("Honduran Lempira", "HNL", "L.", 2), HRK: fiat("Croatian Kuna", "HRK", "kn", 2), + HTG: fiat("Haitian Gourde", "HTG", "G", 2), HUF: fiat("Hungarian Forint", "HUF", "Ft", 2), IDR: fiat("Indonesian Rupiah", "IDR", "Rp", 0), ILS: fiat("Israeli Shekel", "ILS", "₪", 2), INR: fiat("Indian Rupee", "INR", "₹", 2), + IQD: fiat("Iraqi Dinar", "IQD", "د.ع.‏", 2), IRR: fiat("Iranian Rial", "IRR", "﷼", 2), + ISK: fiat("Iceland Krona", "ISK", "kr.", 0), + JMD: fiat("Jamaican Dollar", "JMD", "J$", 2), + JOD: fiat("Jordanian Dinar", "JOD", "د.ا.‏", 3), JPY: fiat("Japanese Yen", "JPY", "¥", 0), - KES: fiat("Kenyan Shilling", "KES", "S", 2), + KES: fiat("Kenyan Shilling", "KES", "KSh", 2), + KGS: fiat("Kyrgyzstani Som", "KGS", "сом", 2), KHR: fiat("Cambodian Riel", "KHR", "៛", 0), + KMF: fiat("Comorian Franc", "KMF", "CF", 2), + KPW: fiat("North Korean Won", "KPW", "₩", 0), KRW: fiat("South Korean Won", "KRW", "₩", 0), + KWD: fiat("Kuwaiti Dinar", "KWD", "د.ك.‏", 3), + KYD: fiat("Cayman Islands Dollar", "KYD", "$", 2), + KZT: fiat("Kazakhstani Tenge", "KZT", "₸", 2), + LAK: fiat("Lao Kip", "LAK", "₭", 0), + LBP: fiat("Lebanese Pound", "LBP", "ل.ل.‏", 2), + LKR: fiat("Sri Lankan Rupee", "LKR", "₨", 0), + LRD: fiat("Liberian Dollar", "LRD", "$", 2), + LSL: fiat("Lesotho Loti", "LSL", "M", 2), + LYD: fiat("Libyan Dinar", "LYD", "د.ل.‏", 3), + MAD: fiat("Moroccan Dirham", "MAD", "د.م.‏", 2), + MDL: fiat("Moldovan Leu", "MDL", "lei", 2), + MGA: fiat("Malagasy Ariary", "MGA", "Ar", 0), + MKD: fiat("Macedonian Denar", "MKD", "ден.", 2), + MMK: fiat("Myanmar Kyat", "MMK", "K", 2), + MNT: fiat("Mongolian Tugrik", "MNT", "₮", 2), + MOP: fiat("Macanese Pataca", "MOP", "MOP$", 2), + MRO: fiat("Mauritanian Ouguiya", "MRO", "UM", 2), + MTL: fiat("Maltese Lira", "MTL", "₤", 2), MUR: fiat("Mauritian Rupee", "MUR", "₨", 2), - MXN: fiat("Mexico Peso", "MXN", "Mex$", 2), + MVR: fiat("Maldivian Rufiyaa", "MVR", "MVR", 1), + MWK: fiat("Malawian Kwacha", "MWK", "MK", 2), + MXN: fiat("Mexican Peso", "MXN", "Mex$", 2), MYR: fiat("Malaysian Ringgit", "MYR", "RM", 2), + MZN: fiat("Mozambican Metical", "MZN", "MT", 0), + NAD: fiat("Namibian Dollar", "NAD", "$", 2), NGN: fiat("Nigerian Naira", "NGN", "₦", 2), + NIO: fiat("Nicaraguan Córdoba", "NIO", "C$", 2), NOK: fiat("Norwegian Krone", "NOK", "kr", 2), + NPR: fiat("Nepalese Rupee", "NPR", "₨", 2), NZD: fiat("New Zealand Dollar", "NZD", "NZ$", 2), + OMR: fiat("Omani Rial", "OMR", "﷼", 3), + PAB: fiat("Panamanian Balboa", "PAB", "B/.", 2), + PEN: fiat("Peruvian Sol", "PEN", "S/.", 2), + PGK: fiat("Papua New Guinean Kina", "PGK", "K", 2), PHP: fiat("Philippine Peso", "PHP", "₱", 2), PKR: fiat("Pakistani Rupee", "PKR", "₨", 2), PLN: fiat("Polish Złoty", "PLN", "zł", 2), - RON: fiat("Romanian Leu", "RON", "lei", 2), + PYG: fiat("Paraguayan Guarani", "PYG", "₲", 2), + QAR: fiat("Qatari Riyal", "QAR", "﷼", 2), + RON: fiat("Romanian Leu", "RON", "L", 2), + RSD: fiat("Serbian Dinar", "RSD", "Дин.", 2), RUB: fiat("Russian Rouble", "RUB", "₽", 2), + RWF: fiat("Rwandan Franc", "RWF", "RWF", 2), + SAR: fiat("Saudi Riyal", "SAR", "﷼", 2), + SBD: fiat("Solomon Islands Dollar", "SBD", "$", 2), + SCR: fiat("Seychellois Rupee", "SCR", "₨", 2), + SDD: fiat("Sudanese Dinar (1992-2007)", "SDD", "LSd", 2), + SDG: fiat("Sudanese Pound", "SDG", "£‏", 2), SEK: fiat("Swedish Krona", "SEK", "kr", 2), SGD: fiat("Singapore Dollar", "SGD", "S$", 2), + SHP: fiat("Saint Helena Pound", "SHP", "£", 2), + SLL: fiat("Sierra Leonean Leone", "SLL", "Le", 2), + SOS: fiat("Somali Shilling", "SOS", "S", 2), + SRD: fiat("Surinamese Dollar", "SRD", "$", 2), + STD: fiat("São Tomé and Príncipe Dobra", "STD", "Db", 2), + SVC: fiat("Salvadoran Colón", "SVC", "₡", 2), + SYP: fiat("Syrian Pound", "SYP", "£", 2), + SZL: fiat("Swazi Lilangeni", "SZL", "E", 2), THB: fiat("Thai Baht", "THB", "฿", 2), - TRY: fiat("Turkish Lira", "TRY", "TL", 2), + TJS: fiat("Tajikistani Somoni", "TJS", "TJS", 2), + TMT: fiat("Turkmenistani Manat", "TMT", "m", 0), + TND: fiat("Tunisian Dinar", "TND", "د.ت.‏", 3), + TOP: fiat("Tongan Pa'anga", "TOP", "T$", 2), + TRY: fiat("Turkish Lira", "TRY", "₺", 2), + TTD: fiat("Trinidad and Tobago Dollar", "TTD", "TT$", 2), + TVD: fiat("Tuvaluan Dollar", "TVD", "$", 2), + TWD: fiat("New Taiwan Dollar", "TWD", "NT$", 2), TZS: fiat("Tanzanian Shilling", "TZS", "TSh", 2), UAH: fiat("Ukrainian Hryvnia", "UAH", "₴", 2), UGX: fiat("Ugandan Shilling", "UGX", "USh", 2), USD: fiat("US Dollar", "USD", "$", 2), - VES: fiat("Venezuelan Bolivar", "VES", "Bs. S.", 2), - VND: fiat("Vietnamese Dong", "VND", "₫", 1), - VUV: fiat("Ni-Vanuatu Vatu", "VUV", "VT", 0), + UYU: fiat("Uruguayan Peso", "UYU", "$U", 2), + UZS: fiat("Uzbekistani Som", "UZS", "сўм", 2), + VEB: fiat("Venezuelan Bolívar (1871–2008)", "VEB", "Bs.", 2), + VEF: fiat("Venezuelan Bolívar (2008–2018)", "VEF", "Bs. F.", 2), + VND: fiat("Vietnamese Dong", "VND", "₫", 0), + VUV: fiat("Vanuatu Vatu", "VUV", "VT", 0), + WON: fiat("North Korean Won", "WON", "₩", 2), + WST: fiat("Samoan Tala", "WST", "WS$", 2), + XAF: fiat("Central African CFA Franc", "XAF", "F", 2), + XBT: fiat("Bitcoin", "XBT", "Ƀ", 2), + XCD: fiat("East Caribbean Dollar", "XCD", "$", 2), + XOF: fiat("West African CFA Franc", "XOF", "F", 2), + XPF: fiat("CFP Franc", "XPF", "F", 2), + YER: fiat("Yemeni Rial", "YER", "﷼", 2), ZAR: fiat("South African Rand", "ZAR", "R", 2), + ZMW: fiat("Zambian Kwacha", "ZMW", "ZK", 2), }; const list = Object.keys(byTicker).map(k => byTicker[k]); diff --git a/libs/live-countervalues-react/src/index.tsx b/libs/live-countervalues-react/src/index.tsx index 037b8fa9adb..2c57ad9b620 100644 --- a/libs/live-countervalues-react/src/index.tsx +++ b/libs/live-countervalues-react/src/index.tsx @@ -70,8 +70,11 @@ const CountervaluesPollingContext = createContext({ }); const CountervaluesUserSettingsContext = createContext({ + // dummy values that are overriden by the context provider trackingPairs: [], autofillGaps: true, + refreshRate: 0, + marketCapBatchingAfterRank: 0, }); const CountervaluesContext = createContext(initialState); @@ -164,13 +167,26 @@ export function Countervalues({ children, userSettings, pollInitDelay = 3 * 1000, - autopollInterval = 8 * 60 * 1000, debounceDelay = 1000, savedState, }: Props): ReactElement { + const autopollInterval = userSettings.refreshRate; const debouncedUserSettings = useDebounce(userSettings, debounceDelay); const [{ state, pending, error }, dispatch] = useReducer(fetchReducer, initialFetchState); + const marketcapIds = useContext(CountervaluesMarketcapIdsContext); + + const { marketCapBatchingAfterRank } = userSettings; + const batchStrategySolver = useMemo(() => { + return { + shouldBatchCurrencyFrom: (currency: Currency) => { + if (currency.type === "FiatCurrency") return false; + const i = marketcapIds.indexOf(currency.id); + return i === -1 || i > marketCapBatchingAfterRank; + }, + }; + }, [marketCapBatchingAfterRank, marketcapIds]); + // flag used to trigger a loadCountervalues const [triggerLoad, setTriggerLoad] = useState(false); // trigger poll only when userSettings changes. in a debounced way. @@ -186,7 +202,7 @@ export function Countervalues({ type: "pending", }); - loadCountervalues(state, userSettings).then( + loadCountervalues(state, userSettings, batchStrategySolver).then( state => { dispatch({ type: "success", @@ -200,7 +216,7 @@ export function Countervalues({ }); }, ); - }, [pending, state, userSettings, triggerLoad]); + }, [pending, state, userSettings, triggerLoad, batchStrategySolver]); // save the state when it changes useEffect(() => { diff --git a/libs/live-countervalues/src/api/api.ts b/libs/live-countervalues/src/api/api.ts index 2d81ec1124b..0fac5adff9d 100644 --- a/libs/live-countervalues/src/api/api.ts +++ b/libs/live-countervalues/src/api/api.ts @@ -2,8 +2,8 @@ import network from "@ledgerhq/live-network/network"; import URL from "url"; import { getEnv } from "@ledgerhq/live-env"; import { promiseAllBatched } from "@ledgerhq/live-promise"; -import { formatPerGranularity, inferCurrencyAPIID } from "../helpers"; -import type { CounterValuesAPI, TrackingPair } from "../types"; +import { formatPerGranularity, inferCurrencyAPIID, pairId } from "../helpers"; +import type { BatchStrategySolver, CounterValuesAPI, TrackingPair } from "../types"; import type { Currency } from "@ledgerhq/types-cryptoassets"; const baseURL = () => getEnv("LEDGER_COUNTERVALUES_API"); @@ -26,27 +26,43 @@ const api: CounterValuesAPI = { return data; }, - fetchLatest: async (pairs: TrackingPair[]): Promise => { + fetchLatest: async ( + pairs: TrackingPair[], + batchStrategySolver?: BatchStrategySolver, + ): Promise => { if (pairs.length === 0) return []; + const shouldBatchCurrencyFrom = batchStrategySolver?.shouldBatchCurrencyFrom || (() => true); + // we group the pairs into chunks (of max LATEST_CHUNK) of the same "to" currency // the batches preserve the ordering of "pairs" so the output returns the result in same orders // we essentially assume that pairs' to's field are not changing / are sorted const batches: Array<[Currency[], Currency]> = []; // array of [froms, to] const first = pairs[0]; let batch: [Currency[], Currency] = [[first.from], first.to]; + + // separately store all the pairs that can't be batched + const singles: Array<[Currency[], Currency]> = []; + for (let i = 1; i < pairs.length; i++) { const pair = pairs[i]; - if (pair.to !== batch[1] || batch[0].length >= LATEST_CHUNK) { - batches.push(batch); - batch = [[pair.from], pair.to]; + const inBatch = shouldBatchCurrencyFrom(pair.from); + if (!inBatch) { + singles.push([[pair.from], pair.to]); } else { - batch[0].push(pair.from); + if (pair.to !== batch[1] || batch[0].length >= LATEST_CHUNK) { + batches.push(batch); + batch = [[pair.from], pair.to]; + } else { + batch[0].push(pair.from); + } } } batches.push(batch); + const allBatches = batches.concat(singles); - const all = await promiseAllBatched(4, batches, async ([froms, to]): Promise => { + const map = new Map(); + await promiseAllBatched(4, allBatches, async ([froms, to]) => { const fromIds = froms.map(inferCurrencyAPIID); const url = URL.format({ pathname: `${baseURL()}/v3/spot/simple`, @@ -58,11 +74,16 @@ const api: CounterValuesAPI = { const { data } = await network({ method: "GET", url }); - // backend returns an object with keys being the froms - return fromIds.map(id => data[id] || 0); + // store all countervalues in a global map + for (let i = 0; i < fromIds.length; i++) { + const from = froms[i]; + const value = data[fromIds[i]]; + map.set(pairId({ from, to }), value); + } }); - const data = all.flat(); + // we return the result in the same order as the input pairs + const data = pairs.map(pair => map.get(pairId(pair)) || 0); return data; }, @@ -70,7 +91,7 @@ const api: CounterValuesAPI = { fetchIdsSortedByMarketcap: async () => { const { data } = await network({ method: "GET", - url: `${baseURL()}/v3/currencies/supported`, + url: `${baseURL()}/v3/supported/crypto`, }); return data; }, diff --git a/libs/live-countervalues/src/api/index.ts b/libs/live-countervalues/src/api/index.ts index 288f44f365e..96c918bcc6f 100644 --- a/libs/live-countervalues/src/api/index.ts +++ b/libs/live-countervalues/src/api/index.ts @@ -8,8 +8,10 @@ const api: CounterValuesAPI = { getEnv("MOCK_COUNTERVALUES") ? mockAPI.fetchHistorical(granularity, pair) : prodAPI.fetchHistorical(granularity, pair), - fetchLatest: pairs => - getEnv("MOCK_COUNTERVALUES") ? mockAPI.fetchLatest(pairs) : prodAPI.fetchLatest(pairs), + fetchLatest: (pairs, batchStrategySolver) => + getEnv("MOCK_COUNTERVALUES") + ? mockAPI.fetchLatest(pairs, batchStrategySolver) + : prodAPI.fetchLatest(pairs, batchStrategySolver), fetchIdsSortedByMarketcap: () => getEnv("MOCK_COUNTERVALUES") ? mockAPI.fetchIdsSortedByMarketcap() diff --git a/libs/live-countervalues/src/logic.integration.test.ts b/libs/live-countervalues/src/logic.integration.test.ts index 696fc5d167b..16c7167144c 100644 --- a/libs/live-countervalues/src/logic.integration.test.ts +++ b/libs/live-countervalues/src/logic.integration.test.ts @@ -40,6 +40,8 @@ describe("API sanity", () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }); @@ -64,6 +66,8 @@ describe("API sanity", () => { }, ], autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }); const currentValue = calculate(state, { @@ -105,6 +109,8 @@ describe("extreme cases", () => { startDate: new Date(), })), autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }); @@ -130,6 +136,8 @@ describe("extreme cases", () => { startDate: new Date(), })), autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }); @@ -161,6 +169,8 @@ describe("WETH rules", () => { }, ], autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }); const value = calculate(state, { @@ -193,6 +203,8 @@ test("export and import it back", async () => { }, ], autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, disableAutoRecoverErrors: true, }; const state = await loadCountervalues(initialState, settings); diff --git a/libs/live-countervalues/src/logic.ts b/libs/live-countervalues/src/logic.ts index da278ae3b4d..e6c18a36726 100644 --- a/libs/live-countervalues/src/logic.ts +++ b/libs/live-countervalues/src/logic.ts @@ -14,6 +14,7 @@ import type { RateGranularity, PairRateMapCache, RateMapRaw, + BatchStrategySolver, } from "./types"; import { pairId, @@ -145,6 +146,7 @@ const MAX_RETRY_DELAY = 7 * incrementPerGranularity.daily; export async function loadCountervalues( state: CounterValuesState, settings: CountervaluesSettings, + batchStrategySolver?: BatchStrategySolver, ): Promise { const data = { ...state.data }; const cache = { ...state.cache }; @@ -164,14 +166,10 @@ export async function loadCountervalues( rateGranularities.forEach((granularity: RateGranularity) => { const format = formatPerGranularity[granularity]; const earliestHisto = format(nowDate); - log("countervalues", "earliestHisto=" + earliestHisto); const limit = datapointLimits[granularity]; settings.trackingPairs.forEach(({ from, to, startDate }) => { - const key = pairId({ - from, - to, - }); + const key = pairId({ from, to }); const c: PairRateMapCache | null | undefined = cache[key]; const stats = c?.stats; @@ -288,7 +286,7 @@ export async function loadCountervalues( }), ), api - .fetchLatest(latestToFetch) + .fetchLatest(latestToFetch, batchStrategySolver) .then(rates => { const out: Record = {}; let hasData = false; diff --git a/libs/live-countervalues/src/mock.test.ts b/libs/live-countervalues/src/mock.test.ts index c177e5ad7da..175ebdfa61c 100644 --- a/libs/live-countervalues/src/mock.test.ts +++ b/libs/live-countervalues/src/mock.test.ts @@ -25,6 +25,8 @@ test("mock load with nothing to track", async () => { const state = await loadCountervalues(initialState, { trackingPairs: [], autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -48,6 +50,8 @@ test("mock load with btc-usd to track", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -97,6 +101,8 @@ test("mock load with eth-btc to track", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -117,6 +123,8 @@ test("mock load with btc-eth to track", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -137,6 +145,8 @@ test("DAI EUR latest price", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -157,6 +167,8 @@ test("calculate(now()) is calculate(null)", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -184,6 +196,8 @@ test("missing rate in mock", async () => { }, ], autofillGaps: false, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); expect( @@ -205,6 +219,8 @@ test("missing rate in mock is filled by autofillGaps", async () => { }, ], autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); expect(state).toBeDefined(); // shifting rates to the right. hole in data looks up in older datapoint diff --git a/libs/live-countervalues/src/portfolio.test.ts b/libs/live-countervalues/src/portfolio.test.ts index 1a480fd4064..ae87521c128 100644 --- a/libs/live-countervalues/src/portfolio.test.ts +++ b/libs/live-countervalues/src/portfolio.test.ts @@ -315,6 +315,8 @@ async function loadCV(a: Account | Account[], cvTicker = "USD") { const state = await loadCountervalues(initialState, { trackingPairs: inferTrackingPairForAccounts(accounts, to), autofillGaps: true, + refreshRate: 60000, + marketCapBatchingAfterRank: 20, }); return { state, diff --git a/libs/live-countervalues/src/types.ts b/libs/live-countervalues/src/types.ts index 2a4f3c483b4..54a22c0aa36 100644 --- a/libs/live-countervalues/src/types.ts +++ b/libs/live-countervalues/src/types.ts @@ -4,8 +4,14 @@ import type { Currency } from "@ledgerhq/types-cryptoassets"; // we generally will just infer it from Accounts export type CountervaluesSettings = { + // the list of pairs we need to load trackingPairs: TrackingPair[]; + // if true, we will autofill gaps in the data (a missing datapoint between two day). This is typically needed to have smooth graphs. autofillGaps: boolean; + // preference that define the general refresh rate of countervalues (how often loadCountervalues loop is recalled) + refreshRate: number; + // in the hybrid batching strategy implementation of latest fetching, defines after which rank we start to batch + marketCapBatchingAfterRank: number; // throw exception in "loadCountervalues" if ANY error occurs (for test purpose) disableAutoRecoverErrors?: boolean; }; @@ -34,12 +40,20 @@ export type TrackingPair = { to: Currency; startDate: Date; }; + +export type BatchStrategySolver = { + shouldBatchCurrencyFrom: (from: Currency) => boolean; +}; + export type CounterValuesAPI = { fetchHistorical: ( granularity: RateGranularity, pair: TrackingPair, ) => Promise>; - fetchLatest: (pairs: TrackingPair[]) => Promise>; + fetchLatest: ( + pairs: TrackingPair[], + batchStrategySolver?: BatchStrategySolver, + ) => Promise>; fetchIdsSortedByMarketcap: () => Promise; }; export type CounterValuesStatus = Record<