package com.yoghurt.crypto.transactions.server.servlets.providers;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.ParseException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
import org.apache.http.util.EntityUtils;
import com.yoghurt.crypto.transactions.server.util.HttpClientProxy;
import com.yoghurt.crypto.transactions.server.util.json.JSONRPCEncoder;
import com.yoghurt.crypto.transactions.server.util.json.JSONRPCParser;
import com.yoghurt.crypto.transactions.shared.domain.AddressInformation;
import com.yoghurt.crypto.transactions.shared.domain.AddressOutpoint;
import com.yoghurt.crypto.transactions.shared.domain.BlockInformation;
import com.yoghurt.crypto.transactions.shared.domain.JSONRPCMethod;
import com.yoghurt.crypto.transactions.shared.domain.TransactionInformation;
import com.yoghurt.crypto.transactions.shared.domain.config.BitcoinCoreNodeConfig;
import com.yoghurt.crypto.transactions.shared.domain.exception.ApplicationException;
import com.yoghurt.crypto.transactions.shared.domain.exception.ApplicationException.Reason;
import com.yoghurt.crypto.transactions.shared.service.BlockchainRetrievalService;
public class BitcoinJSONRPCRetriever implements BlockchainRetrievalService {
private static final String JSON_RPC_REALM = "jsonrpc";
private static final String AUTH_SCHEME = AuthSchemes.BASIC;
private static final String URI_FORMAT = "http://%s:%s";
/**
* The JSON-RPC interface doesn't return the genesis coinbase transaction, so it's been hard-coded here.
*/
private static final String GENESIS_COINBASE_TXID = "4A5E1E4BAAB89F3A32518A88C31BC87F618F76673E2CC77AB2127B7AFDEDA33B";
private static final String GENESIS_COINBASE_RAW = "01000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000";
private final String uri;
private final HttpClientContext localContext;
private final CredentialsProvider credentialsProvider = new SystemDefaultCredentialsProvider();
private final ArrayList<JSONRPCMethod> allowedRPCMethods;
public BitcoinJSONRPCRetriever(final BitcoinCoreNodeConfig config) {
this(config.getHost(), Integer.parseInt(config.getPort()), config.getRpcUser(), config.getRpcPass());
}
private BitcoinJSONRPCRetriever(final String host, final int port, final String rpcUser, final String rpcPassword) {
uri = String.format(URI_FORMAT, host, port);
credentialsProvider.setCredentials(new AuthScope(host, port, JSON_RPC_REALM, AUTH_SCHEME),
new UsernamePasswordCredentials(rpcUser, rpcPassword));
final AuthCache authCache = new BasicAuthCache();
authCache.put(new HttpHost(host, port), new BasicScheme());
localContext = HttpClientContext.create();
localContext.setAuthCache(authCache);
allowedRPCMethods = new ArrayList<JSONRPCMethod>();
for (final JSONRPCMethod method : JSONRPCMethod.values()) {
allowedRPCMethods.add(method);
}
}
@Override
public String getLatestBlockHash() throws ApplicationException {
try {
return doSimpleJSONRPCMethod("getbestblockhash");
} catch (IOException | HttpException e) {
e.printStackTrace();
throw new ApplicationException(e.getMessage());
}
}
@Override
public TransactionInformation getTransactionInformation(final String txid) throws ApplicationException {
if (GENESIS_COINBASE_TXID.equalsIgnoreCase(txid)) {
final TransactionInformation ti = new TransactionInformation();
ti.setRawHex(GENESIS_COINBASE_RAW);
return ti;
}
try (CloseableHttpClient client = getAuthenticatedHttpClientProxy();
InputStream jsonData = doComplexJSONRPCMethod(client, "getrawtransaction", txid, 1).getContent()) {
return JSONRPCParser.getTransactionInformation(jsonData);
} catch (IOException | HttpException e) {
e.printStackTrace();
throw new ApplicationException(e.getMessage());
}
}
@Override
public BlockInformation getBlockInformationFromHeight(final int height) throws ApplicationException {
return getBlockInformationFromHash(getBlockHashFromHeight(height));
}
@Override
public AddressInformation getAddressInformation(final String address) throws ApplicationException {
try (final CloseableHttpClient client = getAuthenticatedHttpClientProxy();
final InputStream jsonData = doComplexJSONRPCMethod(client, "searchrawtransactions", address).getContent()) {
final AddressInformation addressInformation = JSONRPCParser.getAddressInformation(address, jsonData);
for (final AddressOutpoint outpoint : addressInformation.getOutpoints()) {
final String txid = new String(Hex.encodeHex(outpoint.getReferenceTransaction()));
try (final InputStream utxoJsonData = doComplexJSONRPCMethod(client, "gettxout", txid, outpoint.getIndex())
.getContent()) {
outpoint.setSpent(JSONRPCParser.isNullResult(utxoJsonData));
}
}
return addressInformation;
} catch (IOException | HttpException | DecoderException e) {
e.printStackTrace();
throw new ApplicationException(e.getMessage());
}
}
private String getBlockHashFromHeight(final int height) throws ApplicationException {
try {
return doSimpleJSONRPCMethod("getblockhash", height);
} catch (final IOException | HttpException e) {
e.printStackTrace();
throw new ApplicationException(e.getMessage());
}
}
@Override
public BlockInformation getBlockInformationLast() throws ApplicationException {
return getBlockInformationFromHash(getLatestBlockHash());
}
@Override
public ArrayList<String> getTransactionList(final int height) throws ApplicationException {
try (CloseableHttpClient client = getAuthenticatedHttpClientProxy();
InputStream jsonData = doComplexJSONRPCMethod(client, "getblock", getBlockHashFromHeight(height))
.getContent()) {
return JSONRPCParser.getTransactionList(jsonData);
} catch (IOException | HttpException e) {
e.printStackTrace();
System.out.println("What up.");
throw new ApplicationException(e.getMessage());
}
}
@Override
public BlockInformation getBlockInformationFromHash(final String identifier) throws ApplicationException {
try (CloseableHttpClient client = getAuthenticatedHttpClientProxy();
InputStream jsonData = doComplexJSONRPCMethod(client, "getblock", identifier).getContent()) {
final BlockInformation blockInformation = JSONRPCParser.getBlockInformation(jsonData);
// Extract the coinbase tx id (TODO: Refactor, shouldn't be using a mock
// object like this)
final String coinbaseTxid = blockInformation.getCoinbaseInformation().getRawHex();
// Retrieve raw coinbase tx and its blockchain information
final TransactionInformation ti = getTransactionInformation(coinbaseTxid);
// Stick it in the block info
blockInformation.setCoinbaseInformation(ti);
// Return the result
return blockInformation;
} catch (IOException | HttpException | DecoderException e) {
e.printStackTrace();
throw new ApplicationException(e.getMessage());
}
}
@Override
public String getJSONRPCResponse(final JSONRPCMethod method, final String[] arguments) throws ApplicationException {
if (!allowedRPCMethods.contains(method)) {
throw new ApplicationException(Reason.ILLEGAL_OPERATION);
}
try (final CloseableHttpClient client = getAuthenticatedHttpClientProxy()) {
final HttpEntity jsonData = doComplexJSONRPCMethod(client, method.getMethodName(), true, (Object[]) arguments);
return EntityUtils.toString(jsonData);
} catch (final IOException | IllegalStateException | ParseException | HttpException e) {
e.printStackTrace();
throw new ApplicationException(Reason.INTERNAL_ERROR);
}
}
private String doSimpleJSONRPCMethod(final String method, final Object... params) throws IOException, HttpException {
try (CloseableHttpClient client = getAuthenticatedHttpClientProxy();
InputStream stream = doComplexJSONRPCMethod(client, method, params).getContent()) {
return JSONRPCParser.getResultString(stream);
}
}
private HttpEntity doComplexJSONRPCMethod(final CloseableHttpClient client, final String method,
final Object... params) throws IOException, IllegalStateException, ParseException, HttpException {
return doComplexJSONRPCMethod(client, method, false, params);
}
private HttpEntity doComplexJSONRPCMethod(final CloseableHttpClient client, final String method, final boolean unsafe,
final Object... params) throws IOException, IllegalStateException, ParseException, HttpException {
final String payload = JSONRPCEncoder.getRequestString(method, params);
// Temporary
System.out.println("> " + payload);
return HttpClientProxy.postRemoteContent(client, unsafe, uri, payload);
}
private CloseableHttpClient getAuthenticatedHttpClientProxy() {
return HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build();
}
}