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.HttpURLConnection; 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.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; 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.JsonParseException; /** * Adapter for integrating with Bitstamp exchange. * <p> * Bitstamp API details here: * https://www.bitstamp.net/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 BitstampExchangeAdapter implements TradingApi { /** Logger */ private static final Logger LOG = Logger.getLogger(BitstampExchangeAdapter.class); /** The Auth API URI */ private static final String API_BASE_URL = "https://www.bitstamp.net/api/"; /** * Your Bitstamp API keys and connection timeout config. * This file *must* be on Ada's classpath. */ private static final String CONFIG_LOCATION = "bitstamp-config.properties"; /** Name of client id prop in config file. */ private static final String CLIENT_ID_PROPERTY_NAME = "client-id"; /** Name of key prop in config file.*/ private static final String KEY_PROPERTY_NAME = "key"; /** Name of secret prop in config file. */ private static final String SECRET_PROPERTY_NAME = "secret"; /** Name of connection timeout prop in config file. */ private static final String CONNECTION_TIMEOUT_PROPERTY_NAME = "connection-timeout"; /** * Nonce is a regular integer number. It must be increasing with every request we make. * We use unix time for inititialising the first nonce param. */ 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 client id */ private String clientId = ""; /** * Our key used in the MAC message: * message = nonce + client_id + api_key */ private String key = ""; /** * Our secret used for signing MAC message * signature = hmac.new(API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() */ private String secret = ""; /** * 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. * * Signature is a HMAC-SHA256 encoded message containing: nonce, client ID and API key. * The HMAC-SHA256 code must be generated using a secret key that was generated with your API key. * This code must be converted to it's hexadecimal representation (64 uppercase characters). */ private Mac mac; /** GSON engine used for parsing JSON in stamp API call responses */ private Gson gson; /* * Constructor gets the adapter ready for calling the stamp API. */ public BitstampExchangeAdapter() { // 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(); } ///////////////////////////////////////////////////////////////////// // // Bitstamp API Calls we adapt to the common Trading API. // See https://www.bitstamp.net/api/ // ///////////////////////////////////////////////////////////////////// @Override public MarketOrderBook getMarketOrders(String marketId) throws TradingApiException { final String results = sendPublicRequestToExchange("order_book", null); // TODO tmp debug to trap all the diff types of error response in JSON //LOG.debug("getMarketOrders() response: " + results); final BitstampOrderBook bitstampOrderBook = gson.fromJson(results, BitstampOrderBook.class); //adapt BUYs final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>(); final List<List<BigDecimal>> bitstampBuyOrders = bitstampOrderBook.bids; for (final List<BigDecimal> order : bitstampBuyOrders) { final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, order.get(0), // price order.get(1), // quanity order.get(0).multiply(order.get(1))); buyOrders.add(buyOrder); } // adapt SELLs final List<MarketOrder> sellOrders = new ArrayList<MarketOrder>(); final List<List<BigDecimal>> bitstampSellOrders = bitstampOrderBook.asks; for (final List<BigDecimal> order : bitstampSellOrders) { final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, order.get(0), // price order.get(1), // quantity order.get(0).multiply(order.get(1))); sellOrders.add(sellOrder); } final MarketOrderBook orderBook = new MarketOrderBook(marketId, sellOrders, buyOrders); return orderBook; } @Override public List<OpenOrder> getYourOpenOrders(String marketId) throws TradingApiException { final String results = sendAuthenticatedRequestToExchange("open_orders", null); // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("getYourOpenOrders() response: " + results); final BitstampOrderResponse[] myOpenOrders = gson.fromJson(results, BitstampOrderResponse[].class); // adapt final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>(); for (final BitstampOrderResponse openOrder : myOpenOrders) { OrderType orderType = null; if (openOrder.type == 0) { orderType = OrderType.BUY; } else if (openOrder.type == 1) { orderType = OrderType.SELL; } else { throw new TradingApiException( "Unrecognised order type received in getYourOpenOrders(). Value: " + openOrder.type); } final OpenOrder order = new OpenOrder(Long.toString(openOrder.id), openOrder.datetime, marketId, orderType, openOrder.price, openOrder.amount, new BigDecimal(0), //cryptsyOrders[i].orig_quantity - not provided by stamp :-( new BigDecimal(0) //cryptsyOrders[i].total - not provided by stamp :-( ); ordersToReturn.add(order); } return ordersToReturn; } @Override public String createOrder(String marketId, String orderType, BigDecimal quantity, BigDecimal price) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); // note we need to limit to 8 decimal places else exchange will barf params.put("price", new DecimalFormat("#.########").format(price)); params.put("amount", new DecimalFormat("#.########").format(quantity)); final String results; if (orderType == OrderType.BUY.getStringValue()) { // buying BTC results = sendAuthenticatedRequestToExchange("buy", params); } else if (orderType == OrderType.SELL.getStringValue()) { // selling BTC results = sendAuthenticatedRequestToExchange("sell", params); } else { final String errorMsg = "Invalid order type: " + orderType + " - Can only be " + OrderType.BUY.getStringValue() + " or " + OrderType.SELL.getStringValue(); LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("createOrder() response: " + results); final BitstampOrderResponse createOrderResponse = gson.fromJson(results, BitstampOrderResponse.class); return Long.toString(createOrderResponse.id); } @Override public boolean cancelOrder(String orderId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("id", orderId); final String results = sendAuthenticatedRequestToExchange("cancel_order", params); // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("cancelOrder() response: " + results); // Returns 'true' if order has been found and canceled. if (results.equalsIgnoreCase("true")) { return true; } else { return false; } } /** * Fetches the latest price for a given market in USD. * * @param marketId the id of the market. * @return the latest market price in USD. * @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(String marketId) throws TradingApiException { final String results = sendPublicRequestToExchange("ticker", null); // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("getLatestMarketPrice() response: " + results); final BitstampTicker bitstampTicker = gson.fromJson(results, BitstampTicker.class); return bitstampTicker.last; } @Override public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee() { // TODO get from API! return new BigDecimal(0.005); // 0.5% } @Override public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() { // TODO get from API! return new BigDecimal(0.005); // 0.5% } @Override public BalanceInfo getBalanceInfo() throws TradingApiException { final String results = sendAuthenticatedRequestToExchange("balance", null); // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("getBalanceInfo() response: " + results); final BitstampBalance balances = gson.fromJson(results, BitstampBalance.class); // adapt final Map<String, BigDecimal> balancesAvailable = new HashMap<String, BigDecimal>(); balancesAvailable.put("BTC", balances.btc_available); balancesAvailable.put("USD", balances.usd_available); final Map<String, BigDecimal> balancesOnOrder = new HashMap<String, BigDecimal>(); balancesOnOrder.put("BTC", balances.btc_reserved); balancesOnOrder.put("USD", balances.usd_reserved); final BalanceInfo balanceInfo = new BalanceInfo(balancesAvailable, balancesOnOrder); return balanceInfo; } @Override public String getImplName() { return "Bitstamp Exchange"; } ///////////////////////////////////////////////////////////////////// // // GSON classes for JSON responses // See https://www.bitstamp.net/api/ // ///////////////////////////////////////////////////////////////////// /** * GSON class for holding Bitstamp Balance response from balance API call. * <p> * JSON looks like: * <pre> * { * "btc_reserved": "0", "fee": "0.5000", "btc_available": "0", * "usd_reserved": "0", "btc_balance": "0", "usd_balance": "0.00", "usd_available": "0.00" * } * </pre> * @author gazbert */ private static class BitstampBalance { // field names map to the JSON arg names public BigDecimal btc_reserved; public BigDecimal fee; public BigDecimal btc_available; public BigDecimal usd_reserved; public BigDecimal usd_balance; public BigDecimal usd_available; @Override public String toString() { return BitstampBalance.class.getSimpleName() + " [btc_reserved=" + btc_reserved + ", fee=" + fee + ", btc_available=" + btc_available + ", usd_reserved=" + usd_reserved + ", usd_balance=" + usd_balance + ", usd_available=" + usd_available + "]"; } } /** * GSON class for holding Bitstamp Order Book response from order_book API call. * <p> * JSON looks like: * <pre> * { * "timestamp": "1400943488", * "bids": [["521.86", "0.00017398"], ["519.58", "0.25100000"], ["0.01", "38820.00000000"]], * "asks": [["521.88", "10.00000000"], ["522.00", "310.24504478"], ["522.13", "0.02852084"]] * } * </pre> * Each is a list of open orders and each order is represented as a list of price and amount. * * @author gazbert */ private static class BitstampOrderBook { // field names map to the JSON arg names public long timestamp; //unix timestamp date and time public List<List<BigDecimal>> bids; public List<List<BigDecimal>> asks; @Override public String toString() { return BitstampOrderBook.class.getSimpleName() + " [timestamp=" + timestamp + ", bids=" + bids + ", asks=" + asks + "]"; } } /** * GSON class for a Bitstamp ticker response. * <p> * JSON looks like: * <pre> * { * "high": "586.34", "last": "576.55", "timestamp": "1401031033", "bid": "575.02", "vwap": "560.61", * "volume": "18412.67728589", "low": "517.55", "ask": "576.55" * } * </pre> * @author gazbert */ private static class BitstampTicker { // field names map to the JSON arg names public BigDecimal high; public BigDecimal last; public long timestamp; public BigDecimal bid; public BigDecimal vwap; public BigDecimal volume; public BigDecimal low; public BigDecimal ask; @Override public String toString() { return BitstampTicker.class.getSimpleName() + " [high=" + high + ", last=" + last + ", timestamp=" + timestamp + ", bid=" + bid + ", vwap=" + vwap + ", volume=" + volume + ", low=" + low + ", ask=" + ask + "]"; } } /** * GSON class for Bitstamp create order response. * <p> * JSON looks like: * <pre> * [{"price": "569.50", "amount": "0.00893170", "type": 0, "id": 25716384, "datetime": "2014-05-25 19:45:59"}] * </pre> * @author gazbert */ private static class BitstampOrderResponse { // field names map to the JSON arg names public long id; public Date datetime; public int type; // 0 = buy; 1 = sell public BigDecimal price; public BigDecimal amount; @Override public String toString() { return BitstampOrderResponse.class.getSimpleName() + " [" + "id=" + id + ", datetime=" + datetime + ", type=" + type + ", price=" + price + ", amount=" + amount + "]"; } } /** * Needed because stamp Date format is different in open_order response and causes default Gson parsing to barf: * <pre> * [main] 2014-05-25 20:51:31,074 ERROR BitstampExchangeAdapter - Failed to parse a Bitstamp date * java.text.ParseException: Unparseable date: "2014-05-25 19:50:32" * at java.text.DateFormat.parse(DateFormat.java:357) * at com.gazbert.ada.adapter.BitstampExchangeAdapter$DateDeserializer.deserialize(BitstampExchangeAdapter.java:596) * at com.gazbert.ada.adapter.BitstampExchangeAdapter$DateDeserializer.deserialize(BitstampExchangeAdapter.java:1) * at com.google.gson.TreeTypeAdapter.read(TreeTypeAdapter.java:58) * </pre> * @author gazbert */ private class BitstampDateDeserializer implements JsonDeserializer<Date> { private SimpleDateFormat bitstampDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public Date deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { Date dateFromBitstamp = null; if (json.isJsonPrimitive()) { try { dateFromBitstamp = bitstampDateFormat.parse(json.getAsString()); } catch (ParseException e) { // TODO should we barf big time here?? dateFromBitstamp = null; final String errorMsg = "Failed to parse a Bitstamp date"; LOG.error(errorMsg, e); } } return dateFromBitstamp; } } ///////////////////////////////////////////////////////////////////// // // Private utils. // ///////////////////////////////////////////////////////////////////// /* * Makes a public API call to Bitstamp exchange. Uses HTTP GET. * * @param apiMethod the API method to call * @param params the query param args to use in the API call * @return the response from Bitstamp. * @throws TradingApiException */ private final String sendPublicRequestToExchange(final String apiMethod, Map<String, String> params) throws TradingApiException { // Connect to the exchange HttpURLConnection exchangeConnection = null; final StringBuffer exchangeResponse = new StringBuffer(); try { if (params == null) { params = new HashMap<String, String>(); } // Build the URL with query param args in it String queryString = ""; if (params.size() > 0) { queryString = "?"; } for (final Iterator<String> paramsIterator = params.keySet().iterator(); paramsIterator.hasNext();) { final String param = paramsIterator.next(); if (queryString.length() > 0) { queryString += "&"; } queryString += param + "=" + URLEncoder.encode(params.get(param)); } // MUST have the trailing slash even if no params... final URL url = new URL(API_BASE_URL + apiMethod + "/" + queryString); if (LOG.isDebugEnabled()) { LOG.debug("Using following URL for API call: " + url); } exchangeConnection = (HttpURLConnection) url.openConnection(); exchangeConnection.setUseCaches(false); exchangeConnection.setDoOutput(true); // TODO check this is correct format 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); // 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(); // return the JSON response string return exchangeResponse.toString(); } 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); } } finally { if (exchangeConnection != null) { exchangeConnection.disconnect(); } } } /* * Makes an authenticated (private) API call to Bitstamp exchange. Uses HTTP POST. * * @param apiMethod the API method to call * @param params the query param args to use in the API call * @return the response from Bitstamp. * @throws TradingApiException */ private final String sendAuthenticatedRequestToExchange(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); } // Connect to the exchange HttpURLConnection exchangeConnection = null; final StringBuffer exchangeResponse = new StringBuffer(); try { if (params == null) { params = new HashMap<String, String>(); } params.put("key", key); params.put("nonce", Long.toString(nonce)); // Create MAC message for signature // message = nonce + client_id + api_key mac.reset(); // force reset mac.update(String.valueOf(nonce).getBytes()); mac.update(clientId.getBytes()); mac.update(key.getBytes()); // signature = hmac.new(API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() final String signature = toHex(mac.doFinal()).toUpperCase(); //final String signature = String.format("%064x", new BigInteger(1, mac.doFinal())).toUpperCase(); params.put("signature", signature); // increment ready 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)); } final URL url = new URL(API_BASE_URL + apiMethod + "/"); // MUST have the trailing slash... if (LOG.isDebugEnabled()) { LOG.debug("Using following URL for API call: " + url); } exchangeConnection = (HttpURLConnection) url.openConnection(); exchangeConnection.setUseCaches(false); exchangeConnection.setDoOutput(true); // TODO check this is correct format 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(); // return the JSON response string return exchangeResponse.toString(); } 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); } } finally { if (exchangeConnection != null) { exchangeConnection.disconnect(); } } } /* * 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(secret.getBytes("UTF-8"), "HmacSHA256"); mac = Mac.getInstance("HmacSHA256"); mac.init(keyspec); initializedMACAuthentication = true; } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA256 installed?"; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } catch (InvalidKeyException e) { final String errorMsg = "Failed to setup MAC security. Secret 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 Bistamp config at: " + CONFIG_LOCATION + " HINT: is it on Ada's classpath?"; LOG.error(errorMsg); throw new IllegalStateException(errorMsg); } try { configEntries.load(inputStream); /* * Grab the client id */ clientId = configEntries.getProperty(CLIENT_ID_PROPERTY_NAME); // WARNING: Never log this unless testing in the green zone //LOG.info(CLIENT_ID_PROPERTY_NAME + ": " + clientId); if (clientId == null || clientId.length() == 0) { final String errorMsg = CLIENT_ID_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 key */ key = configEntries.getProperty(KEY_PROPERTY_NAME); // WARNING: Don't log this on 'exposed' system... //LOG.info(KEY_PROPERTY_NAME + ": " + key); if (key == null || key.length() == 0) { final String errorMsg = 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 secret */ secret = configEntries.getProperty(SECRET_PROPERTY_NAME); // WARNING: Never log this unless testing in the green zone //LOG.info(SECRET_PROPERTY_NAME + ": " + secret); if (secret == null || secret.length() == 0) { final String errorMsg = SECRET_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 BitstampDateDeserializer()); gson = gsonBuilder.create(); } }