Skip to content

Commit

Permalink
Merge pull request #17 from rikublock/riku/fix-gettxout
Browse files Browse the repository at this point in the history
improve gettxout
  • Loading branch information
rikublock authored Aug 7, 2023
2 parents 49da559 + bb0b5e0 commit fbf1dba
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.subgraph.orchid.encoders.Hex;
import io.cloudchains.app.App;
import io.cloudchains.app.net.CoinInstance;
import io.cloudchains.app.net.CoinTicker;
Expand All @@ -31,6 +32,9 @@
import org.apache.http.message.BasicHeader;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.bitcoinj.core.Address;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.json.JSONArray;
import org.json.JSONObject;

Expand Down Expand Up @@ -172,6 +176,79 @@ private String doPost(String endpoint, JsonObject params) {
return res;
}

/**
* Returns all utxos for a list of addresses.
* Note: This method does neither use nor update any caches!
*
* @param coinTicker Fetch utxos from this coin
* @param address Fetch utxos from this address
* @return JsonArray or null on error
*/
public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) {
CoinInstance coinInstance = CoinInstance.getInstance(coinTicker);

JsonArray innerParams = new JsonArray();
innerParams.add(CoinTickerUtils.tickerToString(coinTicker));
innerParams.add(new Gson().toJsonTree(addresses).getAsJsonArray());

JsonObject params = new JsonObject();
params.addProperty("method", "getutxos");
params.add("params", innerParams);

String res = doPost("/", params);
LOGGER.log(Level.FINER, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " " + res);

if (res == null) {
LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null post result");
return null;
}

JSONObject jsonObject = null;
JSONArray utxoArr = null;
try {
jsonObject = new JSONObject(res);
utxoArr = jsonObject.getJSONArray("utxos");
} catch (Exception e) {
e.printStackTrace();
}

if (jsonObject == null || utxoArr == null) {
if (jsonObject == null)
LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null jsonObject");
if (utxoArr == null)
LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null utxoArr");
return null;
}

JsonArray utxoList = new JsonArray();
for (int i = 0; i < utxoArr.length(); i++) {
JsonObject utxoJSON = new JsonObject();
utxoJSON.addProperty("txid", utxoArr.getJSONObject(i).getString("txhash"));
utxoJSON.addProperty("vout", utxoArr.getJSONObject(i).getInt("vout"));
utxoJSON.addProperty("value", utxoArr.getJSONObject(i).getDouble("value"));
utxoJSON.addProperty("spendable", true);

String address = utxoArr.getJSONObject(i).getString("address");
utxoJSON.addProperty("address", address);

Address addr = Address.fromBase58(coinInstance.getNetworkParameters(), address);
Script script = ScriptBuilder.createOutputScript(addr);
utxoJSON.addProperty("scriptPubKey", new String(Hex.encode(script.getProgram())));

int height = utxoArr.getJSONObject(i).getInt("block_number");
int currentHeight = CoinInstance.getBlockCountByTicker(coinTicker);
int confirmations = (currentHeight - height) + 1;
if (height == 0)
confirmations = 0;

utxoJSON.addProperty("confirmations", confirmations);

utxoList.add(utxoJSON);
}

return utxoList;
}

/**
* Returns all utxos.
* @param coinTicker Fetch utxos from this coin
Expand Down Expand Up @@ -739,4 +816,4 @@ private JsonArray filterHistory(JsonArray txs, int startTime, int endTime) {
private String lastFetchTimesKey(CoinTicker ticker, String method) {
return method + ":" + ticker.name();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -951,66 +951,206 @@ private JsonObject getResponse(String method, JsonArray params) {
break;
}
case "gettxout": {
if (params.size() != 2) {
if (params.size() < 2 || params.size() > 3) {
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();
errorJSON.addProperty("code", -1);
errorJSON.addProperty("message", "Usage: gettxout txid vout\n\ntxid (string, required) - The UTXO's transaction ID\nvout(numeric, required) - The UTXO's vout or n value");
errorJSON.addProperty("message", "Usage: gettxout txid n [include_mempool]\\n" + //
"\\n" + //
"txid (string, required) - The transaction ID\\n" + //
"n (numeric, required) - The vout value\\n" + //
"include_mempool (boolean, optional, default=true) - Whether to include the mempool (WARNING: This can block execution for several seconds)");

response.add("error", errorJSON);
break;
}

Sha256Hash reqTxid = Sha256Hash.wrap(params.get(0).getAsString());
int vout = params.get(1).getAsInt();
String txid = params.get(0).getAsString();
int n = params.get(1).getAsInt();
boolean includeMempool = true;
if (params.size() == 3) {
includeMempool = params.get(2).getAsBoolean();
}

UTXO requested = null;
// attempt to find UTXO in cache
UTXO requested = this.getUtxo(Sha256Hash.wrap(txid), n);

for (AddressBalance addressBalance : coin.getAddressKeyPairs()) {
for (UTXO utxo : addressBalance.getUtxos()) {
if (utxo.createUTXO().getHash().equals(reqTxid) && utxo.createUTXO().getIndex() == vout) {
requested = utxo;
break;
if (requested != null) {
LOGGER.log(Level.FINER, "[http-server-handler] Using cached UTXO for gettxout");

org.bitcoinj.core.UTXO utxo = requested.createUTXO();
JsonObject resultJSON = new JsonObject();

resultJSON.addProperty("confirmations",
(CoinInstance.getBlockCountByTicker(coin.getTicker()) - utxo.getHeight()) + 1);
resultJSON.addProperty("value", utxo.getValue().value / 100000000.0);

JsonObject scriptPubKey = new JsonObject();
scriptPubKey.addProperty("asm", utxo.getScript().toString());
scriptPubKey.addProperty("hex", new String(Hex.encode(utxo.getScript().getProgram())));
scriptPubKey.addProperty("reqSigs", utxo.getScript().getNumberOfSignaturesRequiredToSpend());

Script.ScriptType type = utxo.getScript().getScriptType();
getScriptType(scriptPubKey, type);

JsonArray addresses = new JsonArray();
addresses.add(utxo.getAddress());
scriptPubKey.add("addresses", addresses);

resultJSON.add("scriptPubKey", scriptPubKey);
resultJSON.addProperty("coinbase", utxo.isCoinbase());

response.add("result", resultJSON);
response.add("error", JsonNull.INSTANCE);
break;
}

if (!includeMempool) {
LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that is not ours!");
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();

errorJSON.addProperty("code", -5);
errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (not ours)");
response.add("error", errorJSON);
break;
}

// considering mempool, find/wait for the transaction
JsonObject transaction = null;
int retries = includeMempool ? 5 : 1;
for (int i = 0; i < retries; i++) {
transaction = httpClient.getTransaction(coin.getTicker(), txid, true);
if (transaction == null || transaction.has("result") && transaction.get("result").isJsonNull()) {
if (i < retries - 1) {
try {
Thread.sleep(2000);
} catch (Exception e) {
}
continue;
}
}
if (requested != null)
} else {
break;
}
}

if (requested == null) {
LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that is not ours!");
if (transaction == null || transaction.has("error") && !transaction.get("error").isJsonNull()) {
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();

errorJSON.addProperty("code", -5);
errorJSON.addProperty("message", "Invalid or non-wallet transaction ID");
errorJSON.addProperty("message", "Invalid transaction ID");
response.add("error", errorJSON);
break;
}

org.bitcoinj.core.UTXO utxo = requested.createUTXO();
JsonObject resultJSON = new JsonObject();
// extract the UTXO
try {
// Note: will throw when attempting to parse a coinbase utxo (that's fine)
JsonObject result = transaction.getAsJsonObject("result");
JsonElement confirmations = result.get("confirmations");
JsonArray vout = result.getAsJsonArray("vout");

if (vout.size() <= n) {
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();

resultJSON.addProperty("confirmations", (CoinInstance.getBlockCountByTicker(coin.getTicker()) - utxo.getHeight()) + 1);
resultJSON.addProperty("value", utxo.getValue().value / 100000000.0);
errorJSON.addProperty("code", -5);
errorJSON.addProperty("message", "Invalid transaction output index");
response.add("error", errorJSON);
break;
}

JsonObject entry = vout.get(n).getAsJsonObject();
JsonElement value = entry.get("value");
JsonObject scriptPubKey = entry.getAsJsonObject("scriptPubKey");
JsonElement addr = scriptPubKey.get("address");
JsonArray addresses = scriptPubKey.getAsJsonArray("addresses");

String address = null;
if(addr != null) {
address = addr.getAsString();
} else {
address = addresses.asList().get(0).getAsString();
}

JsonObject scriptPubKey = new JsonObject();
scriptPubKey.addProperty("asm", utxo.getScript().toString());
scriptPubKey.addProperty("hex", new String(Hex.encode(utxo.getScript().getProgram())));
scriptPubKey.addProperty("reqSigs", utxo.getScript().getNumberOfSignaturesRequiredToSpend());
// ensure address belongs to our wallet
boolean isOurs = false;
for (AddressBalance addressBalance : coin.getAddressKeyPairs()) {
String addressCheck = addressBalance.getAddress().toString();
if (address.equals(addressCheck)) {
isOurs = true;
break;
}
}

Script.ScriptType type = utxo.getScript().getScriptType();
getScriptType(scriptPubKey, type);
if (!isOurs) {
LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!");
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();

JsonArray addresses = new JsonArray();
addresses.add(utxo.getAddress());
scriptPubKey.add("addresses", addresses);
errorJSON.addProperty("code", -5);
errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (cannot be ours)");
response.add("error", errorJSON);
break;
}

resultJSON.add("scriptPubKey", scriptPubKey);
resultJSON.addProperty("coinbase", utxo.isCoinbase());
int count = 0;
if (confirmations != null) {
count = confirmations.getAsInt();
}

// ensure UTXO is unspent
// Caution: Beware of race conditions; the backend might return results for 'getTransaction'
// before updating entries for 'getUtxos'. As a result, we only check confirmed transactions.
// Note that unconfirmed UTXOs spent in the memory pool will still be returned, leading to
// a slightly different behavior compared to a core wallet.
if (count > 0) {
// Note: this request is expensive
boolean unspent = false;
JsonArray utxos = httpClient.getUtxosUncached(coin.getTicker(), new String[] { address });
for (JsonElement utxo : utxos.asList()) {
String newtxid = utxo.getAsJsonObject().get("txid").getAsString();
int newvout = utxo.getAsJsonObject().get("vout").getAsInt();
if (newtxid.equals(txid) && newvout == n) {
unspent = true;
break;
}
}

if (!unspent) {
LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that was already spent!");
response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();

errorJSON.addProperty("code", -5);
errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (already spent)");
response.add("error", errorJSON);
break;
}
}

// assemble result
JsonObject resultJSON = new JsonObject();
resultJSON.addProperty("confirmations", count);
resultJSON.add("value", value);
resultJSON.add("scriptPubKey", scriptPubKey);
resultJSON.addProperty("coinbase", false);

response.add("result", resultJSON);
response.add("error", JsonNull.INSTANCE);
} catch (Exception e) {
LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction!");
e.printStackTrace();

response.add("result", JsonNull.INSTANCE);
JsonObject errorJSON = new JsonObject();
errorJSON.addProperty("code", -1010);
errorJSON.addProperty("message", "Error while parsing transaction");

response.add("error", errorJSON);
}

response.add("result", resultJSON);
response.add("error", JsonNull.INSTANCE);
break;
}
case "getnewaddress": {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/cloudchains/app/util/UTXO.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public org.bitcoinj.core.UTXO createUTXO() {
Script scriptForUTXO = ScriptBuilder.createOutputScript(address);

Sha256Hash sha256Hash = Sha256Hash.wrap(getTxid());
return new org.bitcoinj.core.UTXO(sha256Hash, getVout(), Coin.valueOf(getValue()), getHeight(), true, scriptForUTXO, getAddress());
return new org.bitcoinj.core.UTXO(sha256Hash, getVout(), Coin.valueOf(getValue()), getHeight(), false, scriptForUTXO, getAddress());
}

public String toString() {
Expand Down

0 comments on commit fbf1dba

Please sign in to comment.