diff --git a/build.gradle b/build.gradle index a3d667c..ca47f99 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,6 @@ buildscript { plugins { id 'java-library' id 'extra-java-module-info' - id 'com.github.johnrengelman.shadow' version '5.2.0' } tasks.withType(AbstractArchiveTask) { @@ -17,26 +16,17 @@ tasks.withType(AbstractArchiveTask) { reproducibleFileOrder = true } -group 'com.sparrowwallet' -version '1.0' - def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { osName = "osx" } -sourceCompatibility = 16 -targetCompatibility = 16 - repositories { mavenCentral() } dependencies { - implementation ('org.zeromq:jeromq:0.5.0') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } implementation ('com.googlecode.json-simple:json-simple:1.1.1') { exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'junit', module: 'junit' @@ -70,28 +60,6 @@ processResources { } } -task(runDrongo, dependsOn: 'classes', type: JavaExec) { - mainClass = 'com.sparrowwallet.drongo.Main' - classpath = sourceSets.main.runtimeClasspath - args 'drongo.properties' -} - -jar { - manifest { - attributes "Main-Class": "com.sparrowwallet.drongo.Main" - } - - exclude('logback.xml') - - archiveBaseName = 'drongo' - archiveVersion = '0.9' -} - -shadowJar { - archiveVersion = '0.9' - classifier = 'all' -} - extraJavaModuleInfo { module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') { exports('ch.qos.logback.core') @@ -106,9 +74,6 @@ extraJavaModuleInfo { requires('java.xml') requires('java.logging') } - module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') { - exports('org.zeromq') - } module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') { exports('org.json.simple') exports('org.json.simple.parser') diff --git a/src/main/java/com/sparrowwallet/drongo/Drongo.java b/src/main/java/com/sparrowwallet/drongo/Drongo.java index 58bc521..b2b0d7e 100644 --- a/src/main/java/com/sparrowwallet/drongo/Drongo.java +++ b/src/main/java/com/sparrowwallet/drongo/Drongo.java @@ -1,83 +1,12 @@ package com.sparrowwallet.drongo; -import com.sparrowwallet.drongo.rpc.BitcoinJSONRPCClient; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; -import org.zeromq.SocketType; -import org.zeromq.ZContext; -import org.zeromq.ZMQ; import java.security.Provider; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class Drongo { - private static final Logger log = LoggerFactory.getLogger(Drongo.class); - - private String nodeZmqAddress; - private BitcoinJSONRPCClient bitcoinJSONRPCClient; - private List watchWallets; - private String[] notifyRecipients; - - public Drongo(String nodeZmqAddress, Map nodeRpc, List watchWallets, String[] notifyRecipients) { - this.nodeZmqAddress = nodeZmqAddress; - this.bitcoinJSONRPCClient = new BitcoinJSONRPCClient(nodeRpc.get("host"), nodeRpc.get("port"), nodeRpc.get("user"), nodeRpc.get("password")); - this.watchWallets = watchWallets; - this.notifyRecipients = notifyRecipients; - - for(WatchWallet wallet : watchWallets) { - wallet.initialiseAddresses(); - } - } - - public void start() { - ExecutorService executorService = null; - - try { - executorService = Executors.newFixedThreadPool(2); - - try (ZContext context = new ZContext()) { - ZMQ.Socket subscriber = context.createSocket(SocketType.SUB); - subscriber.setRcvHWM(0); - subscriber.connect(nodeZmqAddress); - - String subscription = "rawtx"; - subscriber.subscribe(subscription.getBytes(ZMQ.CHARSET)); - - while (true) { - String topic = subscriber.recvStr(); - if (topic == null) - break; - byte[] data = subscriber.recv(); - assert (topic.equals(subscription)); - - if(subscriber.hasReceiveMore()) { - byte[] endData = subscriber.recv(); - } - - TransactionTask transactionTask = new TransactionTask(this, data); - executorService.submit(transactionTask); - } - } - } finally { - if(executorService != null) { - executorService.shutdown(); - } - } - } - - public BitcoinJSONRPCClient getBitcoinJSONRPCClient() { - return bitcoinJSONRPCClient; - } - - public List getWallets() { - return watchWallets; - } - public static void setRootLogLevel(Level level) { ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); root.setLevel(ch.qos.logback.classic.Level.toLevel(level.toString())); diff --git a/src/main/java/com/sparrowwallet/drongo/Main.java b/src/main/java/com/sparrowwallet/drongo/Main.java deleted file mode 100644 index 205b81e..0000000 --- a/src/main/java/com/sparrowwallet/drongo/Main.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.sparrowwallet.drongo; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.*; - -public class Main { - private static final Logger log = LoggerFactory.getLogger(Main.class); - - public static void main(String [] args) { - String propertiesFile = "./drongo.properties"; - if(args.length > 0) { - propertiesFile = args[0]; - } - - Properties properties = new Properties(); - properties.setProperty("nodeAddress", "localhost"); - - try { - File file = new File(propertiesFile); - properties.load(new FileInputStream(propertiesFile)); - log.info("Loaded properties from " + file.getCanonicalPath()); - } catch (IOException e) { - log.error("Could not load properties from provided path " + propertiesFile); - } - - String nodeZmqAddress = properties.getProperty("node.zmqpubrawtx"); - if(nodeZmqAddress == null) { - log.error("Property node.zmqpubrawtx not set, provide the zmqpubrawtx setting of the local node"); - System.exit(1); - } - - Map rpcConnection = new LinkedHashMap() { - { - put("host", properties.getProperty("node.rpcconnect", "127.0.0.1")); - put("port", properties.getProperty("node.rpcport", "8332")); - put("user", properties.getProperty("node.rpcuser")); - put("password", properties.getProperty("node.rpcpassword")); - } - }; - - List watchWallets = new ArrayList<>(); - int walletNumber = 1; - WatchWallet wallet = getWalletFromProperties(properties, walletNumber); - if(wallet == null) { - log.error("Property wallet.name.1 and/or wallet.descriptor.1 not set, provide wallet name and Base58 encoded key starting with xpub or ypub"); - System.exit(1); - } - while(wallet != null) { - watchWallets.add(wallet); - wallet = getWalletFromProperties(properties, ++walletNumber); - } - - String notifyRecipients = properties.getProperty("notify.recipients"); - if(notifyRecipients == null) { - log.error("Property notify.recipients not set, provide comma separated email addresses to receive wallet change notifications"); - System.exit(1); - } - - Drongo drongo = new Drongo(nodeZmqAddress, rpcConnection, watchWallets, notifyRecipients.split(",")); - drongo.start(); - } - - private static WatchWallet getWalletFromProperties(Properties properties, int walletNumber) { - String walletName = properties.getProperty("wallet.name." + walletNumber); - String walletDescriptor = properties.getProperty("wallet.descriptor." + walletNumber); - if(walletName != null && walletDescriptor != null) { - return new WatchWallet(walletName, walletDescriptor); - } - - return null; - } -} diff --git a/src/main/java/com/sparrowwallet/drongo/TransactionTask.java b/src/main/java/com/sparrowwallet/drongo/TransactionTask.java deleted file mode 100644 index bbb1e28..0000000 --- a/src/main/java/com/sparrowwallet/drongo/TransactionTask.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.sparrowwallet.drongo; - -import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.protocol.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; - -public class TransactionTask implements Runnable { - private static final Logger log = LoggerFactory.getLogger(Drongo.class); - - private Drongo drongo; - private byte[] transactionData; - - public TransactionTask(Drongo drongo, byte[] transactionData) { - this.drongo = drongo; - this.transactionData = transactionData; - } - - @Override - public void run() { - Transaction transaction = new Transaction(transactionData); - Map referencedTransactions = new HashMap<>(); - - Sha256Hash txid = transaction.getTxId(); - StringBuilder builder = new StringBuilder("Txid: " + txid.toString() + " "); - StringJoiner inputJoiner = new StringJoiner(", ", "[", "]"); - - int vin = 0; - for(TransactionInput input : transaction.getInputs()) { - if(input.isCoinBase()) { - inputJoiner.add("Coinbase:" + vin); - } else { - String referencedTxID = input.getOutpoint().getHash().toString(); - long referencedVout = input.getOutpoint().getIndex(); - - Transaction referencedTransaction = referencedTransactions.get(referencedTxID); - if(referencedTransaction == null) { - String referencedTransactionHex = drongo.getBitcoinJSONRPCClient().getRawTransaction(referencedTxID); - referencedTransaction = new Transaction(Utils.hexToBytes(referencedTransactionHex)); - referencedTransactions.put(referencedTxID, referencedTransaction); - } - - TransactionOutput referencedOutput = referencedTransaction.getOutputs().get((int)referencedVout); - if(referencedOutput.getScript().containsToAddress()) { - try { - Address[] inputAddresses = referencedOutput.getScript().getToAddresses(); - input.getOutpoint().setAddresses(inputAddresses); - inputJoiner.add((inputAddresses.length == 1 ? inputAddresses[0] : Arrays.asList(inputAddresses)) + ":" + vin); - } catch(NonStandardScriptException e) { - //Cannot happen - } - } else { - log.warn("Could not determine nature of referenced input tx: " + referencedTxID + ":" + referencedVout); - } - } - - vin++; - } - - builder.append(inputJoiner.toString() + " => "); - StringJoiner outputJoiner = new StringJoiner(", ", "[", "]"); - - int vout = 0; - for(TransactionOutput output : transaction.getOutputs()) { - try { - if(output.getScript().containsToAddress()) { - try { - Address[] outputAddresses = output.getScript().getToAddresses(); - output.setAddresses(outputAddresses); - outputJoiner.add((outputAddresses.length == 1 ? outputAddresses[0] : Arrays.asList(outputAddresses)) + ":" + vout + " (" + output.getValue() + ")"); - } catch(NonStandardScriptException e) { - //Cannot happen - } - } - } catch(ProtocolException e) { - log.debug("Invalid script for output " + vout + " detected (" + e.getMessage() + "). Skipping..."); - } - - vout++; - } - - builder.append(outputJoiner.toString()); - log.debug(builder.toString()); - - checkWallet(transaction); - } - - private void checkWallet(Transaction transaction) { - for(WatchWallet wallet : drongo.getWallets()) { - List
fromAddresses = new ArrayList<>(); - for(TransactionInput input : transaction.getInputs()) { - for(Address address : input.getOutpoint().getAddresses()) { - if(wallet.containsAddress(address)) { - fromAddresses.add(address); - } - } - } - - Map toAddresses = new HashMap<>(); - for(TransactionOutput output : transaction.getOutputs()) { - for(Address address : output.getAddresses()) { - if(wallet.containsAddress(address)) { - toAddresses.put(address, output.getValue()); - } - } - } - - if(!fromAddresses.isEmpty()) { - StringBuilder builder = new StringBuilder(); - builder.append("Wallet ").append(wallet.getName()).append(" sent from address").append(fromAddresses.size() == 1 ? " " : "es "); - StringJoiner fromJoiner = new StringJoiner(", ", "[", "]"); - for(Address address : fromAddresses) { - fromJoiner.add(address.toString() + " [" + Utils.formatHDPath(wallet.getAddressPath(address)) + "]"); - } - builder.append(fromJoiner.toString()).append(" in txid ").append(transaction.getTxId()); - log.info(builder.toString()); - } - - if(!toAddresses.isEmpty()) { - StringBuilder builder = new StringBuilder(); - builder.append("Wallet ").append(wallet.getName()).append(" received to address").append(toAddresses.size() == 1 ? " " : "es "); - StringJoiner toJoiner = new StringJoiner(", ", "[", "]"); - for(Address address : toAddresses.keySet()) { - toJoiner.add(address.toString() + " [" + Utils.formatHDPath(wallet.getAddressPath(address)) + "]" + " (" + toAddresses.get(address) + " sats)"); - } - builder.append(toJoiner.toString()).append(" in txid ").append(transaction.getTxId()); - log.info(builder.toString()); - } - } - } -} diff --git a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinJSONRPCClient.java b/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinJSONRPCClient.java deleted file mode 100644 index 0578427..0000000 --- a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinJSONRPCClient.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.sparrowwallet.drongo.rpc; - -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.*; -import java.nio.charset.Charset; -import java.util.*; - -public class BitcoinJSONRPCClient { - private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class); - public static final Charset QUERY_CHARSET = Charset.forName("ISO8859-1"); - public static final String RESPONSE_ID = "drongo"; - - public final URL rpcURL; - private final URL noAuthURL; - private final String authStr; - - public BitcoinJSONRPCClient(String host, String port, String user, String password) { - this.rpcURL = getConnectUrl(host, port, user, password); - - try { - this.noAuthURL = new URI(rpcURL.getProtocol(), null, rpcURL.getHost(), rpcURL.getPort(), rpcURL.getPath(), rpcURL.getQuery(), null).toURL(); - } catch (MalformedURLException | URISyntaxException ex) { - throw new IllegalArgumentException(rpcURL.toString(), ex); - } - - this.authStr = rpcURL.getUserInfo() == null ? null : new String(Base64.getEncoder().encode(rpcURL.getUserInfo().getBytes(QUERY_CHARSET)), QUERY_CHARSET); - } - - private URL getConnectUrl(String host, String port, String user, String password) { - try { - return new URL("http://" + user + ':' + password + "@" + host + ":" + (port == null ? "8332" : port) + "/"); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Invalid RPC connection details", e); - } - } - - public Object query(String method, Object... o) throws BitcoinRPCException { - HttpURLConnection conn; - try { - conn = (HttpURLConnection) noAuthURL.openConnection(); - - conn.setDoOutput(true); - conn.setDoInput(true); - - conn.setRequestProperty("Authorization", "Basic " + authStr); - byte[] r = prepareRequest(method, o); - log.debug("Bitcoin JSON-RPC request: " + new String(r, QUERY_CHARSET)); - conn.getOutputStream().write(r); - conn.getOutputStream().close(); - int responseCode = conn.getResponseCode(); - if (responseCode != 200) { - InputStream errorStream = conn.getErrorStream(); - throw new BitcoinRPCException(method, - Arrays.deepToString(o), - responseCode, - conn.getResponseMessage(), - errorStream == null ? null : new String(loadStream(errorStream, true))); - } - return loadResponse(conn.getInputStream(), RESPONSE_ID, true); - } catch (IOException ex) { - throw new BitcoinRPCException(method, Arrays.deepToString(o), ex); - } - } - - protected byte[] prepareRequest(final String method, final Object... params) { - return JSONObject.toJSONString(new LinkedHashMap() { - { - put("method", method); - put("params", Arrays.asList(params)); - put("id", RESPONSE_ID); - put("jsonrpc", "1.0"); - } - }).getBytes(QUERY_CHARSET); - } - - private static byte[] loadStream(InputStream in, boolean close) throws IOException { - ByteArrayOutputStream o = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - for (;;) { - int nr = in.read(buffer); - - if (nr == -1) - break; - if (nr == 0) - throw new IOException("Read timed out"); - - o.write(buffer, 0, nr); - } - return o.toByteArray(); - } - - @SuppressWarnings("rawtypes") - public Object loadResponse(InputStream in, Object expectedID, boolean close) throws IOException, BitcoinRPCException { - try { - String r = new String(loadStream(in, close), QUERY_CHARSET); - log.debug("Bitcoin JSON-RPC response: " + r); - try { - JSONParser jsonParser = new JSONParser(); - Map response = (Map) jsonParser.parse(r); - - if (!expectedID.equals(response.get("id"))) - throw new BitcoinRPCException("Wrong response ID (expected: " + String.valueOf(expectedID) + ", response: " + response.get("id") + ")"); - - if (response.get("error") != null) - throw new BitcoinRPCException(new BitcoinRPCError((Map)response.get("error"))); - - return response.get("result"); - } catch (ClassCastException | ParseException ex) { - throw new BitcoinRPCException("Invalid server response format (data: \"" + r + "\")"); - } - } finally { - if (close) - in.close(); - } - } - - public String getRawTransaction(String txId) throws BitcoinRPCException { - return (String) query("getrawtransaction", txId); - } -} diff --git a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCError.java b/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCError.java deleted file mode 100644 index bab37d2..0000000 --- a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCError.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sparrowwallet.drongo.rpc; - -import java.util.Map; - -public class BitcoinRPCError { - private int code; - private String message; - - @SuppressWarnings({ "rawtypes" }) - public BitcoinRPCError(Map errorMap) { - Number n = (Number) errorMap.get("code"); - this.code = n != null ? n.intValue() : 0; - this.message = (String) errorMap.get("message"); - } - - public int getCode() { - return code; - } - - public String getMessage() { - return message; - } -} diff --git a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCException.java b/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCException.java deleted file mode 100644 index 282fce0..0000000 --- a/src/main/java/com/sparrowwallet/drongo/rpc/BitcoinRPCException.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.sparrowwallet.drongo.rpc; - -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -public class BitcoinRPCException extends RuntimeException { - private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class); - - private String rpcMethod; - private String rpcParams; - private int responseCode; - private String responseMessage; - private String response; - private BitcoinRPCError rpcError; - - /** - * Creates a new instance of BitcoinRPCException with response - * detail. - * - * @param method the rpc method called - * @param params the parameters sent - * @param responseCode the HTTP code received - * @param responseMessage the HTTP response message - * @param response the error stream received - */ - @SuppressWarnings("rawtypes") - public BitcoinRPCException(String method, - String params, - int responseCode, - String responseMessage, - String response) { - super("RPC Query Failed (method: " + method + ", params: " + params + ", response code: " + responseCode + " responseMessage " + responseMessage + ", response: " + response); - this.rpcMethod = method; - this.rpcParams = params; - this.responseCode = responseCode; - this.responseMessage = responseMessage; - this.response = response; - if ( responseCode == 500 ) { - // Bitcoind application error when handle the request - // extract code/message for callers to handle - try { - JSONParser jsonParser = new JSONParser(); - Map error = (Map) ((Map)jsonParser.parse(response)).get("error"); - if ( error != null ) { - rpcError = new BitcoinRPCError(error); - } - } catch(ParseException e) { - log.error("Could not parse bitcoind error", e); - } - } - } - - public BitcoinRPCException(String method, String params, Throwable cause) { - super("RPC Query Failed (method: " + method + ", params: " + params + ")", cause); - this.rpcMethod = method; - this.rpcParams = params; - } - - /** - * Constructs an instance of BitcoinRPCException with the - * specified detail message. - * - * @param msg the detail message. - */ - public BitcoinRPCException(String msg) { - super(msg); - } - - public BitcoinRPCException(BitcoinRPCError error) { - super(error.getMessage()); - this.rpcError = error; - } - - public BitcoinRPCException(String message, Throwable cause) { - super(message, cause); - } - - public int getResponseCode() { - return responseCode; - } - - public String getRpcMethod() { - return rpcMethod; - } - - public String getRpcParams() { - return rpcParams; - } - - /** - * @return the HTTP response message - */ - public String getResponseMessage() { - return responseMessage; - } - - /** - * @return response message from bitcored - */ - public String getResponse() { - return this.response; - } - - /** - * @return response message from bitcored - */ - public BitcoinRPCError getRPCError() { - return this.rpcError; - } -} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8e48428..5ce479c 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -5,7 +5,6 @@ open module com.sparrowwallet.drongo { requires logback.core; requires logback.classic; requires json.simple; - requires jeromq; exports com.sparrowwallet.drongo; exports com.sparrowwallet.drongo.psbt; exports com.sparrowwallet.drongo.protocol; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 59cfd65..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - drongo.log - - %date %level [%thread] %logger{10} [%file:%line] %msg%n - - - - - - %date %level %msg%n - - - - - - - - \ No newline at end of file