Skip to content

Add CoinbaseRateProvider implementation to address issue #3 #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.javamoney.shelter.bitcoin.provider;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.javamoney.moneta.convert.ExchangeRateBuilder;
import org.javamoney.moneta.spi.AbstractRateProvider;
import org.javamoney.moneta.spi.DefaultNumberValue;

import javax.money.CurrencyUnit;
import javax.money.convert.*;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Currency;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

public class CoinbaseRateProvider extends AbstractRateProvider {
private static final RateType RATE_TYPE = RateType.DEFERRED;
private static final ProviderContext CONTEXT = ProviderContextBuilder.of("CoinbaseRateProvider", RATE_TYPE)
.set("providerDescription", "Coinbase - Bitcoin exchange rate provider")
.build();
private static final String DEFAULT_BASE_CURRENCY = "BTC";

private final List<String> supportedCurrencies = new ArrayList<>();
private final Map<String, Number> rates = new ConcurrentHashMap<>();

private final Logger log = Logger.getLogger(getClass().getName());

public CoinbaseRateProvider() {
super(CONTEXT);
loadSupportedCurrencies();
}

@Override
public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
CurrencyUnit baseCurrency = conversionQuery.getBaseCurrency();
CurrencyUnit termCurrency = conversionQuery.getCurrency();
ConversionContext conversionContext = ConversionContext.of(getContext().getProviderName(), RATE_TYPE);

if (!DEFAULT_BASE_CURRENCY.equals(baseCurrency.getCurrencyCode())) {
throw new CurrencyConversionException(baseCurrency, termCurrency, conversionContext, "Base currency not supported: " + baseCurrency);
}

if (!supportedCurrencies.contains(termCurrency.getCurrencyCode())) {
throw new CurrencyConversionException(baseCurrency, termCurrency, conversionContext, "Term currency not supported: " + termCurrency);
}

loadRates();

Number rate = rates.get(termCurrency.getCurrencyCode());
if (rate == null) {
throw new CurrencyConversionException(baseCurrency, termCurrency, conversionContext, "Rate not available for currency: " + termCurrency);
}
return new ExchangeRateBuilder(conversionContext)
.setBase(baseCurrency)
.setTerm(termCurrency)
.setFactor(DefaultNumberValue.of(rate))
.build();
}

private void loadSupportedCurrencies() {
try {
HttpClient httpClient = HttpClient.newHttpClient();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably check if the httpClient is an instance of AutoCloseable and if so #close() it.See JDK-8304165

Copy link
Author

@sernamar sernamar Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback! After your suggestion, I considered using a try-with-resources construct to close the connection. However, while reviewing this issue, I realized that java.net.http.HttpClient is not supported in JDK 8, which is the version we’re using for this project.

Rather than upgrading the JDK version, we can switch to Apache HttpClient, which is compatible with JDK 8 and provides similar functionality. CloseableHttpClient in Apache HttpClient implements AutoCloseable, allowing us to use the try-with-resources construct for proper resource management.

I've added a new commit with these changes.

String url = "https://api.coinbase.com/v2/currencies";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(response.body());
JsonNode dataNode = jsonNode.get("data");
dataNode.forEach(node -> supportedCurrencies.add(node.get("id").asText()));
} catch (IOException | InterruptedException e) {
log.severe("Failed to load supported currencies from Coinbase API: " + e.getMessage());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code throws a MonetaryException, it may be worth considering keeping this behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I've kept the MonetaryException behavior as suggested.

}
}

private void loadRates() {
try {
HttpClient httpClient = HttpClient.newHttpClient();
String url = "https://api.coinbase.com/v2/exchange-rates?currency=" + DEFAULT_BASE_CURRENCY;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(response.body());
JsonNode ratesNode = jsonNode.get("data").get("rates");
ratesNode.fields().forEachRemaining(entry -> rates.put(entry.getKey(), entry.getValue().asDouble()));
} catch (IOException | InterruptedException e) {
log.severe("Failed to load exchange rates from Coinbase API: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.javamoney.shelter.bitcoin.provider;

import org.javamoney.moneta.CurrencyUnitBuilder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import javax.money.UnknownCurrencyException;
import javax.money.convert.CurrencyConversionException;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;

public class CoinbaseRateProviderTest {
private static CoinbaseRateProvider coinbaseRateProvider;

@BeforeClass
public static void setUpBeforeClass() {
coinbaseRateProvider = new CoinbaseRateProvider();
CurrencyUnitBuilder.of("BTC", "CoinbaseRateProvider")
.setDefaultFractionDigits(8)
.build(true);
}

@AfterClass
public static void tearDownAfterClass() {
coinbaseRateProvider = null;
}

@Test
public void testGetExchangeRate() {
assertNotNull(coinbaseRateProvider.getExchangeRate("BTC", "USD"));
}

@Test
public void testGetExchangeRateWithNotSupportedBaseCurrency() {
assertThrows(CurrencyConversionException.class, () -> coinbaseRateProvider.getExchangeRate("USD", "BTC"));
}

@Test
public void testGetExchangeRateWithNotSupportedTermCurrency() {
assertThrows(CurrencyConversionException.class, () -> coinbaseRateProvider.getExchangeRate("BTC", "ZWL"));
}
}