From 5cc76f48aa4028f5001a51409a3a0e4e6ce2d7f2 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Sun, 19 Jan 2025 08:28:53 -0600 Subject: [PATCH] Add CoinGecko to CurrencyConversionManager --- service/config/sample-secrets-bundle.yml | 2 +- service/config/sample.yml | 6 +- .../textsecuregcm/WhisperServerService.java | 8 +-- .../PaymentsServiceClientsConfiguration.java | 10 +-- .../PaymentsServiceClientsFactory.java | 4 +- ...ketCapClient.java => CoinGeckoClient.java} | 48 +++++++-------- .../currency/CurrencyConversionManager.java | 44 ++++++------- .../StubPaymentsServiceClientsFactory.java | 10 +-- .../currency/CoinGeckoClientTest.java | 40 ++++++++++++ .../currency/CoinMarketCapClientTest.java | 61 ------------------- .../CurrencyConversionManagerTest.java | 48 +++++++-------- .../resources/config/test-secrets-bundle.yml | 2 +- 12 files changed, 130 insertions(+), 153 deletions(-) rename service/src/main/java/org/whispersystems/textsecuregcm/currency/{CoinMarketCapClient.java => CoinGeckoClient.java} (51%) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClientTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java diff --git a/service/config/sample-secrets-bundle.yml b/service/config/sample-secrets-bundle.yml index a5fe057cc..a0176db19 100644 --- a/service/config/sample-secrets-bundle.yml +++ b/service/config/sample-secrets-bundle.yml @@ -87,7 +87,7 @@ backupsZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijkl paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users paymentsService.fixerApiKey: unset -paymentsService.coinMarketCapApiKey: unset +paymentsService.coinGeckoApiKey: unset currentReportingKey.secret: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA= diff --git a/service/config/sample.yml b/service/config/sample.yml index 7e4af1533..dec908daf 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -327,9 +327,9 @@ paymentsService: - MOB externalClients: fixerApiKey: secret://paymentsService.fixerApiKey - coinMarketCapApiKey: secret://paymentsService.coinMarketCapApiKey - coinMarketCapCurrencyIds: - MOB: 7878 + coinGeckoApiKey: secret://paymentsService.coinGeckoApiKey + coinGeckoCurrencyIds: + MOB: mobilecoin badges: badges: diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 4a4b66ae3..47763080f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -133,7 +133,7 @@ import org.whispersystems.textsecuregcm.controllers.StickerController; import org.whispersystems.textsecuregcm.controllers.SubscriptionController; import org.whispersystems.textsecuregcm.controllers.VerificationController; -import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient; +import org.whispersystems.textsecuregcm.currency.CoinGeckoClient; import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; import org.whispersystems.textsecuregcm.currency.FixerClient; import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; @@ -698,9 +698,9 @@ public void run(WhisperServerConfiguration config, Environment environment) thro HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build(); FixerClient fixerClient = config.getPaymentsServiceConfiguration().externalClients() .buildFixerClient(currencyClient); - CoinMarketCapClient coinMarketCapClient = config.getPaymentsServiceConfiguration().externalClients() - .buildCoinMarketCapClient(currencyClient); - CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, + CoinGeckoClient coinGeckoClient = config.getPaymentsServiceConfiguration().externalClients() + .buildCoinGeckoClient(currencyClient); + CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinGeckoClient, cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), recurringJobExecutor, Clock.systemUTC()); VirtualThreadPinEventMonitor virtualThreadPinEventMonitor = new VirtualThreadPinEventMonitor( virtualThreadEventLoggerExecutor, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsConfiguration.java index 231609c9c..0c526b08b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsConfiguration.java @@ -12,13 +12,13 @@ import java.net.http.HttpClient; import java.util.Map; import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; -import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient; +import org.whispersystems.textsecuregcm.currency.CoinGeckoClient; import org.whispersystems.textsecuregcm.currency.FixerClient; @JsonTypeName("default") -public record PaymentsServiceClientsConfiguration(@NotNull SecretString coinMarketCapApiKey, +public record PaymentsServiceClientsConfiguration(@NotNull SecretString coinGeckoApiKey, @NotNull SecretString fixerApiKey, - @NotEmpty Map<@NotBlank String, Integer> coinMarketCapCurrencyIds) implements + @NotEmpty Map<@NotBlank String, String> coinGeckoCurrencyIds) implements PaymentsServiceClientsFactory { @Override @@ -27,7 +27,7 @@ public FixerClient buildFixerClient(final HttpClient httpClient) { } @Override - public CoinMarketCapClient buildCoinMarketCapClient(final HttpClient httpClient) { - return new CoinMarketCapClient(httpClient, coinMarketCapApiKey.value(), coinMarketCapCurrencyIds); + public CoinGeckoClient buildCoinGeckoClient(final HttpClient httpClient) { + return new CoinGeckoClient(httpClient, coinGeckoApiKey.value(), coinGeckoCurrencyIds); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsFactory.java index 734d92407..7c2077a27 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsFactory.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceClientsFactory.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.dropwizard.jackson.Discoverable; -import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient; +import org.whispersystems.textsecuregcm.currency.CoinGeckoClient; import org.whispersystems.textsecuregcm.currency.FixerClient; import java.net.http.HttpClient; @@ -16,5 +16,5 @@ public interface PaymentsServiceClientsFactory extends Discoverable { FixerClient buildFixerClient(final HttpClient httpClient); - CoinMarketCapClient buildCoinMarketCapClient(HttpClient httpClient); + CoinGeckoClient buildCoinGeckoClient(HttpClient httpClient); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClient.java similarity index 51% rename from service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java rename to service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClient.java index 9606e4d6a..c75fbaa21 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClient.java @@ -5,8 +5,8 @@ package org.whispersystems.textsecuregcm.currency; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.math.BigDecimal; @@ -14,26 +14,23 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.Locale; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.util.SystemMapper; -public class CoinMarketCapClient { +public class CoinGeckoClient { private final HttpClient httpClient; private final String apiKey; - private final Map currencyIdsBySymbol; + private final Map currencyIdsBySymbol; - private static final Logger logger = LoggerFactory.getLogger(CoinMarketCapClient.class); + private static final Logger logger = LoggerFactory.getLogger(CoinGeckoClient.class); - record CoinMarketCapResponse(@JsonProperty("data") PriceConversionResponse priceConversionResponse) {}; + private static final TypeReference>> RESPONSE_TYPE = new TypeReference<>() {}; - record PriceConversionResponse(int id, String symbol, Map quote) {}; - - record PriceConversionQuote(BigDecimal price) {}; - - public CoinMarketCapClient(final HttpClient httpClient, final String apiKey, final Map currencyIdsBySymbol) { + public CoinGeckoClient(final HttpClient httpClient, final String apiKey, final Map currencyIdsBySymbol) { this.httpClient = httpClient; this.apiKey = apiKey; this.currencyIdsBySymbol = currencyIdsBySymbol; @@ -45,40 +42,41 @@ public BigDecimal getSpotPrice(final String currency, final String base) throws } final URI quoteUri = URI.create( - String.format("https://pro-api.coinmarketcap.com/v2/tools/price-conversion?amount=1&id=%d&convert=%s", - currencyIdsBySymbol.get(currency), base)); + String.format("https://pro-api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=%s", + currencyIdsBySymbol.get(currency), base.toLowerCase(Locale.ROOT))); try { final HttpResponse response = httpClient.send(HttpRequest.newBuilder() - .GET() - .uri(quoteUri) - .header("X-CMC_PRO_API_KEY", apiKey) - .build(), - HttpResponse.BodyHandlers.ofString()); + .GET() + .uri(quoteUri) + .header("Accept", "application/json") + .header("x-cg-pro-api-key", apiKey) + .build(), + HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 200 || response.statusCode() >= 300) { - logger.warn("CoinMarketCapRequest failed with response: {}", response); - throw new IOException("CoinMarketCap request failed with status code " + response.statusCode()); + logger.warn("CoinGecko request failed with response: {}", response); + throw new IOException("CoinGecko request failed with status code " + response.statusCode()); } - return extractConversionRate(parseResponse(response.body()), base); + return extractConversionRate(parseResponse(response.body()).get(currencyIdsBySymbol.get(currency)), base.toLowerCase(Locale.ROOT)); } catch (final InterruptedException e) { throw new IOException("Interrupted while waiting for a response", e); } } @VisibleForTesting - static CoinMarketCapResponse parseResponse(final String responseJson) throws JsonProcessingException { - return SystemMapper.jsonMapper().readValue(responseJson, CoinMarketCapResponse.class); + static Map> parseResponse(final String responseJson) throws JsonProcessingException { + return SystemMapper.jsonMapper().readValue(responseJson, RESPONSE_TYPE); } @VisibleForTesting - static BigDecimal extractConversionRate(final CoinMarketCapResponse response, final String destinationCurrency) + static BigDecimal extractConversionRate(final Map response, final String destinationCurrency) throws IOException { - if (!response.priceConversionResponse().quote.containsKey(destinationCurrency)) { + if (!response.containsKey(destinationCurrency)) { throw new IOException("Response does not contain conversion rate for " + destinationCurrency); } - return response.priceConversionResponse().quote.get(destinationCurrency).price(); + return response.get(destinationCurrency); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java index d09603d7b..6602e03be 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java @@ -36,16 +36,16 @@ public class CurrencyConversionManager implements Managed { @VisibleForTesting static final Duration FIXER_REFRESH_INTERVAL = Duration.ofHours(2); - private static final Duration COIN_MARKET_CAP_REFRESH_INTERVAL = Duration.ofMinutes(5); + private static final Duration COIN_GECKO_CAP_REFRESH_INTERVAL = Duration.ofMinutes(5); @VisibleForTesting - static final String COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY = "CurrencyConversionManager::CoinMarketCapCacheCurrent"; + static final String COIN_GECKO_CAP_SHARED_CACHE_CURRENT_KEY = "CurrencyConversionManager::CoinGeckoCacheCurrent"; - private static final String COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY = "CurrencyConversionManager::CoinMarketCapCacheData"; + private static final String COIN_GECKO_SHARED_CACHE_DATA_KEY = "CurrencyConversionManager::CoinGeckoCacheData"; private final FixerClient fixerClient; - private final CoinMarketCapClient coinMarketCapClient; + private final CoinGeckoClient coinGeckoClient; private final FaultTolerantRedisClusterClient cacheCluster; @@ -61,18 +61,18 @@ public class CurrencyConversionManager implements Managed { private Map cachedFixerValues; - private Map cachedCoinMarketCapValues; + private Map cachedCoinGeckoValues; public CurrencyConversionManager( final FixerClient fixerClient, - final CoinMarketCapClient coinMarketCapClient, + final CoinGeckoClient coinGeckoClient, final FaultTolerantRedisClusterClient cacheCluster, final List currencies, final ScheduledExecutorService executor, final Clock clock) { this.fixerClient = fixerClient; - this.coinMarketCapClient = coinMarketCapClient; + this.coinGeckoClient = coinGeckoClient; this.cacheCluster = cacheCluster; this.currencies = currencies; this.executor = executor; @@ -102,49 +102,49 @@ void updateCacheIfNecessary() throws IOException { } { - final Map coinMarketCapValuesFromSharedCache = cacheCluster.withCluster(connection -> { + final Map CoinGeckoValuesFromSharedCache = cacheCluster.withCluster(connection -> { final Map parsedSharedCacheData = new HashMap<>(); - connection.sync().hgetall(COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY).forEach((currency, conversionRate) -> + connection.sync().hgetall(COIN_GECKO_SHARED_CACHE_DATA_KEY).forEach((currency, conversionRate) -> parsedSharedCacheData.put(currency, new BigDecimal(conversionRate))); return parsedSharedCacheData; }); - if (coinMarketCapValuesFromSharedCache != null && !coinMarketCapValuesFromSharedCache.isEmpty()) { - cachedCoinMarketCapValues = coinMarketCapValuesFromSharedCache; + if (CoinGeckoValuesFromSharedCache != null && !CoinGeckoValuesFromSharedCache.isEmpty()) { + cachedCoinGeckoValues = CoinGeckoValuesFromSharedCache; } } final boolean shouldUpdateSharedCache = cacheCluster.withCluster(connection -> - "OK".equals(connection.sync().set(COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY, + "OK".equals(connection.sync().set(COIN_GECKO_CAP_SHARED_CACHE_CURRENT_KEY, "true", - SetArgs.Builder.nx().ex(COIN_MARKET_CAP_REFRESH_INTERVAL)))); + SetArgs.Builder.nx().ex(COIN_GECKO_CAP_REFRESH_INTERVAL)))); - if (shouldUpdateSharedCache || cachedCoinMarketCapValues == null) { - final Map conversionRatesFromCoinMarketCap = new HashMap<>(currencies.size()); + if (shouldUpdateSharedCache || cachedCoinGeckoValues == null) { + final Map conversionRatesFromCoinGecko = new HashMap<>(currencies.size()); for (final String currency : currencies) { - conversionRatesFromCoinMarketCap.put(currency, coinMarketCapClient.getSpotPrice(currency, "USD")); + conversionRatesFromCoinGecko.put(currency, coinGeckoClient.getSpotPrice(currency, "USD")); } - cachedCoinMarketCapValues = conversionRatesFromCoinMarketCap; + cachedCoinGeckoValues = conversionRatesFromCoinGecko; if (shouldUpdateSharedCache) { cacheCluster.useCluster(connection -> { - final Map sharedCoinMarketCapValues = new HashMap<>(); + final Map sharedCoinGeckoValues = new HashMap<>(); - cachedCoinMarketCapValues.forEach((currency, conversionRate) -> - sharedCoinMarketCapValues.put(currency, conversionRate.toString())); + cachedCoinGeckoValues.forEach((currency, conversionRate) -> + sharedCoinGeckoValues.put(currency, conversionRate.toString())); - connection.sync().hset(COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY, sharedCoinMarketCapValues); + connection.sync().hset(COIN_GECKO_SHARED_CACHE_DATA_KEY, sharedCoinGeckoValues); }); } } List entities = new LinkedList<>(); - for (Map.Entry currency : cachedCoinMarketCapValues.entrySet()) { + for (Map.Entry currency : cachedCoinGeckoValues.entrySet()) { BigDecimal usdValue = stripTrailingZerosAfterDecimal(currency.getValue()); Map values = new HashMap<>(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java index 28d1423ba..ef5462fb2 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java @@ -10,7 +10,7 @@ import java.net.http.HttpClient; import java.util.Collections; import java.util.Map; -import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient; +import org.whispersystems.textsecuregcm.currency.CoinGeckoClient; import org.whispersystems.textsecuregcm.currency.FixerClient; @JsonTypeName("stub") @@ -22,8 +22,8 @@ public FixerClient buildFixerClient(final HttpClient httpClient) { } @Override - public CoinMarketCapClient buildCoinMarketCapClient(final HttpClient httpClient) { - return new StubCoinMarketCapClient(); + public CoinGeckoClient buildCoinGeckoClient(final HttpClient httpClient) { + return new StubCoinGeckoClient(); } /** @@ -44,9 +44,9 @@ public Map getConversionsForBase(final String base) throws F /** * Always returns {@code 0} for spot price checks */ - private static class StubCoinMarketCapClient extends CoinMarketCapClient { + private static class StubCoinGeckoClient extends CoinGeckoClient { - public StubCoinMarketCapClient() { + public StubCoinGeckoClient() { super(null, null, null); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClientTest.java new file mode 100644 index 000000000..f53d829bc --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinGeckoClientTest.java @@ -0,0 +1,40 @@ +package org.whispersystems.textsecuregcm.currency; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class CoinGeckoClientTest { + + private static final String RESPONSE_JSON = """ + { + "mobilecoin": { + "usd": 0.226212 + } + } + """; + + @Test + void parseResponse() throws JsonProcessingException { + final Map> parsedResponse = CoinGeckoClient.parseResponse(RESPONSE_JSON); + + assertTrue(parsedResponse.containsKey("mobilecoin")); + + assertEquals(1, parsedResponse.get("mobilecoin").size()); + assertEquals(new BigDecimal("0.226212"), parsedResponse.get("mobilecoin").get("usd")); + } + + @Test + void extractConversionRate() throws IOException { + final Map> parsedResponse = CoinGeckoClient.parseResponse(RESPONSE_JSON); + + assertEquals(new BigDecimal("0.226212"), CoinGeckoClient.extractConversionRate(parsedResponse.get("mobilecoin"), "usd")); + assertThrows(IOException.class, () -> CoinGeckoClient.extractConversionRate(parsedResponse.get("mobilecoin"), "CAD")); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java deleted file mode 100644 index 85f3c5c60..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.whispersystems.textsecuregcm.currency; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class CoinMarketCapClientTest { - - private static final String RESPONSE_JSON = """ - { - "status": { - "timestamp": "2022-11-09T17:15:06.356Z", - "error_code": 0, - "error_message": null, - "elapsed": 41, - "credit_count": 1, - "notice": null - }, - "data": { - "id": 7878, - "symbol": "MOB", - "name": "MobileCoin", - "amount": 1, - "last_updated": "2022-11-09T17:14:00.000Z", - "quote": { - "USD": { - "price": 0.6625319895827952, - "last_updated": "2022-11-09T17:14:00.000Z" - } - } - } - } - """; - - @Test - void parseResponse() throws JsonProcessingException { - final CoinMarketCapClient.CoinMarketCapResponse parsedResponse = CoinMarketCapClient.parseResponse(RESPONSE_JSON); - - assertEquals(7878, parsedResponse.priceConversionResponse().id()); - assertEquals("MOB", parsedResponse.priceConversionResponse().symbol()); - - final Map quote = - parsedResponse.priceConversionResponse().quote(); - - assertEquals(1, quote.size()); - assertEquals(new BigDecimal("0.6625319895827952"), quote.get("USD").price()); - } - - @Test - void extractConversionRate() throws IOException { - final CoinMarketCapClient.CoinMarketCapResponse parsedResponse = CoinMarketCapClient.parseResponse(RESPONSE_JSON); - - assertEquals(new BigDecimal("0.6625319895827952"), CoinMarketCapClient.extractConversionRate(parsedResponse, "USD")); - assertThrows(IOException.class, () -> CoinMarketCapClient.extractConversionRate(parsedResponse, "CAD")); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java index 220bc8775..126591491 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java @@ -38,16 +38,16 @@ class CurrencyConversionManagerTest { @Test void testCurrencyCalculations() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient coinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(coinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.822876"), "FJD", new BigDecimal("2.0577"), "FKP", new BigDecimal("0.743446") )); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, Clock.systemUTC()); manager.updateCacheIfNecessary(); @@ -66,9 +66,9 @@ void testCurrencyCalculations() throws IOException { @Test void testCurrencyCalculations_noTrailingZeros() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient CoinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("1.00000")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("1.00000")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.200000"), "FJD", new BigDecimal("3.00000"), @@ -76,7 +76,7 @@ void testCurrencyCalculations_noTrailingZeros() throws IOException { "CAD", new BigDecimal("700.000") )); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, CoinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, Clock.systemUTC()); manager.updateCacheIfNecessary(); @@ -96,16 +96,16 @@ void testCurrencyCalculations_noTrailingZeros() throws IOException { @Test void testCurrencyCalculations_accuracy() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient CoinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("0.999999")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("0.999999")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("1.000001"), "FJD", new BigDecimal("0.000001"), "FKP", new BigDecimal("1") )); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, CoinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, Clock.systemUTC()); manager.updateCacheIfNecessary(); @@ -125,21 +125,21 @@ void testCurrencyCalculations_accuracy() throws IOException { @Test void testCurrencyCalculationsTimeoutNoRun() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient CoinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.822876"), "FJD", new BigDecimal("2.0577"), "FKP", new BigDecimal("0.743446") )); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, CoinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, Clock.systemUTC()); manager.updateCacheIfNecessary(); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); manager.updateCacheIfNecessary(); @@ -155,26 +155,26 @@ void testCurrencyCalculationsTimeoutNoRun() throws IOException { } @Test - void testCurrencyCalculationsCoinMarketCapTimeoutWithRun() throws IOException { + void testCurrencyCalculationsCoinGeckoTimeoutWithRun() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient CoinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.822876"), "FJD", new BigDecimal("2.0577"), "FKP", new BigDecimal("0.743446") )); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, CoinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, Clock.systemUTC()); manager.updateCacheIfNecessary(); REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> - connection.sync().del(CurrencyConversionManager.COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY)); + connection.sync().del(CurrencyConversionManager.COIN_GECKO_CAP_SHARED_CACHE_CURRENT_KEY)); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); manager.updateCacheIfNecessary(); CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); @@ -192,9 +192,9 @@ void testCurrencyCalculationsCoinMarketCapTimeoutWithRun() throws IOException { @Test void testCurrencyCalculationsFixerTimeoutWithRun() throws IOException { FixerClient fixerClient = mock(FixerClient.class); - CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + CoinGeckoClient CoinGeckoClient = mock(CoinGeckoClient.class); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.822876"), "FJD", new BigDecimal("2.0577"), @@ -207,12 +207,12 @@ void testCurrencyCalculationsFixerTimeoutWithRun() throws IOException { when(clock.instant()).thenReturn(currentTime); when(clock.millis()).thenReturn(currentTime.toEpochMilli()); - CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, CoinGeckoClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), List.of("FOO"), EXECUTOR, clock); manager.updateCacheIfNecessary(); - when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + when(CoinGeckoClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( "EUR", new BigDecimal("0.922876"), "FJD", new BigDecimal("2.0577"), @@ -239,7 +239,7 @@ void testCurrencyCalculationsFixerTimeoutWithRun() throws IOException { @Test void convertToUsd() { final CurrencyConversionManager currencyConversionManager = new CurrencyConversionManager(mock(FixerClient.class), - mock(CoinMarketCapClient.class), + mock(CoinGeckoClient.class), mock(FaultTolerantRedisClusterClient.class), Collections.emptyList(), EXECUTOR, diff --git a/service/src/test/resources/config/test-secrets-bundle.yml b/service/src/test/resources/config/test-secrets-bundle.yml index 545cbf7c7..a68a3ec5f 100644 --- a/service/src/test/resources/config/test-secrets-bundle.yml +++ b/service/src/test/resources/config/test-secrets-bundle.yml @@ -157,7 +157,7 @@ callingZkConfig.serverSecret: AIZmPk8ms6TWBTGFcFE1iEuu4kSpTRL1EAPA2ZVWm4EIIF/N81 backupsZkConfig.serverSecret: AIZmPk8ms6TWBTGFcFE1iEuu4kSpTRL1EAPA2ZVWm4EIIF/N811ZhILbCx8QSLBf90mNXhUtsfNF5PY5UdnJMgBGu3AtrVs5erRXf5hi6RxvCkl1QnYs/tcuUGNbkejyR9bPR2uJaK6CxGJS0RRUDWf8f2hQloe/+kWKilM1I/MHSV2+PcyCDJIigPi9RhbD2STXc6cHEpYXReg+1OYSEQk3K2M0qnUoVOAjbPuFXANEPU+106f37w/iF6MhyfWyDCb+oit29DFtoDS31cxheB3x1KVga2ErfnIyHpQrSWYHUdGPZLXc0xRmaa0VwDyyXzK0o3w4oS/F9+xqWYUWkwgsAm9e7dP4l0qVolnPQ67uNj7BFG4JQ0vXxD/JJQ+5B4bHyK+v5ndJpRMXDC9rJw8ehopvDCTXSoICqN7nvY8Fyqhf5zkM880Su2XiBa2paDTVuZgwq07zBeDrrPc2zQ8A4neV6++t95veOfpp94FymnHJ8ILaznKqzJluGDdtCA== paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users paymentsService.fixerApiKey: unset -paymentsService.coinMarketCapApiKey: unset +paymentsService.coinGeckoApiKey: unset currentReportingKey.secret: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA=