Java tutorial
package com.gazbert.ada.adapter; /* * The MIT License (MIT) * * Copyright (c) 2014 Gareth Jon Lynch * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.log4j.Logger; import com.gazbert.ada.trading.api.BalanceInfo; import com.gazbert.ada.trading.api.MarketOrder; import com.gazbert.ada.trading.api.MarketOrderBook; import com.gazbert.ada.trading.api.OpenOrder; import com.gazbert.ada.trading.api.OrderType; import com.gazbert.ada.trading.api.TimeoutException; import com.gazbert.ada.trading.api.TradingApi; import com.gazbert.ada.trading.api.TradingApiException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; /** * Adapter for integrating with Cryptsy exchange. * <p> * Cryptsy API details here: * https://www.cryptsy.com/pages/api * <p> * This adapter is Single threaded: we <em>must</em> preserve trading transaction order... the URLConnection * does this by blocking when waiting for input stream (response) for each API call. It is *not* thread safe in its * current form. * <p> * API calls will throw a {@link TimeoutException} if: * <ul> * <li>A {@link java.net.SocketTimeoutException} is thrown by the {@link URLConnection}</li> * <li>A {@link java.io.IOException} thrown by the {@link URLConnection} with HTTP status codes: * 502 Bad Gateway, 503 Servivce Unavailable, 504 Gateway Timeout.</li> * </ul> * * @author gazbert * */ public final class CryptsyExchangeAdapter implements TradingApi { /** Logger */ private static final Logger LOG = Logger.getLogger(CryptsyExchangeAdapter.class); /** The Auth API URI */ private static final String AUTH_API_URL = "https://api.cryptsy.com/api"; /** * Your Cryptsy API keys and connection timeout config. * This file *must* be on Ada's classpath. */ private static final String CONFIG_LOCATION = "cryptsy-config.properties"; /** Name of PUBLIC key prop in config file.*/ private static final String PUBLIC_KEY_PROPERTY_NAME = "public-key"; /** Name of PRIVATE key prop in config file. */ private static final String PRIVATE_KEY_PROPERTY_NAME = "private-key"; /** Name of connection timeout prop in config file. */ private static final String CONNECTION_TIMEOUT_PROPERTY_NAME = "connection-timeout"; /** Nonce used for auth packets */ private static long nonce = 0; /** The connection timeout in SECONDS for terminating hung connections to the exchange. */ private int connectionTimeout; /** Used to indicate if we have initialised the MAC authentication protocol */ private boolean initializedMACAuthentication = false; /** Our public key used in MAC authentication */ private String publicKey = ""; /** Our private key used in MAC authentication */ private String privateKey = ""; /** * MAC - This class provides the functionality of a "Message Authentication Code" (MAC) algorithm. * Used to encrypt the hash of the entire message with our private key to ensure message integrity. */ private Mac mac; /** GSON engine used for parsing JSON in Cryptsy API call responses */ private Gson gson; /* * Constructor gets the adapter ready for calling the Cryptsy API. */ public CryptsyExchangeAdapter() { // set the initial nonce used in the secure messaging. nonce = System.currentTimeMillis() / 1000; // set up the secure messaging layer for payload integrity. initSecureMessageLayer(); // Initialise GSON layer for processing the JSON in the API calls/responses. initGson(); } ///////////////////////////////////////////////////////////////////// // // Cryptsy API Calls we adapt to the common Trading API. // See https://www.cryptsy.com/pages/privateapi // ///////////////////////////////////////////////////////////////////// @Override public MarketOrderBook getMarketOrders(final String marketId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("marketid", marketId); final String results = sendRequestToExchange("marketorders", params); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON //LOG.debug("getMarketOrders() response: " + results); final CryptsyMarketOrderBookWrapper marketOrderWrapper = gson.fromJson(results, CryptsyMarketOrderBookWrapper.class); //adapt BUYs final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>(); final CryptsyBuyOrder[] cryptsyBuyOrders = marketOrderWrapper.info.buyorders; for (int i = 0; i < cryptsyBuyOrders.length; i++) { final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, cryptsyBuyOrders[i].buyprice, cryptsyBuyOrders[i].quantity, cryptsyBuyOrders[i].total); buyOrders.add(buyOrder); } // adapt SELLs final List<MarketOrder> sellOrders = new ArrayList<MarketOrder>(); final CryptsySellOrder[] cryptsySellOrders = marketOrderWrapper.info.sellorders; for (int i = 0; i < cryptsySellOrders.length; i++) { final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, cryptsySellOrders[i].sellprice, cryptsySellOrders[i].quantity, cryptsySellOrders[i].total); sellOrders.add(sellOrder); } final MarketOrderBook orderBook = new MarketOrderBook(marketId, sellOrders, buyOrders); return orderBook; } @Override public List<OpenOrder> getYourOpenOrders(final String marketId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("marketid", marketId); final String results = sendRequestToExchange("myorders", params); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON //LOG.debug("getYourOpenOrders() response: " + results); final CryptsyMyOpenOrders myOpenOrders = gson.fromJson(results, CryptsyMyOpenOrders.class); // adapt final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>(); final CryptsyOrder[] cryptsyOrders = myOpenOrders.orders; for (int i = 0; i < cryptsyOrders.length; i++) { OrderType orderType = null; final String cryptsyOrderType = cryptsyOrders[i].ordertype; if (cryptsyOrderType.equalsIgnoreCase(OrderType.BUY.getStringValue())) { orderType = OrderType.BUY; } else if (cryptsyOrderType.equalsIgnoreCase(OrderType.SELL.getStringValue())) { orderType = OrderType.SELL; } else { throw new TradingApiException( "Unrecognised order type received in getYourOpenOrders(). Value: " + cryptsyOrderType); } final OpenOrder order = new OpenOrder(Long.toString(cryptsyOrders[i].orderid), cryptsyOrders[i].created, marketId, orderType, cryptsyOrders[i].price, cryptsyOrders[i].quantity, cryptsyOrders[i].orig_quantity, cryptsyOrders[i].total); ordersToReturn.add(order); } return ordersToReturn; } @Override public String createOrder(final String marketId, final String orderType, final BigDecimal quantity, final BigDecimal price) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("marketid", marketId); params.put("ordertype", orderType); // note we need to limit to 8 decimal places else exchange will barf params.put("quantity", new DecimalFormat("#.########").format(quantity)); params.put("price", new DecimalFormat("#.########").format(price)); final String results = sendRequestToExchange("createorder", params); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON LOG.debug("createOrder() response: " + results); final CryptsyCreateOrderResponse createOrderResponse = gson.fromJson(results, CryptsyCreateOrderResponse.class); // Some logging for strange crypsy failures... if (createOrderResponse.success == 1) { return Long.toString(createOrderResponse.orderid); } else { LOG.error("Failed to place order on exchange. Error response: " + createOrderResponse.error + " ***MoreInfo: " + createOrderResponse.moreinfo); return "0"; } } @Override public boolean cancelOrder(final String orderId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("orderid", orderId); final String results = sendRequestToExchange("cancelorder", params); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON LOG.debug("cancelOrder() response: " + results); final CryptsyStringResults cancelOrderResponse = gson.fromJson(results, CryptsyStringResults.class); return (cancelOrderResponse.success == 1); } /** * Fetches the latest price for a given market in BTC. * * @param marketId the id of the market. * @return the latest market price in BTC. * @throws TimeoutException if a timeout occurred trying to connect to the exchange. The timeout limit is * implementation specific for each Exchange Adapter; see the documentation for the adatper you are using. * @throws TradingApiException if the API call failed for any reason other than a timeout. */ @Override public BigDecimal getLatestMarketPrice(final String marketId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("marketid", marketId); final String results = sendRequestToExchange("markettrades", params); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON //LOG.debug("getLatestMarketPrice() response: " + results); final CryptsyLatestTrades trades = gson.fromJson(results, CryptsyLatestTrades.class); // adapt final CryptsyTrade[] cryptsyTrades = trades.trades; if (cryptsyTrades.length == 0) { final String errorMsg = "Cryptsy 'markettrades' API call returned empty array of trades: " + cryptsyTrades; throw new TradingApiException(errorMsg); } // just take the latest one final BigDecimal latestTradePrice = new BigDecimal(cryptsyTrades[0].tradeprice); return latestTradePrice; } @Override public BalanceInfo getBalanceInfo() throws TradingApiException { final String results = sendRequestToExchange("getinfo", null); // TODO tmp debug to trap all the diff types of Cryptsy error response in JSON //LOG.debug("getBalanceInfo() response: " + results); final CryptsyInfoWrapper info = gson.fromJson(results, CryptsyInfoWrapper.class); // adapt final BalanceInfo balanceInfo = new BalanceInfo(info.info.balances_available, info.info.balances_hold); return balanceInfo; } @Override public String getImplName() { return "Cryptsy Exchange"; } @Override public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee() { // TODO get from Cryptsy API? return new BigDecimal(0.002); // 0.2% } @Override public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() { // TODO get from Cryptsy API? return new BigDecimal(0.003); // 0.3% } ///////////////////////////////////////////////////////////////////// // // GSON classes for JSON responses // See https://www.cryptsy.com/pages/privateapi // ///////////////////////////////////////////////////////////////////// /** * GSON class for the Order Book wrapper. * @author gazbert */ private static class CryptsyMarketOrderBookWrapper extends CryptsyMessageBase { // field names map to the JSON arg names @SerializedName("return") public CryptsyMarketOrderBook info; @Override public String toString() { return CryptsyMarketOrderBookWrapper.class.getSimpleName() + " [" + "info=" + info + "]"; } } /** * GSON class for a market Order Book. * @author gazbert */ private static class CryptsyMarketOrderBook { // field names map to the JSON arg names public CryptsySellOrder[] sellorders; public CryptsyBuyOrder[] buyorders; @Override public String toString() { return CryptsyMarketOrderBook.class.getSimpleName() + " [" + "sellorders=" + Arrays.toString(sellorders) + ", buyorders=" + Arrays.toString(buyorders) + "]"; } } /** * GSON class for a BUY order. * @author gazbert */ private static class CryptsyBuyOrder { // field names map to the JSON arg names public BigDecimal buyprice; public BigDecimal quantity; public BigDecimal total; @Override public String toString() { return CryptsyBuyOrder.class.getSimpleName() + " [" + "buyprice=" + buyprice + ", quantity=" + quantity + ", total=" + total + "]"; } } /** * GSON class for a SELL order. * @author gazbert */ private static class CryptsySellOrder { // field names map to the JSON arg names public BigDecimal sellprice; public BigDecimal quantity; public BigDecimal total; @Override public String toString() { return CryptsySellOrder.class.getSimpleName() + " [" + "sellprice=" + sellprice + ", quantity=" + quantity + ", total=" + total + "]"; } } /** * GSON class used to receive response after creating an order. * @author gazbert */ private static class CryptsyCreateOrderResponse extends CryptsyMessageBase { // field names map to the JSON arg names public long orderid; public String moreinfo; @Override public String toString() { return CryptsyCreateOrderResponse.class.getSimpleName() + " [" + "orderid=" + orderid + ", moreinfo=" + moreinfo + "]"; } } /** * GSON class for receiving my open orders in API call response. * @author gazbert */ private static class CryptsyMyOpenOrders extends CryptsyMessageBase { // field names map to the JSON arg names @SerializedName("return") public CryptsyOrder[] orders; @Override public String toString() { return CryptsyMyOpenOrders.class.getSimpleName() + " [" + "orders=" + Arrays.toString(orders) + "]"; } } /** * GSON class for mapping returned orders from API call response. * @author gazbert */ private static class CryptsyOrder { // field names map to the JSON arg names public long orderid; public Date created; public int marketid; public String ordertype; public BigDecimal price; public BigDecimal quantity; public BigDecimal orig_quantity; public BigDecimal total; @Override public String toString() { return CryptsyOrder.class.getSimpleName() + " [" + "orderid=" + orderid + ", created=" + created + ", marketid=" + marketid + ", ordertype=" + ordertype + ", price=" + price + ", quantity=" + quantity + ", orig_quantity=" + orig_quantity + ", total=" + total + "]"; } } /** * GSON class for mapping last completed Trades from API call response. * @author gazbert */ private static class CryptsyLatestTrades extends CryptsyMessageBase { // field names map to the JSON arg names @SerializedName("return") public CryptsyTrade[] trades; } /** * GSON class for mapping a completed Trade from API call response. * @author gazbert */ private static class CryptsyTrade { // field names map to the JSON arg names public long tradeid; public String tradetype; public Date datetime; public int marketid; public double tradeprice; public double quantity; public double fee; public double total; public String initiate_ordertype; public long order_id; @Override public String toString() { return CryptsyTrade.class.getSimpleName() + " [" + "tradeid=" + tradeid + ", tradetype=" + tradetype + ", datetime=" + datetime + ", marketid=" + marketid + ", tradeprice=" + tradeprice + ", quantity=" + quantity + ", fee=" + fee + ", total=" + total + ", initiate_ordertype=" + initiate_ordertype + ", order_id=" + order_id + "]"; } } /** * GSON base class for API call requests and responses. * @author gazbert */ private static class CryptsyMessageBase { // field names map to the JSON arg names public int success; public String error; public CryptsyMessageBase() { error = ""; } @Override public String toString() { return CryptsyMessageBase.class.getSimpleName() + " [" + "success=" + success + ", error=" + error + "]"; } } /** * GSON class for holding String results from API call requests/responses. * @author gazbert */ private static class CryptsyStringResults extends CryptsyMessageBase { // field names map to the JSON arg names @SerializedName("return") public String info; } /** * GSON class for wrapping Cryptsy Info response from getInfo() API call. * @author gazbert */ private static class CryptsyInfoWrapper extends CryptsyMessageBase { @SerializedName("return") public CryptsyInfo info; } /** * GSON class for holding Cryptsy info response from getInfo() API call. * @author gazbert */ private static class CryptsyInfo { // field names map to the JSON arg names public CryptsyBalances balances_available; public CryptsyBalances balances_hold; public long servertimestamp; public String servertimezone; public Date serverdatetime; public int openordercount; @Override public String toString() { return CryptsyInfo.class.getSimpleName() + " [servertimestamp=" + servertimestamp + ", servertimezone=" + servertimezone + ", serverdatetime=" + serverdatetime + ", openordercount=" + openordercount + "]"; } } /** * GSON class for holding altcoin wallet balances - basically a GSON enabled map. * @author gazbert */ private static class CryptsyBalances extends HashMap<String, BigDecimal> { /** SID*/ private static final long serialVersionUID = -4919716060747077759L; } /* * Needed because Cryptsy causes Gson to throw ISE under certain circumstances: * * TODO show exception... */ private class DateDeserializer implements JsonDeserializer<Date> { private SimpleDateFormat cryptsDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public Date deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { Date dateFromCryptsy = null; if (json.isJsonPrimitive()) { try { dateFromCryptsy = cryptsDateFormat.parse(json.getAsString()); } catch (ParseException e) { // TODO should we barf big time here?? dateFromCryptsy = null; final String errorMsg = "Failed to parse a Cryptsty date"; LOG.error(errorMsg, e); } } return dateFromCryptsy; } } /* * Needed because Cryptsy causes Gson to throw ISE under certain circumstances: * * Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 * at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:176) * at com.google.gson.Gson.fromJson(Gson.java:803) * at com.google.gson.Gson.fromJson(Gson.java:768) * at com.google.gson.Gson.fromJson(Gson.java:717) * at com.google.gson.Gson.fromJson(Gson.java:689) * at com.gazbert.ada.adapter.CryptsyExchangeAdapter.getBalanceInfo(CryptsyExchangeAdapter.java:313) * at com.gazbert.ada.engine.TradingEngine.checkAndBailIfBtcBalanceOnExchangeIsBelowEmergencyStopLimit(TradingEngine.java:280) * at com.gazbert.ada.engine.TradingEngine.startTradeCycle(TradingEngine.java:170) * at com.gazbert.ada.engine.TradingEngine.start(TradingEngine.java:150) * at com.gazbert.ada.engine.Ada.main(Ada.java:60) * Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 * at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:374) * at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:165) * ... 9 more * */ private class BalancesDeserializer implements JsonDeserializer<CryptsyBalances> { public CryptsyBalances deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { final CryptsyBalances balances = new CryptsyBalances(); if (json.isJsonObject()) { final JsonObject jsonObject = json.getAsJsonObject(); final Iterator<Entry<String, JsonElement>> balancesIterator = jsonObject.entrySet().iterator(); while (balancesIterator.hasNext()) { final Entry<String, JsonElement> jsonOrder = balancesIterator.next(); final String currency = jsonOrder.getKey(); final BigDecimal balance = context.deserialize(jsonOrder.getValue(), BigDecimal.class); balances.put(currency, balance); } } return balances; } } ///////////////////////////////////////////////////////////////////// // // Private utils. // ///////////////////////////////////////////////////////////////////// /* * Makes an API call to Cryptsy exchange. * * @param apiMethod the API method to call * @param params the query param args to use in the API call * @return the response from Cryptsy. * @throws TradingApiException */ private final String sendRequestToExchange(final String apiMethod, Map<String, String> params) throws TradingApiException { if (!initializedMACAuthentication) { final String errorMsg = "MAC Message security layer has not been initialized."; LOG.error(errorMsg); throw new IllegalStateException(errorMsg); } // method is the API method name // nonce is required by Cryptsy in every request if (params == null) { params = new HashMap<String, String>(); } params.put("method", apiMethod); params.put("nonce", Long.toString(nonce)); // increment ready for next call... // must be 1 higher for next call nonce++; // Build the URL with query param args in it String postData = ""; for (final Iterator<String> paramsIterator = params.keySet().iterator(); paramsIterator.hasNext();) { final String param = paramsIterator.next(); if (postData.length() > 0) { postData += "&"; } postData += param + "=" + URLEncoder.encode(params.get(param)); } // Connect to the exchange URLConnection exchangeConnection = null; final StringBuffer exchangeResponse = new StringBuffer(); try { final URL url = new URL(AUTH_API_URL); exchangeConnection = url.openConnection(); exchangeConnection.setUseCaches(false); exchangeConnection.setDoOutput(true); // Add my public key exchangeConnection.setRequestProperty("Key", publicKey); // Sign the payload with my private key exchangeConnection.setRequestProperty("Sign", toHex(mac.doFinal(postData.getBytes("UTF-8")))); exchangeConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Scare them with her name!!! Er, perhaps, I need to be a bit more stealth here... exchangeConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"); /* * Add a timeout so we don't get blocked indefinitley; Timeout is in milllis. * * Cryptsy gets stuck here for ~ 1 min once every half hour or so. Especially read timeouts. * * Give it time before killing it off - to prevent me from having to write more horrible order state * management code in the strategy ;-) * * connectionTimeout is in SECONDS and from config. */ final int timeoutInMillis = connectionTimeout * 1000; exchangeConnection.setConnectTimeout(timeoutInMillis); exchangeConnection.setReadTimeout(timeoutInMillis); // POST the request final OutputStreamWriter outputPostStream = new OutputStreamWriter( exchangeConnection.getOutputStream()); outputPostStream.write(postData); outputPostStream.close(); // Grab the response - we just block here as per Connection API final BufferedReader responseInputStream = new BufferedReader( new InputStreamReader(exchangeConnection.getInputStream())); // Read the JSON response lines into our response buffer String responseLine = null; while ((responseLine = responseInputStream.readLine()) != null) { exchangeResponse.append(responseLine); } responseInputStream.close(); } catch (MalformedURLException e) { final String errorMsg = "Failed to send request to Exchange."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } catch (SocketTimeoutException e) { final String errorMsg = "Failed to connect to Exchange due to socket timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } catch (IOException e) { /* * Crypsy often fails with these codes, but recovers by next request... */ if (e.getMessage().contains("502") || e.getMessage().contains("503") || e.getMessage().contains("504")) { final String errorMsg = "Failed to connect to Exchange due to 5XX timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } else { final String errorMsg = "Failed to connect to Exchange due to some other IO error."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } } // return the JSON response string return exchangeResponse.toString(); } /* * Converts a given byte array to a hex String. */ private String toHex(byte[] byteArrayToConvert) throws UnsupportedEncodingException { final StringBuilder hexString = new StringBuilder(); for (final byte aByte : byteArrayToConvert) { hexString.append(String.format("%02x", aByte & 0xff)); } return hexString.toString(); } /* * Initialises the Security layer. * Loads API keys and sets up the MAC to encrypt the data we send to the exchange. * We fail hard n fast if any of this stuff blows. */ private void initSecureMessageLayer() { // load API keys from config loadApiKeysAndOtherConfig(); // Setup the MAC try { final SecretKeySpec keyspec = new SecretKeySpec(privateKey.getBytes("UTF-8"), "HmacSHA512"); mac = Mac.getInstance("HmacSHA512"); mac.init(keyspec); initializedMACAuthentication = true; } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA512 installed?"; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } catch (InvalidKeyException e) { final String errorMsg = "Failed to setup MAC security. Private key seems invalid!"; LOG.error(errorMsg, e); throw new IllegalArgumentException(errorMsg, e); } } /* * Loads Cryptsy API keys and other config. */ private void loadApiKeysAndOtherConfig() { final Properties configEntries = new Properties(); final InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(CONFIG_LOCATION); if (inputStream == null) { final String errorMsg = "Cannot find Cryptsy config at: " + CONFIG_LOCATION + " HINT: is it on Ada's classpath?"; LOG.error(errorMsg); throw new IllegalStateException(errorMsg); } try { configEntries.load(inputStream); /* * Grab the public key */ publicKey = configEntries.getProperty(PUBLIC_KEY_PROPERTY_NAME); // WARNING: Don't log this on 'exposed' system... //LOG.info(PUBLIC_KEY_PROPERTY_NAME + ": " + publicKey); if (publicKey == null || publicKey.length() == 0) { final String errorMsg = PUBLIC_KEY_PROPERTY_NAME + " cannot be null or zero length!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } /* * Grab the private key */ privateKey = configEntries.getProperty(PRIVATE_KEY_PROPERTY_NAME); // WARNING: Never log this unless testing in the green zone //LOG.info(PRIVATE_KEY_PROPERTY_NAME + ": " + privateKey); if (privateKey == null || privateKey.length() == 0) { final String errorMsg = PRIVATE_KEY_PROPERTY_NAME + " cannot be null or zero length!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } /* * Grab the connection timeout */ connectionTimeout = Integer.parseInt( // will barf bigtime if not a number; we want this to fail fast. configEntries.getProperty(CONNECTION_TIMEOUT_PROPERTY_NAME)); if (connectionTimeout == 0) { final String errorMsg = CONNECTION_TIMEOUT_PROPERTY_NAME + " cannot be 0 value!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } LOG.info(CONNECTION_TIMEOUT_PROPERTY_NAME + ": " + connectionTimeout); } catch (IOException e) { final String errorMsg = "Failed to load Exchange config: " + CONFIG_LOCATION; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } finally { try { inputStream.close(); } catch (IOException e) { final String errorMsg = "Failed to close input stream for: " + CONFIG_LOCATION; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } } } /* * Initialises the GSON layer. */ private void initGson() { final GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer()); gsonBuilder.registerTypeAdapter(CryptsyBalances.class, new BalancesDeserializer()); gson = gsonBuilder.create(); } }