remove legacy standalone application

This commit is contained in:
Craig Raw 2024-01-29 12:05:14 +02:00
parent a436de319a
commit 562d82ddaf
9 changed files with 0 additions and 601 deletions

View file

@ -9,7 +9,6 @@ buildscript {
plugins { plugins {
id 'java-library' id 'java-library'
id 'extra-java-module-info' id 'extra-java-module-info'
id 'com.github.johnrengelman.shadow' version '5.2.0'
} }
tasks.withType(AbstractArchiveTask) { tasks.withType(AbstractArchiveTask) {
@ -17,26 +16,17 @@ tasks.withType(AbstractArchiveTask) {
reproducibleFileOrder = true reproducibleFileOrder = true
} }
group 'com.sparrowwallet'
version '1.0'
def os = org.gradle.internal.os.OperatingSystem.current() def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName() def osName = os.getFamilyName()
if(os.macOsX) { if(os.macOsX) {
osName = "osx" osName = "osx"
} }
sourceCompatibility = 16
targetCompatibility = 16
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { 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') { implementation ('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core' exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'junit', module: 'junit' 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 { extraJavaModuleInfo {
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') { module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
exports('ch.qos.logback.core') exports('ch.qos.logback.core')
@ -106,9 +74,6 @@ extraJavaModuleInfo {
requires('java.xml') requires('java.xml')
requires('java.logging') 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') { module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple') exports('org.json.simple')
exports('org.json.simple.parser') exports('org.json.simple.parser')

View file

@ -1,83 +1,12 @@
package com.sparrowwallet.drongo; package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.rpc.BitcoinJSONRPCClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.event.Level; import org.slf4j.event.Level;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import java.security.Provider; 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 { public class Drongo {
private static final Logger log = LoggerFactory.getLogger(Drongo.class);
private String nodeZmqAddress;
private BitcoinJSONRPCClient bitcoinJSONRPCClient;
private List<WatchWallet> watchWallets;
private String[] notifyRecipients;
public Drongo(String nodeZmqAddress, Map<String, String> nodeRpc, List<WatchWallet> 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<WatchWallet> getWallets() {
return watchWallets;
}
public static void setRootLogLevel(Level level) { 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); 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())); root.setLevel(ch.qos.logback.classic.Level.toLevel(level.toString()));

View file

@ -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<String, String> rpcConnection = new LinkedHashMap<String, String>() {
{
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<WatchWallet> 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;
}
}

View file

@ -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<String, Transaction> 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<Address> fromAddresses = new ArrayList<>();
for(TransactionInput input : transaction.getInputs()) {
for(Address address : input.getOutpoint().getAddresses()) {
if(wallet.containsAddress(address)) {
fromAddresses.add(address);
}
}
}
Map<Address,Long> 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());
}
}
}
}

View file

@ -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<String, Object>() {
{
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);
}
}

View file

@ -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;
}
}

View file

@ -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 <code>BitcoinRPCException</code> 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 <code>BitcoinRPCException</code> 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;
}
}

View file

@ -5,7 +5,6 @@ open module com.sparrowwallet.drongo {
requires logback.core; requires logback.core;
requires logback.classic; requires logback.classic;
requires json.simple; requires json.simple;
requires jeromq;
exports com.sparrowwallet.drongo; exports com.sparrowwallet.drongo;
exports com.sparrowwallet.drongo.psbt; exports com.sparrowwallet.drongo.psbt;
exports com.sparrowwallet.drongo.protocol; exports com.sparrowwallet.drongo.protocol;

View file

@ -1,19 +0,0 @@
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>drongo.log</file>
<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %level %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
</root>
</configuration>