Add CoinGecko to CurrencyConversionManager

This commit is contained in:
Chris Eager
2025-01-19 08:28:53 -06:00
committed by Chris Eager
parent 3ceaa8bd20
commit 5cc76f48aa
12 changed files with 130 additions and 153 deletions

View File

@@ -10,7 +10,7 @@ import java.math.BigDecimal;
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 class StubPaymentsServiceClientsFactory implements PaymentsServiceClients
}
@Override
public CoinMarketCapClient buildCoinMarketCapClient(final HttpClient httpClient) {
return new StubCoinMarketCapClient();
public CoinGeckoClient buildCoinGeckoClient(final HttpClient httpClient) {
return new StubCoinGeckoClient();
}
/**
@@ -44,9 +44,9 @@ public class StubPaymentsServiceClientsFactory implements PaymentsServiceClients
/**
* 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);
}

View File

@@ -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<String, Map<String, BigDecimal>> 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<String, Map<String, BigDecimal>> 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"));
}
}

View File

@@ -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<String, CoinMarketCapClient.PriceConversionQuote> 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"));
}
}

View File

@@ -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 @@ class CurrencyConversionManagerTest {
@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 @@ class CurrencyConversionManagerTest {
"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 @@ class CurrencyConversionManagerTest {
@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 @@ class CurrencyConversionManagerTest {
@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 @@ class CurrencyConversionManagerTest {
}
@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 @@ class CurrencyConversionManagerTest {
@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 @@ class CurrencyConversionManagerTest {
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 @@ class CurrencyConversionManagerTest {
@Test
void convertToUsd() {
final CurrencyConversionManager currencyConversionManager = new CurrencyConversionManager(mock(FixerClient.class),
mock(CoinMarketCapClient.class),
mock(CoinGeckoClient.class),
mock(FaultTolerantRedisClusterClient.class),
Collections.emptyList(),
EXECUTOR,