package com.mygeopay.core.exchange.shapeshift;
import com.mygeopay.core.coins.CoinID;
import com.mygeopay.core.coins.CoinType;
import com.mygeopay.core.coins.Value;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftAmountTx;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftCoins;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftEmail;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftException;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftLimit;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftMarketInfo;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftNormalTx;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftRate;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftTime;
import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftTxStatus;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import org.bitcoinj.core.Address;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import static com.mygeopay.core.Preconditions.checkNotNull;
import static com.mygeopay.core.Preconditions.checkState;
/**
* @author John L. Jegutanis
*/
public class ShapeShift extends Connection {
private static final Logger log = LoggerFactory.getLogger(ShapeShift.class);
private static final MediaType MEDIA_TYPE_JSON
= MediaType.parse("application/json");
private static final String GET_COINS_API = "getcoins";
private static final String MARKET_INFO_API = "marketinfo/%s";
private static final String RATE_API = "rate/%s";
private static final String LIMIT_API = "limit/%s";
private static final String TIME_REMAINING_API = "timeremaining/%s";
private static final String TX_STATUS_API = "txStat/%s";
private static final String NORMAL_TX_API = "shift";
private static final String FIXED_AMOUNT_TX_API = "sendamount";
private static final String EMAIL_RECEIPT_API = "mail";
private String apiPublicKey;
public ShapeShift(OkHttpClient client) {
super(client);
}
public ShapeShift() {}
public void setApiPublicKey(String apiPublicKey) {
this.apiPublicKey = apiPublicKey;
}
/**
* Get List of Supported Coins
*
* List of all the currencies that Shapeshift currently supports at any given time. Sometimes
* coins become temporarily unavailable during updates or unexpected service issues.
*/
public ShapeShiftCoins getCoins() throws ShapeShiftException, IOException {
Request request = new Request.Builder().url(getApiUrl(GET_COINS_API)).build();
return new ShapeShiftCoins(getMakeApiCall(request));
}
/**
* Get Market Info
*
* This is a combined call for {@link #getRate(CoinType, CoinType) getRate()} and
* {@link #getLimit(CoinType, CoinType) getLimit()} API calls.
*/
public ShapeShiftMarketInfo getMarketInfo(CoinType typeFrom, CoinType typeTo)
throws ShapeShiftException, IOException {
return getMarketInfo(getPair(typeFrom, typeTo));
}
/**
* Get Market Info
*
* This is a combined call for {@link #getRate(CoinType, CoinType) getRate()} and
* {@link #getLimit(CoinType, CoinType) getLimit()} API calls.
*/
public ShapeShiftMarketInfo getMarketInfo(String pair)
throws ShapeShiftException, IOException {
log.info("Market info for pair {}", pair);
String apiUrl = getApiUrl(String.format(MARKET_INFO_API, pair));
Request request = new Request.Builder().url(apiUrl).build();
ShapeShiftMarketInfo reply = new ShapeShiftMarketInfo(getMakeApiCall(request));
if (!reply.isError) checkPair(pair, reply.pair);
return reply;
}
/**
* Rate
*
* Gets the current rate offered by Shapeshift. This is an estimate because the rate can
* occasionally change rapidly depending on the markets. The rate is also a 'use-able' rate not
* a direct market rate. Meaning multiplying your input coin amount times the rate should give
* you a close approximation of what will be sent out. This rate does not include the
* transaction (miner) fee taken off every transaction.
*/
public ShapeShiftRate getRate(CoinType typeFrom, CoinType typeTo)
throws ShapeShiftException, IOException {
String pair = getPair(typeFrom, typeTo);
String apiUrl = getApiUrl(String.format(RATE_API, pair));
Request request = new Request.Builder().url(apiUrl).build();
ShapeShiftRate reply = new ShapeShiftRate(getMakeApiCall(request));
if (!reply.isError) checkPair(pair, reply.pair);
return reply;
}
/**
* Deposit Limit
*
* Gets the current deposit limit set by Shapeshift. Amounts deposited over this limit will be
* sent to the return address if one was entered, otherwise the user will need to contact
* ShapeShift support to retrieve their coins. This is an estimate because a sudden market swing
* could move the limit.
*/
public ShapeShiftLimit getLimit(CoinType typeFrom, CoinType typeTo)
throws ShapeShiftException, IOException {
String pair = getPair(typeFrom, typeTo);
String apiUrl = getApiUrl(String.format(LIMIT_API, pair));
Request request = new Request.Builder().url(apiUrl).build();
ShapeShiftLimit reply = new ShapeShiftLimit(getMakeApiCall(request));
if (!reply.isError) checkPair(pair, reply.pair);
return reply;
}
/**
* Time Remaining on Fixed Amount Transaction
*
* When a transaction is created with a fixed amount requested there is a 10 minute window for
* the deposit. After the 10 minute window if the deposit has not been received the transaction
* expires and a new one must be created. This api call returns how many seconds are left before
* the transaction expires.
*/
public ShapeShiftTime getTime(Address address) throws ShapeShiftException, IOException {
String apiUrl = getApiUrl(String.format(TIME_REMAINING_API, address.toString()));
Request request = new Request.Builder().url(apiUrl).build();
return new ShapeShiftTime(getMakeApiCall(request));
}
/**
* Status of deposit to address
*
* This returns the status of the most recent deposit transaction to the address.
*/
public ShapeShiftTxStatus getTxStatus(Address address) throws ShapeShiftException, IOException {
String apiUrl = getApiUrl(String.format(TX_STATUS_API, address.toString()));
Request request = new Request.Builder().url(apiUrl).build();
ShapeShiftTxStatus reply = new ShapeShiftTxStatus(getMakeApiCall(request));
if (!reply.isError && reply.address != null) checkAddress(address, reply.address);
return new ShapeShiftTxStatus(reply, address);
}
/**
* Normal Transaction
*
* Make a normal exchange and receive with {@code withdrawal} address. The exchange pair is
* determined from the {@link CoinType}s of {@code refund} and {@code withdrawal}.
*/
public ShapeShiftNormalTx exchange(Address withdrawal, Address refund)
throws ShapeShiftException, IOException {
JSONObject requestJson = new JSONObject();
try {
requestJson.put("withdrawal", withdrawal.toString());
requestJson.put("pair", getPair(
(CoinType) refund.getParameters(), (CoinType) withdrawal.getParameters()));
requestJson.put("returnAddress", refund.toString());
if (apiPublicKey != null) requestJson.put("apiKey", apiPublicKey);
} catch (JSONException e) {
throw new ShapeShiftException("Could not create a JSON request", e);
}
String apiUrl = getApiUrl(NORMAL_TX_API);
RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString());
Request request = new Request.Builder().url(apiUrl).post(body).build();
ShapeShiftNormalTx reply = new ShapeShiftNormalTx(getMakeApiCall(request));
if (!reply.isError) checkAddress(withdrawal, reply.withdrawal);
return reply;
}
/**
* Fixed Amount Transaction
*
* This call allows you to request a fixed amount to be sent to the {@code withdrawal} address.
* You provide a withdrawal address and the amount you want sent to it. We return the amount
* to deposit and the address to deposit to. This allows you to use shapeshift as a payment
* mechanism.
*
* The exchange pair is determined from the {@link CoinType}s of {@code refund} and
* {@code withdrawal}.
*/
public ShapeShiftAmountTx exchangeForAmount(Value amount, Address withdrawal, Address refund)
throws ShapeShiftException, IOException {
String pair = getPair((CoinType) refund.getParameters(),
(CoinType) withdrawal.getParameters());
JSONObject requestJson = new JSONObject();
try {
requestJson.put("withdrawal", withdrawal.toString());
requestJson.put("pair", pair);
requestJson.put("returnAddress", refund.toString());
requestJson.put("amount", amount.toPlainString());
if (apiPublicKey != null) requestJson.put("apiKey", apiPublicKey);
} catch (JSONException e) {
throw new ShapeShiftException("Could not create a JSON request", e);
}
String apiUrl = getApiUrl(FIXED_AMOUNT_TX_API);
RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString());
Request request = new Request.Builder().url(apiUrl).post(body).build();
ShapeShiftAmountTx reply = new ShapeShiftAmountTx(getMakeApiCall(request));
if (!reply.isError) {
checkPair(pair, reply.pair);
checkValue(amount, reply.withdrawalAmount);
checkAddress(withdrawal, reply.withdrawal);
}
return reply;
}
/**
* Request email receipt
*
* This call allows you to request a fixed amount to be sent to the {@code withdrawal} address.
* You provide a withdrawal address and the amount you want sent to it. We return the amount
* to deposit and the address to deposit to. This allows you to use shapeshift as a payment
* mechanism.
*
* The exchange pair is determined from the {@link CoinType}s of {@code refund} and
* {@code withdrawal}.
*/
public ShapeShiftEmail requestEmailReceipt(String email, ShapeShiftTxStatus txStatus)
throws ShapeShiftException, IOException {
JSONObject requestJson = new JSONObject();
try {
requestJson.put("email", email);
checkState(txStatus.status == ShapeShiftTxStatus.Status.COMPLETE,
"Transaction not complete");
requestJson.put("txid", checkNotNull(txStatus.transactionId, "Null transaction id"));
} catch (Exception e) {
throw new ShapeShiftException("Could not create a JSON request", e);
}
String apiUrl = getApiUrl(EMAIL_RECEIPT_API);
RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString());
Request request = new Request.Builder().url(apiUrl).post(body).build();
return new ShapeShiftEmail(getMakeApiCall(request));
}
/**
* Convert types to the ShapeShift format. For example Bitcoin to Litecoin will become btc_ltc.
*/
public static String getPair(CoinType typeFrom, CoinType typeTo) {
return typeFrom.getSymbol().toLowerCase() + "_" + typeTo.getSymbol().toLowerCase();
}
private void checkPair(String expectedPair, String pair)
throws ShapeShiftException {
if (!expectedPair.equals(pair)) {
String errorMsg = String.format("Pair mismatch, expected %s but got %s.",
expectedPair, pair);
throw new ShapeShiftException(errorMsg);
}
}
private void checkValue(Value expected, Value value) throws ShapeShiftException {
if (!expected.equals(value)) {
String errorMsg = String.format("Value mismatch, expected %s but got %s.",
expected, value);
throw new ShapeShiftException(errorMsg);
}
}
private void checkAddress(Address expected, Address address) throws ShapeShiftException {
if (!expected.getParameters().equals(address.getParameters()) ||
!expected.toString().equals(address.toString())) {
String errorMsg = String.format("Address mismatch, expected %s but got %s.",
expected, address);
throw new ShapeShiftException(errorMsg);
}
}
private JSONObject getMakeApiCall(Request request) throws ShapeShiftException, IOException {
try {
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
JSONObject reply = parseReply(response);
String genericMessage = "Error code " + response.code();
throw new IOException(
reply != null ? reply.optString("error", genericMessage) : genericMessage);
}
return parseReply(response);
} catch (JSONException e) {
throw new ShapeShiftException("Could not parse JSON", e);
}
}
private static JSONObject parseReply(Response response) throws IOException, JSONException {
return new JSONObject(response.body().string());
}
public static CoinType[] parsePair(String pair) {
String[] pairs = pair.split("_");
checkState(pairs.length == 2);
return new CoinType[]{CoinID.typeFromSymbol(pairs[0]), CoinID.typeFromSymbol(pairs[1])};
}
}