Unverified Commit 148395e9 authored by TDevD's avatar TDevD Committed by GitHub
Browse files

Merge pull request #2 from Samourai-Wallet/develop

internal release 20181208
parents 7014387f 809ad4e9
language: java
\ No newline at end of file
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
[![Build Status](https://travis-ci.org/Samourai-Wallet/whirlpool-client-cli.svg?branch=develop)](https://travis-ci.org/Samourai-Wallet/whirlpool-client-cli)
[![](https://jitpack.io/v/Samourai-Wallet/whirlpool-client-cli.svg)](https://jitpack.io/#Samourai-Wallet/whirlpool-client-cli)
# whirlpool-client-cli
Command line client for [Whirlpool](https://github.com/Samourai-Wallet/Whirlpool) by Samourai-Wallet.
......@@ -6,7 +9,7 @@ Command line client for [Whirlpool](https://github.com/Samourai-Wallet/Whirlpool
## General usage
```
java -jar target/whirlpool-client-version-run.jar --network={main,test} --server=host:port
[--ssl=true] [--debug] [--pool=] [--test-mode]
[--ssl=true] [--tor=true] [--debug] [--pool=] [--test-mode]
[--rpc-client-url=http://user:password@host:port] {args...}
```
......@@ -16,6 +19,7 @@ java -jar target/whirlpool-client-version-run.jar --network={main,test} --server
### Optional arguments:
- ssl: enable or disable SSL
- tor: enable or disable TOR
- debug: display more logs for debugging
- pool: id of the pool to join
- test-mode: disable tx0 checks, only available when enabled on server
......@@ -39,19 +43,18 @@ You need a wallet holding funds to mix. The script will run the following automa
```
--network={main,test} --server=host:port [--rpc-client-url=http://user:password@host:port] --pool=
--seed-passphrase= --seed-words=
[--clients=1] [--iteration-delay=0] [--client-delay=0] [--auto-aggregate-postmix]
[--clients=1] [--iteration-delay=0] [--client-delay=60] [--auto-aggregate-postmix] [--postmix-index=]
```
Example:
```
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --seed-passphrase=foo --seed-words="all all all all all all all all all all all all" --rpc-client-url=http://user:password@host:port
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --rpc-client-url=http://user:password@host:port
```
- seed-passphrase & seed-words: wallet seed
- clients: number of simultaneous clients to connect
- iteration-delay: delay (in seconds) to wait between mixs
- client-delay: delay (in seconds) between each client connexion
- auto-aggregate-postmix: enable automatically post-mix wallet agregation to refill premix when empty
- postmix-index: force postmix-index instead of reading it from local state. Use --postmix-index=0 to resync local state with API.
## Expert usage
......@@ -60,18 +63,17 @@ You need a valid pre-mix utxo (output of a valid tx0) to mix.
```
--network={main,test} --server=host:port --pool=
--utxo= --utxo-key= --utxo-balance=
--seed-passphrase= --seed-words= [--paynym-index=0]
[--paynym-index=0]
[--mixs=1]
```
Example:
```
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --utxo=5369dfb71b36ed2b91ca43f388b869e617558165e4f8306b80857d88bdd624f2-3 --utxo-key=cN27hV14EEjmwVowfzoeZ9hUGwJDxspuT7N4bQDz651LKmqMUdVs --utxo-balance=100001000 --seed-passphrase=foo --seed-words="all all all all all all all all all all all all" --paynym-index=5
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --utxo=5369dfb71b36ed2b91ca43f388b869e617558165e4f8306b80857d88bdd624f2-3 --utxo-key=cN27hV14EEjmwVowfzoeZ9hUGwJDxspuT7N4bQDz651LKmqMUdVs --utxo-balance=100001000 --paynym-index=5
```
- utxo: (txid:ouput-index) pre-mix input to spend (obtained from a valid tx0)
- utxo-key: ECKey for pre-mix input
- utxo-balance: pre-mix input balance (in satoshis). Whole utxo-balance balance will be spent.
- seed-passphrase & seed-words: wallet seed from which to derive the paynym for computing post-mix address to receive the funds
- paynym-index: paynym index to use for computing post-mix address to receive the funds
- mixs: (1 to N) number of mixes to complete. Client will keep running until completing this number of mixes.
......@@ -80,33 +82,29 @@ java -jar target/whirlpool-client-version-run.jar --network=test --server=host:p
You need a wallet holding funds to split.
```
--network={main,test} --server=host:port [--rpc-client-url=http://user:password@host:port] --pool=
--seed-passphrase= --seed-words=
--tx0=
[--rpc-client-url=http://user:password@host:port]
```
Example:
```
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --seed-passphrase=foo --seed-words="all all all all all all all all all all all all" --tx0=10 --rpc-client-url=http://user:password@host:port
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --tx0=10 --rpc-client-url=http://user:password@host:port
```
- seed-passphrase & seed-words: wallet seed
- tx0: number of pre-mix utxo to generate
### Aggregate postmix
### Aggregate postmix / move funds
Move all postmix funds back to premix wallet and consolidate to a single UTXO.
Only allowed on testnet for testing purpose.
```
--network={main,test} --server=host:port [--rpc-client-url=http://user:password@host:port] --pool=
--seed-passphrase= --seed-words=
[--rpc-client-url=http://user:password@host:port]
--aggregate-postmix
--aggregate-postmix[=address]
```
Example:
```
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --seed-passphrase=foo --seed-words="all all all all all all all all all all all all" --aggregate-postmix --rpc-client-url=http://user:password@host:port
java -jar target/whirlpool-client-version-run.jar --network=test --server=host:port --pool=0.1btc --aggregate-postmix --rpc-client-url=http://user:password@host:port
```
- seed-passphrase & seed-words: wallet seed
- aggregate-postmix: move funds back to premix-wallet. Or --aggregate-postmix=address to move funds to a specific address.
## Build instructions
Build with maven:
......
......@@ -9,6 +9,8 @@
<properties>
<spring.version>2.0.3.RELEASE</spring.version>
<spring-websocket.version>5.0.7.RELEASE</spring-websocket.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
......@@ -18,9 +20,9 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.Polve</groupId>
<groupId>wf.bitcoin</groupId>
<artifactId>JavaBitcoindRpcClient</artifactId>
<version>0.17.x-SNAPSHOT</version>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
......@@ -41,6 +43,16 @@
<version>${spring-websocket.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.silvertunnel-ng</groupId>
<artifactId>netlib</artifactId>
<version>0.0.5</version>
</dependency>
<dependency>
<groupId>com.github.kevinsawicki</groupId>
<artifactId>http-request</artifactId>
<version>5.6</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -79,13 +91,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
......
......@@ -6,6 +6,7 @@ import com.samourai.http.client.IHttpClient;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
......@@ -17,6 +18,7 @@ public class SamouraiApi {
private static final String URL_BACKEND = "https://api.samouraiwallet.com/test";
private static final String URL_UNSPENT = "/v2/unspent?active=";
private static final String URL_MULTIADDR = "/v2/multiaddr?active=";
private static final String URL_INIT_BIP84 = "/v2/xpub";
private static final String URL_FEES = "/v2/fees";
private static final int MAX_FEE_PER_BYTE = 500;
private static final int FAILOVER_FEE_PER_BYTE = 400;
......@@ -73,6 +75,18 @@ public class SamouraiApi {
return address;
}
public void initBip84(String zpub) throws Exception {
String url = URL_BACKEND + URL_INIT_BIP84;
if (log.isDebugEnabled()) {
log.debug("initBip84: zpub=" + zpub);
}
Map<String, String> postBody = new HashMap<>();
postBody.put("xpub", zpub);
postBody.put("type", "new");
postBody.put("segwit", "bip84");
httpClient.postUrlEncoded(url, postBody);
}
public int fetchFees() {
return fetchFees(true);
}
......
package com.samourai.http.client;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.kevinsawicki.http.HttpRequest;
import com.samourai.tor.client.JavaTorClient;
import com.samourai.tor.client.JavaTorConnexion;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JavaHttpClient implements IHttpClient {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private Optional<JavaTorClient> torClient;
private ObjectMapper objectMapper;
public JavaHttpClient(Optional<JavaTorClient> torClient) {
this.torClient = torClient;
this.objectMapper = new ObjectMapper();
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
@Override
public <T> T parseJson(String urlStr, Class<T> entityClass) throws HttpException {
try {
HttpRequest request;
if (torClient.isPresent()) {
// use TOR - same circuit for all GET requests
JavaTorConnexion sharedTorConnexion = torClient.get().getConnexion(false);
URL url = sharedTorConnexion.getUrl(urlStr);
request = HttpRequest.get(url);
} else {
// standard connexion
request = HttpRequest.get(urlStr);
}
checkResponseSuccess(request);
T result = objectMapper.readValue(request.bytes(), entityClass);
// keep sharedTorConnexion open
return result;
} catch (Exception e) {
if (!(e instanceof HttpException)) {
e = new HttpException(e, null);
}
throw (HttpException) e;
}
}
@Override
public <T> T parseJson(String url, Class<T> entityClass) throws HttpException {
RestTemplate restTemplate = new RestTemplate();
public void postJsonOverTor(String urlStr, Object bodyObj) throws HttpException {
JavaTorConnexion privateTorConnexion = null;
try {
ResponseEntity<T> result = restTemplate.getForEntity(url, entityClass);
if (result == null || !result.getStatusCode().is2xxSuccessful()) {
// response error
String responseBody = null;
throw new HttpException(new Exception("unable to retrieve pools"), responseBody);
}
return result.getBody();
} catch (RestClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
throw new HttpException(e, responseBody);
String jsonBody = objectMapper.writeValueAsString(bodyObj);
HttpRequest request;
if (torClient.isPresent()) {
// different circuit for each POST request
privateTorConnexion = torClient.get().getConnexion(true);
URL url = privateTorConnexion.getUrl(urlStr);
request = HttpRequest.post(url);
} else {
// standard connexion
request = HttpRequest.post(urlStr);
}
request.contentType(HttpRequest.CONTENT_TYPE_JSON).send(jsonBody.getBytes());
checkResponseSuccess(request);
if (privateTorConnexion != null) {
privateTorConnexion.close();
}
} catch (Exception e) {
if (!(e instanceof HttpException)) {
e = new HttpException(e, null);
}
if (privateTorConnexion != null) {
privateTorConnexion.close();
}
throw (HttpException) e;
}
}
@Override
public void postJsonOverTor(String url, Object body) throws HttpException {
public void postUrlEncoded(String urlStr, Map<String, String> body) throws HttpException {
String bodyUrlEncoded = HttpRequest.append("", body).substring(1); // remove starting '?'
JavaTorConnexion privateTorConnexion = null;
try {
// TODO use TOR
RestTemplate restTemplate = new RestTemplate();
ResponseEntity result = restTemplate.postForEntity(url, body, null);
if (result == null || !result.getStatusCode().is2xxSuccessful()) {
// response error
String responseBody = null;
throw new HttpException(new Exception("statusCode not successful"), responseBody);
}
} catch (RestClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
throw new HttpException(e, responseBody);
HttpRequest request;
if (torClient.isPresent()) {
// different circuit for each POST request
privateTorConnexion = torClient.get().getConnexion(true);
URL url = privateTorConnexion.getUrl(urlStr);
request = HttpRequest.post(url);
} else {
// standard connexion
request = HttpRequest.post(urlStr);
}
request.contentType(HttpRequest.CONTENT_TYPE_FORM).send(bodyUrlEncoded.getBytes());
checkResponseSuccess(request);
if (privateTorConnexion != null) {
privateTorConnexion.close();
}
} catch (Exception e) {
if (!(e instanceof HttpException)) {
e = new HttpException(e, null);
}
if (privateTorConnexion != null) {
privateTorConnexion.close();
}
throw (HttpException) e;
}
}
private void checkResponseSuccess(HttpRequest request) throws HttpException {
if (!request.ok()) {
throw new HttpException(new Exception("statusCode=" + request.code()), request.body());
}
}
}
package com.samourai.stomp.client;
import com.samourai.tor.client.JavaTorClient;
import com.samourai.whirlpool.client.Application;
import java.util.Map;
import java.util.Optional;
import javax.websocket.MessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -19,6 +21,11 @@ public class JavaStompClient implements IStompClient {
private String stompSessionId;
private StompHeaders connectedHeaders;
private Optional<JavaTorClient> torClient;
public JavaStompClient(Optional<JavaTorClient> torClient) {
this.torClient = torClient;
}
@Override
public void connect(
......
package com.samourai.tor.client;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import org.silvertunnel_ng.netlib.api.NetFactory;
import org.silvertunnel_ng.netlib.api.NetLayerIDs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JavaTorClient {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private NetFactory sharedNetFactory;
private List<NetFactory> netFactories = new ArrayList<>();
private int nbPrivateConnexions;
public JavaTorClient() {
this.nbPrivateConnexions = 1;
}
public void connect() {
// start connecting
adjustPrivateConnexions();
}
public void disconnect() {
if (sharedNetFactory != null) {
sharedNetFactory.clearRegisteredNetLayers();
sharedNetFactory = null;
}
netFactories.forEach(privateNetFactory -> privateNetFactory.clearRegisteredNetLayers());
netFactories.clear();
}
public boolean isConnected() {
return sharedNetFactory != null;
}
public JavaTorConnexion getConnexion(boolean privateCircuit) {
NetFactory netFactory = getNetFactory(privateCircuit);
return new JavaTorConnexion(netFactory);
}
private synchronized NetFactory getNetFactory(boolean privateCircuit) {
if (!isConnected()) {
connect();
}
NetFactory netFactory = privateCircuit ? getNetFactoryReady() : getSharedNetFactory();
return netFactory;
}
private synchronized NetFactory getSharedNetFactory() {
if (sharedNetFactory == null) {
sharedNetFactory = getNetFactoryReady();
}
return sharedNetFactory;
}
private synchronized NetFactory getNetFactoryReady() {
// get first NetFactory ready
NetFactory netFactory = null;
double lastBestIndicator = -1;
while (true) {
double bestIndicator = 0;
for (NetFactory nf : netFactories) {
double indicator = nf.getNetLayerById(NetLayerIDs.TOR).getStatus().getReadyIndicator();
if (indicator == 1.0) {
netFactory = nf;
break;
}
if (indicator > bestIndicator) {
bestIndicator = indicator;
}
}
if (netFactory != null) {
break;
}
if (bestIndicator != lastBestIndicator) {
log.info("Connecting TOR... (" + (Math.round(bestIndicator) * 100) + "%)");
lastBestIndicator = bestIndicator;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
// remove
netFactories.remove(netFactory);
// create a new one for next time
netFactories.add(createNetFactory());
return netFactory;
}
public void waitConnexionReady(int nbConnexions) {
for (int i = 0; i < nbConnexions; i++) {
double lastBestIndicator = -1;
while (true) {
int nbReady = 0;
double bestIndicator = 0;
for (NetFactory nf : netFactories) {
double indicator = nf.getNetLayerById(NetLayerIDs.TOR).getStatus().getReadyIndicator();
if (indicator == 1.0) {
nbReady++;
if (nbReady == nbConnexions) {
return;
}
} else {
if (indicator > bestIndicator) {
bestIndicator = indicator;
lastBestIndicator = bestIndicator;
}
}
}
if (bestIndicator != lastBestIndicator) {
log.info(
"Connecting TOR "
+ nbReady
+ "/"
+ nbConnexions
+ "... ("
+ (Math.round(bestIndicator) * 100)
+ "%)");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
private void adjustPrivateConnexions() {
int nbToAdd = nbPrivateConnexions - netFactories.size();
if (nbToAdd == 0) {
return;
}
if (nbToAdd > 0) {
// create missing connexions
for (int i = 0; i < nbToAdd; i++) {
if (log.isDebugEnabled()) {
log.debug("New private connexion: " + (i + 1) + "/" + nbToAdd);
}
netFactories.add(createNetFactory());
}
} else {
// remove exceeding connexions
int nbToClose = -nbToAdd;
for (int i = 0; i < nbToClose; i++) {
if (log.isDebugEnabled()) {
log.debug("Closing private connexion: " + (i + 1) + "/" + nbToClose);
}
getNetFactory(true).clearRegisteredNetLayers();
}
}
}
private NetFactory createNetFactory() {
NetFactory netFactory = new NetFactory();
netFactory.getNetLayerById(NetLayerIDs.TOR); // start connecting
return netFactory;
}
protected synchronized void removeConnexion(NetFactory netFactory) {
this.netFactories.remove(netFactory);
adjustPrivateConnexions();
}
public void setNbPrivateConnexions(int nbPrivateConnexions) {
this.nbPrivateConnexions = nbPrivateConnexions;
adjustPrivateConnexions();
}
}
package com.samourai.tor.client;
import java.net.URL;
import java.net.URLStreamHandler;
import org.silvertunnel_ng.netlib.adapter.url.NetlibURLStreamHandlerFactory;
import org.silvertunnel_ng.netlib.api.NetFactory;
import org.silvertunnel_ng.netlib.api.NetLayer;
import org.silvertunnel_ng.netlib.api.NetLayerIDs;
public class JavaTorConnexion {
private NetFactory netFactory;
public JavaTorConnexion(NetFactory netFactory) {
this.netFactory = netFactory;
}
public URL getUrl(String urlStr) throws Exception {
NetlibURLStreamHandlerFactory streamHandlerFactory = computeStreamHandlerFactory(netFactory);
String protocol = urlStr.split("://")[0];
URLStreamHandler handler = streamHandlerFactory.createURLStreamHandler(protocol);
URL url = new URL(null, urlStr, handler);
return url;
}
private NetlibURLStreamHandlerFactory computeStreamHandlerFactory(NetFactory netFactory) {
NetLayer netLayer = netFactory.getNetLayerById(NetLayerIDs.TOR);
netLayer.waitUntilReady(); // wait connected
NetlibURLStreamHandlerFactory urlFactory = new NetlibURLStreamHandlerFactory(false);
urlFactory.setNetLayerForHttpHttpsFtp(netLayer);
return urlFactory;
}
public void close() {
netFactory.clearRegisteredNetLayers();