diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f43113 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.git +.DS_Store +dist/ +bin/ +build/ diff --git a/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar new file mode 100644 index 0000000..33b3949 Binary files /dev/null and b/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar differ diff --git a/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar new file mode 100644 index 0000000..3888660 Binary files /dev/null and b/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar differ diff --git a/server_extensions_extra/res/batm-extensions.xml b/server_extensions_extra/res/batm-extensions.xml index 0037f31..363e6de 100644 --- a/server_extensions_extra/res/batm-extensions.xml +++ b/server_extensions_extra/res/batm-extensions.xml @@ -24,10 +24,19 @@ BTC + + BTC + BTC + + + + + BTC + diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java index c575856..1542e43 100644 --- a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java @@ -18,6 +18,7 @@ package com.generalbytes.batm.server.extensions.extra.bitcoin; import com.generalbytes.batm.server.extensions.*; +import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitfinex.BitfinexExchange; import com.generalbytes.batm.server.extensions.extra.bitcoin.paymentprocessors.bitcoinpay.BitcoinPayPP; import com.generalbytes.batm.server.extensions.extra.bitcoin.sources.BitcoinAverageRateSource; import com.generalbytes.batm.server.extensions.extra.bitcoin.sources.FixPriceRateSource; @@ -35,8 +36,19 @@ public class BitcoinExtension implements IExtension{ } @Override - public IExchange createExchange(String exchangeLogin) { - return null; //no BTC exchange available in open source version so far (Bitstamp is in built-in extension) + public IExchange createExchange(String paramString) //(Bitstamp is in built-in extension) + { + if ((paramString != null) && (!paramString.trim().isEmpty())) + { + StringTokenizer paramTokenizer = new StringTokenizer(paramString, ":"); + String prefix = paramTokenizer.nextToken(); + if ("bitfinex".equalsIgnoreCase(prefix)) { + String apiKey = paramTokenizer.nextToken(); + String apiSecret = paramTokenizer.nextToken(); + return new BitfinexExchange(apiKey, apiSecret); + } + } + return null; } @Override @@ -118,6 +130,15 @@ public class BitcoinExtension implements IExtension{ } } return new FixPriceRateSource(rate); + }else if ("bitfinex".equalsIgnoreCase(exchangeType)) { + BigDecimal rate = BigDecimal.ZERO; + if (st.hasMoreTokens()) { + try { + rate = new BigDecimal(st.nextToken()); + } catch (Throwable e) { + } + } + return new BitfinexExchange("**","**"); } } return null; diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java new file mode 100644 index 0000000..30d950e --- /dev/null +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java @@ -0,0 +1,244 @@ +/************************************************************************************* + * Copyright (C) 2014 GENERAL BYTES s.r.o. All rights reserved. + * + * This software may be distributed and modified under the terms of the GNU + * General Public License version 2 (GPL2) as published by the Free Software + * Foundation and appearing in the file GPL2.TXT included in the packaging of + * this file. Please note that GPL2 Section 2[b] requires that all works based + * on this software must also be made publicly available under the terms of + * the GPL2 ("Copyleft"). + * + * Contact information + * ------------------- + * + * GENERAL BYTES s.r.o. + * Web : http://www.generalbytes.com + * + * Other information: + * + * This implementation was created in cooperation with Orillia BVBA + ************************************************************************************/ +package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitfinex; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import com.generalbytes.batm.server.extensions.IRateSource; +import com.xeiam.xchange.dto.trade.LimitOrder; +import com.xeiam.xchange.dto.trade.OpenOrders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.generalbytes.batm.server.extensions.ICurrencies; +import com.generalbytes.batm.server.extensions.IExchange; +import com.xeiam.xchange.Exchange; +import com.xeiam.xchange.ExchangeFactory; +import com.xeiam.xchange.ExchangeSpecification; +import com.xeiam.xchange.currency.CurrencyPair; +import com.xeiam.xchange.dto.Order.OrderType; +import com.xeiam.xchange.dto.marketdata.Ticker; +import com.xeiam.xchange.dto.trade.MarketOrder; +import com.xeiam.xchange.service.polling.PollingAccountService; +import com.xeiam.xchange.service.polling.PollingMarketDataService; +import com.xeiam.xchange.service.polling.PollingTradeService; + +public class BitfinexExchange implements IExchange, IRateSource { + + private static final Logger log = LoggerFactory.getLogger("batm.master.BitfinexEchange"); + private Exchange exchange = null; + private String apiKey; + private String apiSecret; + + private static HashMap rateAmounts = new HashMap(); + private static HashMap rateTimes = new HashMap(); + private static final long MAXIMUM_ALLOWED_TIME_OFFSET = 30 * 1000; + + public BitfinexExchange(String apiKey, String apiSecret) { + this.apiKey = apiKey; + this.apiSecret = apiSecret; + } + + private synchronized Exchange getExchange() { + if (this.exchange == null) { + ExchangeSpecification bfxSpec = new com.xeiam.xchange.bitfinex.v1.BitfinexExchange().getDefaultExchangeSpecification(); + bfxSpec.setApiKey(this.apiKey); + bfxSpec.setSecretKey(this.apiSecret); + this.exchange = ExchangeFactory.INSTANCE.createExchange(bfxSpec); + } + return this.exchange; + } + + public Set getCryptoCurrencies() { + Set cryptoCurrencies = new HashSet(); + cryptoCurrencies.add(ICurrencies.BTC); + return cryptoCurrencies; + } + + public Set getFiatCurrencies() { + Set fiatCurrencies = new HashSet(); + fiatCurrencies.add(ICurrencies.USD); + return fiatCurrencies; + } + + public String getPreferredFiatCurrency() { + return ICurrencies.USD; + } + + + @Override + public synchronized BigDecimal getExchangeRateLast(String cryptoCurrency, String fiatCurrency) { + String key = cryptoCurrency +"_" + fiatCurrency; + synchronized (rateAmounts) { + long now = System.currentTimeMillis(); + BigDecimal amount = rateAmounts.get(key); + if (amount == null) { + BigDecimal result = getExchangeRateLastSync(cryptoCurrency, fiatCurrency); + log.debug("Called bitfinex exchange for rate: " + key + " = " + result); + rateAmounts.put(key,result); + rateTimes.put(key,now+MAXIMUM_ALLOWED_TIME_OFFSET); + return result; + }else { + Long expirationTime = rateTimes.get(key); + if (expirationTime > now) { + return rateAmounts.get(key); + }else{ + //do the job; + BigDecimal result = getExchangeRateLastSync(cryptoCurrency, fiatCurrency); + log.debug("Called bitfinex exchange for rate: " + key + " = " + result); + rateAmounts.put(key,result); + rateTimes.put(key,now+MAXIMUM_ALLOWED_TIME_OFFSET); + return result; + } + } + } + } + + private BigDecimal getExchangeRateLastSync(String cryptoCurrency, String cashCurrency) { + PollingMarketDataService marketDataService = getExchange().getPollingMarketDataService(); + try { + Ticker ticker = marketDataService.getTicker(new CurrencyPair(cryptoCurrency,cashCurrency)); + return ticker.getLast(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public BigDecimal getCryptoBalance(String cryptoCurrency) { + // [TODO] Can be extended to support LTC and DRK (and other currencies supported by BFX) + if (!ICurrencies.BTC.equalsIgnoreCase(cryptoCurrency)) { + return BigDecimal.ZERO; + } + log.debug("Calling Bitfinex exchange (getBalance)"); + + try { + return getExchange().getPollingAccountService().getAccountInfo().getBalance(cryptoCurrency); + } catch (IOException e) { + e.printStackTrace(); + log.error("Bitfinex exchange (getBalance) failed with message: " + e.getMessage()); + } + return null; + } + + public BigDecimal getFiatBalance(String fiatCurrency) { + if (!ICurrencies.USD.equalsIgnoreCase(fiatCurrency)) { + return BigDecimal.ZERO; + } + log.debug("Calling Bitfinex exchange (getBalance)"); + + try { + return getExchange().getPollingAccountService().getAccountInfo().getBalance(fiatCurrency); + } catch (IOException e) { + e.printStackTrace(); + log.error("Bitfinex exchange (getBalance) failed with message: " + e.getMessage()); + } + return null; + } + + public final String sendCoins(String destinationAddress, BigDecimal amount, String cryptoCurrency, String description) { + if (!ICurrencies.BTC.equalsIgnoreCase(cryptoCurrency)) { + log.error("Bitfinex supports only " + ICurrencies.BTC); + return null; + } + + log.info("Calling bitfinex exchange (withdrawal destination: " + destinationAddress + " amount: " + amount + " " + cryptoCurrency + ")"); + + PollingAccountService accountService = getExchange().getPollingAccountService(); + try { + accountService.withdrawFunds(destinationAddress, amount, destinationAddress); + } catch (IOException e) { + e.printStackTrace(); + log.error("Bitfinex exchange (withdrawFunds) failed with message: " + e.getMessage()); + } + return null; + } + + public String purchaseCoins(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + if (!ICurrencies.BTC.equalsIgnoreCase(cryptoCurrency)) { + log.error("Bitfinex implementation supports only " + ICurrencies.BTC); + return null; + } + if (!ICurrencies.USD.equalsIgnoreCase(fiatCurrencyToUse)) { + log.error("Bitfinex supports only " + ICurrencies.USD ); + return null; + } + + log.info("Calling Bitfinex exchange (purchase " + amount + " " + cryptoCurrency + ")"); + PollingAccountService accountService = getExchange().getPollingAccountService(); + PollingTradeService tradeService = getExchange().getPollingTradeService(); + + try { + log.debug("AccountInfo as String: " + accountService.getAccountInfo().toString()); + + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrencyToUse); + + MarketOrder order = new MarketOrder(OrderType.BID, amount, currencyPair); + log.debug("marketOrder = " + order); + + String orderId = tradeService.placeMarketOrder(order); + log.debug("orderId = " + orderId + " " + order); + + try { + Thread.sleep(2000); //give exchange 2 seconds to reflect open order in order book + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // get open orders + log.debug("Open orders:"); + boolean orderProcessed = false; + int numberOfChecks = 0; + while (!orderProcessed && numberOfChecks < 10) { + boolean orderFound = false; + OpenOrders openOrders = tradeService.getOpenOrders(); + for (LimitOrder openOrder : openOrders.getOpenOrders()) { + log.debug("openOrder = " + openOrder); + if (orderId.equalsIgnoreCase(openOrder.getId())) { + orderFound = true; + } + } + if (orderFound) { + log.debug("Waiting for order to be processed."); + try { + Thread.sleep(3000); //don't get your ip address banned + } catch (InterruptedException e) { + e.printStackTrace(); + } + }else{ + orderProcessed = true; + } + numberOfChecks++; + } + if (orderProcessed) { + return orderId; + } + } catch (IOException e) { + e.printStackTrace(); + log.error("Bitfinex exchange (purchaseCoins) failed with message: " + e.getMessage()); + } + return null; + } +}