package org.knowm.xchange.ripple; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.currency.CurrencyPair; import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.account.AccountInfo; import org.knowm.xchange.dto.account.Balance; import org.knowm.xchange.dto.account.Wallet; import org.knowm.xchange.dto.marketdata.OrderBook; import org.knowm.xchange.dto.marketdata.Trades.TradeSortType; import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.dto.trade.OpenOrders; import org.knowm.xchange.dto.trade.UserTrade; import org.knowm.xchange.dto.trade.UserTrades; import org.knowm.xchange.ripple.dto.RippleAmount; import org.knowm.xchange.ripple.dto.account.ITransferFeeSource; import org.knowm.xchange.ripple.dto.account.RippleAccountBalances; import org.knowm.xchange.ripple.dto.account.RippleBalance; import org.knowm.xchange.ripple.dto.marketdata.RippleOrder; import org.knowm.xchange.ripple.dto.marketdata.RippleOrderBook; import org.knowm.xchange.ripple.dto.trade.IRippleTradeTransaction; import org.knowm.xchange.ripple.dto.trade.RippleAccountOrders; import org.knowm.xchange.ripple.dto.trade.RippleAccountOrdersBody; import org.knowm.xchange.ripple.dto.trade.RippleLimitOrder; import org.knowm.xchange.ripple.dto.trade.RippleUserTrade; import org.knowm.xchange.ripple.service.RippleAccountService; import org.knowm.xchange.ripple.service.RippleTradeServiceRaw; import org.knowm.xchange.ripple.service.params.RippleMarketDataParams; import org.knowm.xchange.ripple.service.params.RippleTradeHistoryPreferredCurrencies; import org.knowm.xchange.service.trade.params.TradeHistoryParamCurrencyPair; import org.knowm.xchange.service.trade.params.TradeHistoryParams; import org.knowm.xchange.utils.jackson.CurrencyPairDeserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Various adapters for converting from Ripple DTOs to XChange DTOs */ public abstract class RippleAdapters { private static final Set<Currency> EMPTY_CURRENCY_SET = Collections.emptySet(); private static final Logger logger = LoggerFactory.getLogger(RippleAdapters.class); /** * private Constructor */ private RippleAdapters() { } /** * Adapts a Ripple Account to an XChange Wallet object. */ public static AccountInfo adaptAccountInfo(final RippleAccountBalances account, final String username) { // Adapt account balances to XChange balances final Map<String, List<Balance>> balances = new HashMap<String, List<Balance>>(); for (final RippleBalance balance : account.getBalances()) { final String walletId; if (balance.getCurrency().equals("XRP")) { walletId = null; } else { walletId = balance.getCounterparty(); } if (!balances.containsKey(walletId)) { balances.put(walletId, new LinkedList<Balance>()); } balances.get(walletId).add(new Balance(Currency.getInstance(balance.getCurrency()), balance.getValue())); } final List<Wallet> accountInfo = new ArrayList<Wallet>(balances.size()); for (final Map.Entry<String, List<Balance>> wallet : balances.entrySet()) { accountInfo.add(new Wallet(wallet.getKey(), wallet.getValue())); } return new AccountInfo(username, BigDecimal.ZERO, accountInfo); } /** * Adapts a Ripple OrderBook to an XChange OrderBook object. * <p> * Counterparties are not mapped since the application calling this should know and keep track of the counterparties it is using in the polling * thread. */ public static OrderBook adaptOrderBook(final RippleOrderBook rippleOrderBook, final RippleMarketDataParams params, final CurrencyPair currencyPair) { final String orderBook = rippleOrderBook.getOrderBook(); // e.g. XRP/BTC+rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B final String[] splitPair = orderBook.split("/"); final String[] baseSplit = splitPair[0].split("\\+"); final String baseSymbol = baseSplit[0]; if (baseSymbol.equals(currencyPair.base.getCurrencyCode()) == false) { throw new IllegalStateException(String.format("base symbol in Ripple order book %s does not match requested base %s", orderBook, currencyPair)); } final String baseCounterparty; if (baseSymbol.equals("XRP")) { baseCounterparty = ""; // native currency } else { baseCounterparty = baseSplit[1]; } if (baseCounterparty.equals(params.getBaseCounterparty()) == false) { throw new IllegalStateException(String.format("base counterparty in Ripple order book %s does not match requested counterparty %s", orderBook, params.getBaseCounterparty())); } final String[] counterSplit = splitPair[1].split("\\+"); final String counterSymbol = counterSplit[0]; if (counterSymbol.equals(currencyPair.counter.getCurrencyCode()) == false) { throw new IllegalStateException( String.format("counter symbol in Ripple order book %s does not match requested base %s", orderBook, currencyPair)); } final String counterCounterparty; if (counterSymbol.equals("XRP")) { counterCounterparty = ""; // native currency } else { counterCounterparty = counterSplit[1]; } if (counterCounterparty.equals(params.getCounterCounterparty()) == false) { throw new IllegalStateException(String.format("counter counterparty in Ripple order book %s does not match requested counterparty %s", orderBook, params.getCounterCounterparty())); } final List<LimitOrder> bids = createOrders(currencyPair, OrderType.BID, rippleOrderBook.getBids(), baseCounterparty, counterCounterparty); final List<LimitOrder> asks = createOrders(currencyPair, OrderType.ASK, rippleOrderBook.getAsks(), baseCounterparty, counterCounterparty); return new OrderBook(null, asks, bids); } public static List<LimitOrder> createOrders(final CurrencyPair currencyPair, final OrderType orderType, final List<RippleOrder> orders, final String baseCounterparty, final String counterCounterparty) { final List<LimitOrder> limitOrders = new ArrayList<LimitOrder>(); for (final RippleOrder rippleOrder : orders) { // Taker Pays = the amount the taker must pay to consume this order. // Taker Gets = the amount the taker will get once the order is consumed. // // Funded vs Unfunded https://wiki.ripple.com/Unfunded_offers // amount of base currency final BigDecimal tradableAmount; if (orderType == OrderType.BID) { tradableAmount = rippleOrder.getTakerPaysFunded().getValue(); } else { tradableAmount = rippleOrder.getTakerGetsFunded().getValue(); } // price in counter currency final BigDecimal price = rippleOrder.getPrice().getValue(); final RippleLimitOrder order = new RippleLimitOrder(orderType, tradableAmount, currencyPair, Integer.toString(rippleOrder.getSequence()), null, price, baseCounterparty, counterCounterparty); limitOrders.add(order); } return limitOrders; } /** * Adapts a Ripple Account Orders object to an XChange OpenOrders object * <p> * Counterparties set in additional data since there is no other way of the application receiving this information. */ public static OpenOrders adaptOpenOrders(final RippleAccountOrders rippleOrders, final int scale) { final List<LimitOrder> list = new ArrayList<LimitOrder>(rippleOrders.getOrders().size()); for (final RippleAccountOrdersBody order : rippleOrders.getOrders()) { final OrderType orderType; final RippleAmount baseAmount; final RippleAmount counterAmount; if (order.getType().equals("buy")) { // buying: we receive base and pay with counter, taker receives counter and pays with base counterAmount = order.getTakerGets(); baseAmount = order.getTakerPays(); orderType = OrderType.BID; } else { // selling: we receive counter and pay with base, taker receives base and pays with counter baseAmount = order.getTakerGets(); counterAmount = order.getTakerPays(); orderType = OrderType.ASK; } final String baseSymbol = baseAmount.getCurrency(); final String counterSymbol = counterAmount.getCurrency(); // need to provide rounding scale to prevent ArithmeticException final BigDecimal price = counterAmount.getValue().divide(baseAmount.getValue(), scale, RoundingMode.HALF_UP).stripTrailingZeros(); final CurrencyPair pair = new CurrencyPair(baseSymbol, counterSymbol); final RippleLimitOrder xchangeOrder = (RippleLimitOrder) new RippleLimitOrder.Builder(orderType, pair) .baseCounterparty(baseAmount.getCounterparty()).counterCounterparty(counterAmount.getCounterparty()).id(Long.toString(order.getSequence())) .limitPrice(price).timestamp(null).tradableAmount(baseAmount.getValue()).build(); list.add(xchangeOrder); } return new OpenOrders(list); } public static UserTrade adaptTrade(final IRippleTradeTransaction trade, final TradeHistoryParams params, final ITransferFeeSource transferFeeSource, final int scale) throws IOException { // The order{} section of the body cannot be used to determine trade facts e.g. if the order was to sell BTC.Bitstamp and buy // BTC.SnapSwap, and traded via XRP, and our trade was one of the XRP legs, all we'd see would be the taker getting and paying BTC. // // Details in the balance_changes{} and order_changes{} blocks are relative to the perspective account, i.e. the Ripple account address used in the URI. final List<RippleAmount> balanceChanges = trade.getBalanceChanges(); final Iterator<RippleAmount> iterator = balanceChanges.iterator(); while (iterator.hasNext()) { final RippleAmount amount = iterator.next(); if (amount.getCurrency().equals("XRP") && trade.getFee().equals(amount.getValue().negate())) { // XRP balance change is just the fee - it should not be part of the currency pair considerations iterator.remove(); } } if (balanceChanges.size() != 2) { logger.warn("for hash[{}] of type[{}] balance changes section should contains 2 currency amounts but found {}", trade.getHash(), trade.getClass().getSimpleName(), balanceChanges); return null; } // There is no way of telling the original entered base or counter currency - Ripple just provides 2 currency adjustments. // Check if TradeHistoryParams expressed a preference, otherwise arrange the currencies in the order they are supplied. final Collection<Currency> preferredBase, preferredCounter; if (params instanceof RippleTradeHistoryPreferredCurrencies) { final RippleTradeHistoryPreferredCurrencies rippleParams = (RippleTradeHistoryPreferredCurrencies) params; preferredBase = rippleParams.getPreferredBaseCurrency(); preferredCounter = rippleParams.getPreferredCounterCurrency(); } else { preferredBase = preferredCounter = EMPTY_CURRENCY_SET; } final RippleAmount base, counter; if (preferredBase.contains(Currency.getInstance(balanceChanges.get(0).getCurrency()))) { base = balanceChanges.get(0); counter = balanceChanges.get(1); } else if (preferredBase.contains(Currency.getInstance(balanceChanges.get(1).getCurrency()))) { base = balanceChanges.get(1); counter = balanceChanges.get(0); } else if (preferredCounter.contains(Currency.getInstance(balanceChanges.get(0).getCurrency()))) { counter = balanceChanges.get(0); base = balanceChanges.get(1); } else if (preferredCounter.contains(Currency.getInstance(balanceChanges.get(1).getCurrency()))) { counter = balanceChanges.get(1); base = balanceChanges.get(0); } else if ((params instanceof TradeHistoryParamCurrencyPair) && (((TradeHistoryParamCurrencyPair) params).getCurrencyPair() != null)) { // Searching for a specific currency pair - use this direction final CurrencyPair pair = ((TradeHistoryParamCurrencyPair) params).getCurrencyPair(); if (pair.base.getCurrencyCode().equals(balanceChanges.get(0).getCurrency()) && pair.counter.getCurrencyCode().equals(balanceChanges.get(1).getCurrency())) { base = balanceChanges.get(0); counter = balanceChanges.get(1); } else if (pair.base.getCurrencyCode().equals(balanceChanges.get(1).getCurrency()) && pair.counter.getCurrencyCode().equals(balanceChanges.get(0).getCurrency())) { base = balanceChanges.get(1); counter = balanceChanges.get(0); } else { // Unexpected: this should have been filtered out in RippleTradeServiceRaw.getTradesForAccount(..) method. throw new IllegalStateException(String.format("trade history param currency filter specified %s but trade query returned %s and %s", pair, balanceChanges.get(0).getCurrency(), balanceChanges.get(1).getCurrency())); } } else { // select the currency direction as return from the API base = balanceChanges.get(0); counter = balanceChanges.get(1); } final OrderType type; if (base.getValue().signum() == 1) { type = OrderType.BID; } else { type = OrderType.ASK; } final String currencyPairString = base.getCurrency() + "/" + counter.getCurrency(); final CurrencyPair currencyPair = CurrencyPairDeserializer.getCurrencyPairFromString(currencyPairString); // Ripple has 2 types of fee. // // (a) Transaction fee is a network charge levied in XRP. // https://wiki.ripple.com/Transaction_Fee // // (b) Transfer fee charged by the issuer levied in the currency of traded instrument. Whoever // sends an asset that has a transfer fee pays the fee, the receiver does not incur a charge. // https://wiki.ripple.com/Transit_Fee // https://ripple.com/knowledge_center/transfer-fees/ // Ripple supplies XRP with net quantity and price, must apply these to the // trade as gross amounts to ensure the same as the other XChange connections. final BigDecimal baseTransferFee = RippleTradeServiceRaw.getExpectedTransferFee(transferFeeSource, base.getCounterparty(), base.getCurrency(), base.getValue(), type); final BigDecimal baseValue = base.getValue().abs().subtract(baseTransferFee); final OrderType counterDirection; if (type == OrderType.BID) { counterDirection = OrderType.ASK; } else { counterDirection = OrderType.BID; } final BigDecimal counterTransferFee = RippleTradeServiceRaw.getExpectedTransferFee(transferFeeSource, counter.getCounterparty(), counter.getCurrency(), counter.getValue(), counterDirection); final BigDecimal counterValue = counter.getValue().abs().subtract(counterTransferFee); // Account for transaction fee in quantities. final BigDecimal transactionFee = trade.getFee(); final BigDecimal quantity; if (base.getCurrency().equals("XRP")) { if (type == OrderType.BID) { quantity = baseValue.add(transactionFee); } else { // OrderType.ASK quantity = baseValue.subtract(transactionFee); } } else { quantity = baseValue; } final BigDecimal counterAmount; if (counter.getCurrency().equals("XRP")) { if (type == OrderType.ASK) { counterAmount = counterValue.add(transactionFee); } else { // OrderType.BID counterAmount = counterValue.subtract(transactionFee); } } else { counterAmount = counterValue; } // need to provide rounding scale to prevent ArithmeticException final BigDecimal price = counterAmount.divide(quantity, scale, RoundingMode.HALF_UP); final String orderId = Long.toString(trade.getOrderId()); final RippleUserTrade.Builder builder = (RippleUserTrade.Builder) new RippleUserTrade.Builder().currencyPair(currencyPair) .feeAmount(transactionFee).feeCurrency(Currency.XRP).id(trade.getHash()).orderId(orderId).price(price.stripTrailingZeros()) .timestamp(trade.getTimestamp()).tradableAmount(quantity.stripTrailingZeros()).type(type); builder.baseTransferFee(baseTransferFee.abs()); builder.counterTransferFee(counterTransferFee.abs()); if (base.getCounterparty().length() > 0) { builder.baseCounterparty(base.getCounterparty()); } if (counter.getCounterparty().length() > 0) { builder.counterCounterparty(counter.getCounterparty()); } return builder.build(); } public static UserTrades adaptTrades(final Collection<IRippleTradeTransaction> tradesForAccount, final TradeHistoryParams params, final RippleAccountService accountService, final int roundingScale) throws IOException { final List<UserTrade> trades = new ArrayList<UserTrade>(); for (final IRippleTradeTransaction orderDetails : tradesForAccount) { final UserTrade trade = adaptTrade(orderDetails, params, accountService, roundingScale); if (trade == null) { // any issue should have been reported by adaptTrade } else { trades.add(trade); } } return new UserTrades(trades, TradeSortType.SortByTimestamp); } }