-
Notifications
You must be signed in to change notification settings - Fork 5
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
2ae0c20
c9ffad6
faf8f29
b6c0a53
394cb84
a8ec1b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current code throws a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! I've kept the |
||
} | ||
} | ||
|
||
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")); | ||
} | ||
} |
There was a problem hiding this comment.
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-8304165There was a problem hiding this comment.
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 implementsAutoCloseable
, allowing us to use the try-with-resources construct for proper resource management.I've added a new commit with these changes.