├── .travis.yml ├── src ├── main │ ├── resources │ │ ├── keystore │ │ │ └── whirlpool.p12 │ │ ├── native │ │ │ ├── osx │ │ │ │ └── x64 │ │ │ │ │ └── tor.zip │ │ │ ├── linux │ │ │ │ ├── x64 │ │ │ │ │ └── tor.zip │ │ │ │ └── x86 │ │ │ │ │ └── tor.zip │ │ │ └── windows │ │ │ │ └── x86 │ │ │ │ └── tor.zip │ │ └── application.properties │ └── java │ │ └── com │ │ ├── samourai │ │ ├── whirlpool │ │ │ └── cli │ │ │ │ ├── beans │ │ │ │ ├── CliStatus.java │ │ │ │ ├── CliResult.java │ │ │ │ ├── CliProxyProtocol.java │ │ │ │ ├── CliTorExecutableMode.java │ │ │ │ ├── Encrypted.java │ │ │ │ ├── CliState.java │ │ │ │ ├── CliProxy.java │ │ │ │ └── WhirlpoolPairingPayload.java │ │ │ │ ├── api │ │ │ │ ├── protocol │ │ │ │ │ ├── rest │ │ │ │ │ │ ├── ApiTx0Request.java │ │ │ │ │ │ ├── ApiUtxoConfigureRequest.java │ │ │ │ │ │ ├── ApiCliLoginRequest.java │ │ │ │ │ │ ├── ApiCliInitRequest.java │ │ │ │ │ │ ├── ApiCliInitResponse.java │ │ │ │ │ │ ├── ApiDepositResponse.java │ │ │ │ │ │ ├── ApiTx0PreviewRequest.java │ │ │ │ │ │ ├── ApiTx0Response.java │ │ │ │ │ │ ├── ApiCliConfigResponse.java │ │ │ │ │ │ ├── ApiCliConfigRequest.java │ │ │ │ │ │ ├── ApiTx0PreviewResponse.java │ │ │ │ │ │ ├── ApiWalletStateResponse.java │ │ │ │ │ │ ├── ApiPoolsResponse.java │ │ │ │ │ │ ├── ApiCliStateResponse.java │ │ │ │ │ │ └── ApiWalletUtxosResponse.java │ │ │ │ │ ├── beans │ │ │ │ │ │ ├── ApiEncrypted.java │ │ │ │ │ │ ├── ApiWallet.java │ │ │ │ │ │ ├── ApiPool.java │ │ │ │ │ │ ├── ApiUtxo.java │ │ │ │ │ │ └── ApiCliConfig.java │ │ │ │ │ ├── CliApi.java │ │ │ │ │ └── CliApiEndpoint.java │ │ │ │ └── controllers │ │ │ │ │ ├── wallet │ │ │ │ │ ├── UtxosListController.java │ │ │ │ │ └── DepositController.java │ │ │ │ │ ├── AbstractRestController.java │ │ │ │ │ ├── RestExceptionHandler.java │ │ │ │ │ ├── pools │ │ │ │ │ └── PoolsController.java │ │ │ │ │ ├── mix │ │ │ │ │ └── MixController.java │ │ │ │ │ ├── cli │ │ │ │ │ ├── CliConfigController.java │ │ │ │ │ └── CliController.java │ │ │ │ │ └── utxo │ │ │ │ │ └── UtxoController.java │ │ │ │ ├── exception │ │ │ │ ├── AuthenticationException.java │ │ │ │ └── NoSessionWalletException.java │ │ │ │ ├── config │ │ │ │ ├── security │ │ │ │ │ ├── CliWebServerFactoryCustomizer.java │ │ │ │ │ └── CliWebSecurityConfig.java │ │ │ │ ├── filters │ │ │ │ │ └── CliApiWebFilter.java │ │ │ │ ├── CliServicesConfig.java │ │ │ │ └── CliConfig.java │ │ │ │ ├── run │ │ │ │ ├── RunDumpPayload.java │ │ │ │ ├── RunUpgradeCli.java │ │ │ │ ├── RunListPools.java │ │ │ │ ├── RunCliCommand.java │ │ │ │ ├── RunCliInit.java │ │ │ │ ├── CliStatusOrchestrator.java │ │ │ │ └── CliStatusInteractiveOrchestrator.java │ │ │ │ ├── services │ │ │ │ ├── JavaStompClientService.java │ │ │ │ ├── JavaHttpClientService.java │ │ │ │ ├── CliTorClientService.java │ │ │ │ ├── TxAggregateService.java │ │ │ │ └── WalletAggregateService.java │ │ │ │ ├── utils │ │ │ │ └── EncryptUtils.java │ │ │ │ ├── Application.java │ │ │ │ ├── wallet │ │ │ │ └── CliWallet.java │ │ │ │ └── ApplicationArgs.java │ │ ├── stomp │ │ │ └── client │ │ │ │ ├── JavaStompMessage.java │ │ │ │ ├── JavaStompFrameHandler.java │ │ │ │ └── JavaStompClient.java │ │ ├── tor │ │ │ └── client │ │ │ │ ├── JavaTorSettings.java │ │ │ │ ├── utils │ │ │ │ └── WhirlpoolTorInstaller.java │ │ │ │ └── TorOnionProxyInstance.java │ │ └── http │ │ │ └── client │ │ │ └── JavaHttpClient.java │ │ └── msopentech │ │ └── thali │ │ └── toronionproxy │ │ └── OsData.java └── test │ └── java │ └── com │ └── samourai │ ├── whirlpool │ ├── client │ │ ├── test │ │ │ ├── CaptureStream.java │ │ │ └── AbstractTest.java │ │ └── run │ │ │ ├── AbstractApplicationTest.java │ │ │ └── ApplicationTest.java │ └── cli │ │ ├── beans │ │ ├── CliProxyTest.java │ │ └── WhirlpoolPairingPayloadTest.java │ │ ├── services │ │ └── CliWalletServiceTest.java │ │ └── utils │ │ └── EncryptUtilsTest.java │ └── xmanager │ └── client │ ├── BackendApiTest.java │ └── XManagerClientTest.java ├── README.md ├── README-ADVANCED.md ├── README-API.md └── pom.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /src/main/resources/keystore/whirlpool.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/whirlpool-client-cli/develop/src/main/resources/keystore/whirlpool.p12 -------------------------------------------------------------------------------- /src/main/resources/native/osx/x64/tor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/whirlpool-client-cli/develop/src/main/resources/native/osx/x64/tor.zip -------------------------------------------------------------------------------- /src/main/resources/native/linux/x64/tor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/whirlpool-client-cli/develop/src/main/resources/native/linux/x64/tor.zip -------------------------------------------------------------------------------- /src/main/resources/native/linux/x86/tor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/whirlpool-client-cli/develop/src/main/resources/native/linux/x86/tor.zip -------------------------------------------------------------------------------- /src/main/resources/native/windows/x86/tor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/whirlpool-client-cli/develop/src/main/resources/native/windows/x86/tor.zip -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliStatus.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | public enum CliStatus { 4 | NOT_INITIALIZED, 5 | NOT_READY, 6 | READY; 7 | 8 | private CliStatus() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliResult.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | public enum CliResult { 4 | RESTART, 5 | EXIT_SUCCESS, 6 | KEEP_RUNNING; 7 | 8 | private CliResult() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiTx0Request.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | public class ApiTx0Request extends ApiTx0PreviewRequest { 4 | public Integer mixsTarget; 5 | 6 | public ApiTx0Request() {} 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/beans/ApiEncrypted.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.beans; 2 | 3 | public class ApiEncrypted { 4 | public String iv; 5 | public String salt; 6 | public String ct; 7 | 8 | public ApiEncrypted() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiUtxoConfigureRequest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | public class ApiUtxoConfigureRequest { 4 | public String poolId; 5 | public int mixsTarget; 6 | 7 | public ApiUtxoConfigureRequest() {} 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/CliApi.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol; 2 | 3 | public class CliApi { 4 | public static final String API_VERSION = "0.10"; 5 | public static final String HEADER_API_VERSION = "apiVersion"; 6 | public static final String HEADER_API_KEY = "apiKey"; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliLoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | public class ApiCliLoginRequest { 6 | @NotEmpty public String seedPassphrase; 7 | 8 | public ApiCliLoginRequest() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/exception/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.exception; 2 | 3 | import com.samourai.whirlpool.client.exception.NotifiableException; 4 | 5 | public class AuthenticationException extends NotifiableException { 6 | 7 | public AuthenticationException(String error) { 8 | super(error); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliInitRequest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | public class ApiCliInitRequest { 6 | @NotEmpty public String pairingPayload; 7 | public boolean tor; 8 | public boolean dojo; 9 | 10 | public ApiCliInitRequest() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliInitResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | public class ApiCliInitResponse { 4 | private String apiKey; 5 | 6 | public ApiCliInitResponse(String apiKey) { 7 | this.apiKey = apiKey; 8 | } 9 | 10 | public String getApiKey() { 11 | return apiKey; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/exception/NoSessionWalletException.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.exception; 2 | 3 | import com.samourai.whirlpool.client.exception.NotifiableException; 4 | 5 | public class NoSessionWalletException extends NotifiableException { 6 | 7 | public NoSessionWalletException() { 8 | super("No wallet opened. Please open a wallet first"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiDepositResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | public class ApiDepositResponse { 4 | private String depositAddress; 5 | 6 | public ApiDepositResponse(String depositAddress) { 7 | this.depositAddress = depositAddress; 8 | } 9 | 10 | public String getDepositAddress() { 11 | return depositAddress; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiTx0PreviewRequest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget; 4 | import javax.validation.constraints.NotNull; 5 | 6 | public class ApiTx0PreviewRequest { 7 | @NotNull public Tx0FeeTarget feeTarget; 8 | @NotNull public String poolId; 9 | 10 | public ApiTx0PreviewRequest() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliProxyProtocol.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import java.util.Optional; 4 | 5 | public enum CliProxyProtocol { 6 | HTTP, 7 | SOCKS; 8 | 9 | public static Optional find(String value) { 10 | try { 11 | return Optional.of(valueOf(value)); 12 | } catch (Exception e) { 13 | return Optional.empty(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiTx0Response.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.client.tx0.Tx0; 4 | 5 | public class ApiTx0Response extends ApiTx0PreviewResponse { 6 | private String txid; 7 | 8 | public ApiTx0Response(Tx0 tx0) { 9 | super(tx0); 10 | this.txid = tx0.getTx().getHashAsString(); 11 | } 12 | 13 | public String getTxid() { 14 | return txid; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliTorExecutableMode.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import java.util.Optional; 4 | 5 | public enum CliTorExecutableMode { 6 | AUTO, // try embedded then find local 7 | LOCAL, // find local install 8 | SPECIFIED; // custom path specified 9 | 10 | public static Optional find(String value) { 11 | try { 12 | return Optional.of(valueOf(value)); 13 | } catch (Exception e) { 14 | return Optional.empty(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliConfigResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig; 4 | import com.samourai.whirlpool.cli.config.CliConfig; 5 | 6 | public class ApiCliConfigResponse { 7 | private ApiCliConfig config; 8 | 9 | public ApiCliConfigResponse(CliConfig cliConfig) { 10 | this.config = new ApiCliConfig(cliConfig); 11 | } 12 | 13 | public ApiCliConfig getConfig() { 14 | return config; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliConfigRequest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig; 4 | import javax.validation.constraints.NotNull; 5 | 6 | public class ApiCliConfigRequest { 7 | @NotNull private ApiCliConfig config; 8 | 9 | public ApiCliConfigRequest() {} 10 | 11 | public ApiCliConfig getConfig() { 12 | return config; 13 | } 14 | 15 | public void setConfig(ApiCliConfig config) { 16 | this.config = config; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/client/test/CaptureStream.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.client.test; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.PrintStream; 5 | 6 | public class CaptureStream extends ByteArrayOutputStream { 7 | private PrintStream printStream; 8 | 9 | public CaptureStream(PrintStream printStream) { 10 | this.printStream = printStream; 11 | } 12 | 13 | @Override 14 | public synchronized void write(byte[] b, int off, int len) { 15 | super.write(b, off, len); 16 | 17 | printStream.println(new String(b)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/client/test/AbstractTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.client.test; 2 | 3 | import com.samourai.whirlpool.cli.utils.CliUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.springframework.boot.SpringBootConfiguration; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | @SpringBootConfiguration 10 | @ActiveProfiles(CliUtils.SPRING_PROFILE_TESTING) 11 | public class AbstractTest { 12 | @Before 13 | public void setup() throws Exception {} 14 | 15 | @After 16 | public void tearDown() throws Exception {} 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/stomp/client/JavaStompMessage.java: -------------------------------------------------------------------------------- 1 | package com.samourai.stomp.client; 2 | 3 | import org.springframework.messaging.simp.stomp.StompHeaders; 4 | 5 | public class JavaStompMessage implements IStompMessage { 6 | private StompHeaders headers; 7 | private Object payload; 8 | 9 | public JavaStompMessage(StompHeaders headers, Object payload) { 10 | this.headers = headers; 11 | this.payload = payload; 12 | } 13 | 14 | @Override 15 | public String getStompHeader(String headerName) { 16 | return headers.getFirst(headerName); 17 | } 18 | 19 | @Override 20 | public Object getPayload() { 21 | return payload; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/cli/beans/CliProxyTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class CliProxyTest { 7 | 8 | @Test 9 | public void testValidate() throws Exception { 10 | // valid 11 | Assert.assertTrue(CliProxy.validate("http://localhost:8080")); 12 | Assert.assertTrue(CliProxy.validate("socks://localhost:9050")); 13 | 14 | // invalid 15 | Assert.assertFalse(CliProxy.validate("foo://localhost:9050")); // invalid protocol 16 | Assert.assertFalse(CliProxy.validate("http://localhost")); // missing port 17 | Assert.assertFalse(CliProxy.validate("localhost:8080")); // missing protocol 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/Encrypted.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import org.bouncycastle.util.encoders.Base64; 4 | 5 | public class Encrypted { 6 | private byte[] iv; 7 | private byte[] salt; 8 | private byte[] ct; 9 | 10 | public Encrypted(byte[] iv, byte[] salt, byte[] ct) { 11 | this.iv = iv; 12 | this.salt = salt; 13 | this.ct = ct; 14 | } 15 | 16 | public Encrypted(String iv, String salt, String ct) { 17 | this(Base64.decode(iv), Base64.decode(salt), Base64.decode(ct)); 18 | } 19 | 20 | public byte[] getIv() { 21 | return iv; 22 | } 23 | 24 | public byte[] getSalt() { 25 | return salt; 26 | } 27 | 28 | public byte[] getCt() { 29 | return ct; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliState.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | public class CliState { 4 | private CliStatus cliStatus; 5 | private String cliMessage; 6 | private boolean loggedIn; 7 | private Integer torProgress; 8 | 9 | public CliState(CliStatus cliStatus, String cliMessage, boolean loggedIn, Integer torProgress) { 10 | this.cliStatus = cliStatus; 11 | this.cliMessage = cliMessage; 12 | this.loggedIn = loggedIn; 13 | this.torProgress = torProgress; 14 | } 15 | 16 | public CliStatus getCliStatus() { 17 | return cliStatus; 18 | } 19 | 20 | public String getCliMessage() { 21 | return cliMessage; 22 | } 23 | 24 | public boolean isLoggedIn() { 25 | return loggedIn; 26 | } 27 | 28 | public Integer getTorProgress() { 29 | return torProgress; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/config/security/CliWebServerFactoryCustomizer.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.config.security; 2 | 3 | import com.samourai.whirlpool.cli.config.CliConfig; 4 | import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; 5 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** Configure HTTPS server. */ 9 | @Component 10 | public class CliWebServerFactoryCustomizer 11 | implements WebServerFactoryCustomizer { 12 | private CliConfig cliConfig; 13 | 14 | public CliWebServerFactoryCustomizer(CliConfig cliConfig) { 15 | this.cliConfig = cliConfig; 16 | } 17 | 18 | @Override 19 | public void customize(TomcatServletWebServerFactory factory) { 20 | // configure HTTPS port 21 | factory.setPort(cliConfig.getApi().getPort()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/RunDumpPayload.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.services.CliWalletService; 4 | import com.samourai.whirlpool.cli.utils.CliUtils; 5 | import java.lang.invoke.MethodHandles; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class RunDumpPayload { 10 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 11 | 12 | private CliWalletService cliWalletService; 13 | 14 | public RunDumpPayload(CliWalletService cliWalletService) { 15 | this.cliWalletService = cliWalletService; 16 | } 17 | 18 | public void run() throws Exception { 19 | String payload = cliWalletService.computePairingPayload(); 20 | log.info(CliUtils.LOG_SEPARATOR); 21 | log.info("⣿ DUMP-PAYLOAD"); 22 | log.info("⣿ Pairing-payload of your current wallet:"); 23 | log.info("⣿ " + payload); 24 | log.info(CliUtils.LOG_SEPARATOR); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/cli/services/CliWalletServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.whirlpool.client.test.AbstractTest; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | @RunWith(SpringRunner.class) 12 | @SpringBootTest 13 | public class CliWalletServiceTest extends AbstractTest { 14 | @Autowired private CliWalletService cliWalletService; 15 | 16 | @Override 17 | public void setup() throws Exception { 18 | super.setup(); 19 | } 20 | 21 | @Test 22 | public void decryptSeedWords() throws Exception { 23 | String seedWordsEncrypted; 24 | String passphrase; 25 | 26 | seedWordsEncrypted = 27 | "t6MNj4oCb9T54lKWNAF274Hg72E0q0uJooUwKjzGD+ysWsFv8Ib47ubdnjStkeJ/G9UltiERHAm1tKRtHbaJiA=="; 28 | passphrase = "secret"; 29 | Assert.assertTrue( 30 | cliWalletService.decryptSeedWords(seedWordsEncrypted, passphrase).split(" ").length == 12); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/beans/ApiWallet.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.beans; 2 | 3 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; 4 | import java.util.Collection; 5 | import java.util.Comparator; 6 | import java.util.stream.Collectors; 7 | 8 | public class ApiWallet { 9 | private Collection utxos; 10 | private long balance; 11 | private String zpub; 12 | 13 | public ApiWallet( 14 | Collection whirlpoolUtxos, 15 | String zpub, 16 | Comparator comparator, 17 | int mixsTargetMin) { 18 | this.utxos = 19 | whirlpoolUtxos 20 | .stream() 21 | .sorted(comparator) 22 | .map(whirlpoolUtxo -> new ApiUtxo(whirlpoolUtxo, mixsTargetMin)) 23 | .collect(Collectors.toList()); 24 | this.balance = 25 | whirlpoolUtxos.stream().mapToLong(whirlpoolUtxo -> whirlpoolUtxo.getUtxo().value).sum(); 26 | this.zpub = zpub; 27 | } 28 | 29 | public Collection getUtxos() { 30 | return utxos; 31 | } 32 | 33 | public long getBalance() { 34 | return balance; 35 | } 36 | 37 | public String getZpub() { 38 | return zpub; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiTx0PreviewResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.client.tx0.Tx0Preview; 4 | 5 | public class ApiTx0PreviewResponse { 6 | private long minerFee; 7 | private long feeValue; 8 | private long feeChange; 9 | private long premixValue; 10 | private long changeValue; 11 | private int nbPremix; 12 | 13 | public ApiTx0PreviewResponse(Tx0Preview tx0Preview) { 14 | this.minerFee = tx0Preview.getMinerFee(); 15 | this.feeValue = tx0Preview.getFeeValue(); 16 | this.feeChange = tx0Preview.getFeeChange(); 17 | this.premixValue = tx0Preview.getPremixValue(); 18 | this.changeValue = tx0Preview.getChangeValue(); 19 | this.nbPremix = tx0Preview.getNbPremix(); 20 | } 21 | 22 | public long getMinerFee() { 23 | return minerFee; 24 | } 25 | 26 | public long getFeeValue() { 27 | return feeValue; 28 | } 29 | 30 | public long getFeeChange() { 31 | return feeChange; 32 | } 33 | 34 | public long getPremixValue() { 35 | return premixValue; 36 | } 37 | 38 | public long getChangeValue() { 39 | return changeValue; 40 | } 41 | 42 | public int getNbPremix() { 43 | return nbPremix; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Default cli config 2 | spring.main.banner-mode=off 3 | logging.level.org.springframework=WARN 4 | logging.level.org.apache=WARN 5 | 6 | # https 7 | server.ssl.key-store-type=PKCS12 8 | server.ssl.key-store=classpath:keystore/whirlpool.p12 9 | server.ssl.key-store-password=whirlpool 10 | server.ssl.key-alias=whirlpool 11 | cli.api.port=8899 12 | cli.api.http-port=8898 13 | cli.api.http-enable=false 14 | 15 | # cli.version 16 | cli.server = TESTNET 17 | cli.dojo.url = 18 | cli.dojo.apiKey = 19 | cli.dojo.enabled = false 20 | cli.scode = 21 | cli.tor = false 22 | cli.torConfig.executable = auto 23 | cli.torConfig.coordinator.enabled = true 24 | cli.torConfig.coordinator.onion = true 25 | cli.torConfig.backend.enabled = true 26 | cli.torConfig.backend.onion = true 27 | cli.torConfig.customTorrc = 28 | cli.torConfig.fileCreationTimeout = 20 29 | cli.apiKey = 30 | cli.seed = 31 | cli.seedAppendPassphrase = true 32 | cli.persistDelay = 4 33 | cli.refreshPoolsDelay = 30 34 | cli.tx0MinConfirmations = 0 35 | cli.proxy = 36 | cli.requestTimeout = 30000 37 | 38 | cli.mix.clients = 5 39 | cli.mix.clientsPerPool = 1 40 | cli.mix.tx0MaxOutputs = 0 41 | cli.mix.clientDelay = 15 42 | cli.mix.tx0Delay = 30 43 | cli.mix.autoMix = true 44 | cli.mix.mixsTarget = 1 45 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/config/filters/CliApiWebFilter.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.config.filters; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.CliApi; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import java.io.IOException; 6 | import javax.servlet.Filter; 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.FilterConfig; 9 | import javax.servlet.ServletException; 10 | import javax.servlet.ServletRequest; 11 | import javax.servlet.ServletResponse; 12 | import javax.servlet.annotation.WebFilter; 13 | import javax.servlet.http.HttpServletResponse; 14 | 15 | @WebFilter(CliApiEndpoint.REST_PREFIX + "*") 16 | public class CliApiWebFilter implements Filter { 17 | 18 | @Override 19 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 20 | throws IOException, ServletException { 21 | // add apiVersion header 22 | HttpServletResponse httpServletResponse = (HttpServletResponse) response; 23 | httpServletResponse.setHeader(CliApi.HEADER_API_VERSION, CliApi.API_VERSION); 24 | chain.doFilter(request, response); 25 | } 26 | 27 | @Override 28 | public void init(FilterConfig filterConfig) throws ServletException {} 29 | 30 | @Override 31 | public void destroy() {} 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/services/JavaStompClientService.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.http.client.JavaHttpClient; 5 | import com.samourai.stomp.client.IStompClient; 6 | import com.samourai.stomp.client.IStompClientService; 7 | import com.samourai.stomp.client.JavaStompClient; 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class JavaStompClientService implements IStompClientService { 13 | private JavaHttpClientService httpClientService; 14 | 15 | private ThreadPoolTaskScheduler taskScheduler; 16 | 17 | public JavaStompClientService(JavaHttpClientService httpClientService) { 18 | this.httpClientService = httpClientService; 19 | 20 | taskScheduler = new ThreadPoolTaskScheduler(); 21 | taskScheduler.setPoolSize(1); 22 | taskScheduler.setThreadNamePrefix("stomp-heartbeat"); 23 | taskScheduler.initialize(); 24 | } 25 | 26 | @Override 27 | public IStompClient newStompClient() { 28 | JavaHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_WEBSOCKET); 29 | return new JavaStompClient(httpClient, taskScheduler); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiWalletStateResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiUtxo; 4 | import com.samourai.whirlpool.client.wallet.beans.MixingState; 5 | import java.util.Collection; 6 | import java.util.stream.Collectors; 7 | 8 | public class ApiWalletStateResponse { 9 | private boolean started; 10 | 11 | private int nbMixing; 12 | private int nbQueued; 13 | private Collection threads; 14 | 15 | public ApiWalletStateResponse(MixingState mixingState, int mixsTargetMin) { 16 | this.started = mixingState.isStarted(); 17 | this.nbMixing = mixingState.getNbMixing(); 18 | this.nbQueued = mixingState.getNbQueued(); 19 | this.threads = 20 | mixingState 21 | .getUtxosMixing() 22 | .stream() 23 | .map(whirlpoolUtxo -> new ApiUtxo(whirlpoolUtxo, mixsTargetMin)) 24 | .collect(Collectors.toList()); 25 | } 26 | 27 | public boolean isStarted() { 28 | return started; 29 | } 30 | 31 | public int getNbMixing() { 32 | return nbMixing; 33 | } 34 | 35 | public int getNbQueued() { 36 | return nbQueued; 37 | } 38 | 39 | public Collection getThreads() { 40 | return threads; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/RunUpgradeCli.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.config.CliConfig; 4 | import com.samourai.whirlpool.cli.services.CliConfigService; 5 | import java.lang.invoke.MethodHandles; 6 | import java.util.Properties; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class RunUpgradeCli { 11 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 12 | 13 | private CliConfig cliConfig; 14 | private CliConfigService cliConfigService; 15 | 16 | public RunUpgradeCli(CliConfig cliConfig, CliConfigService cliConfigService) { 17 | this.cliConfig = cliConfig; 18 | this.cliConfigService = cliConfigService; 19 | } 20 | 21 | public void run(int localVersion) throws Exception { 22 | // run upgrades 23 | if (localVersion < CliConfigService.CLI_VERSION_4) { 24 | upgradeV4(); 25 | } 26 | } 27 | 28 | public void upgradeV4() throws Exception { 29 | log.info(" - Upgrading to: V4"); 30 | 31 | // set cli.mix.clients=5 when missing 32 | if (cliConfig.getMix().getClients() == 0) { 33 | Properties props = cliConfigService.loadProperties(); 34 | props.put(CliConfigService.KEY_MIX_CLIENTS, "5"); 35 | cliConfigService.saveProperties(props); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/xmanager/client/BackendApiTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.xmanager.client; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.http.client.JavaHttpClient; 5 | import com.samourai.wallet.api.backend.BackendApi; 6 | import com.samourai.wallet.api.backend.BackendServer; 7 | import com.samourai.whirlpool.cli.utils.CliUtils; 8 | import com.samourai.whirlpool.client.test.AbstractTest; 9 | import java.util.Optional; 10 | import org.eclipse.jetty.client.HttpClient; 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class BackendApiTest extends AbstractTest { 15 | private static final boolean testnet = true; 16 | private static final long requestTimeout = 5000; 17 | 18 | private BackendApi backendApi; 19 | 20 | public BackendApiTest() throws Exception { 21 | HttpClient jettyHttpClient = CliUtils.computeHttpClient(Optional.empty(), "whirlpool-cli/test"); 22 | JavaHttpClient httpClient = 23 | new JavaHttpClient(HttpUsage.BACKEND, jettyHttpClient, requestTimeout); 24 | backendApi = 25 | new BackendApi( 26 | httpClient, BackendServer.TESTNET.getBackendUrlClear(), java8.util.Optional.empty()); 27 | } 28 | 29 | @Disabled 30 | @Test 31 | public void initBip84() throws Exception { 32 | backendApi.initBip84("vpub..."); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/client/run/AbstractApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.client.run; 2 | 3 | import com.samourai.whirlpool.client.test.AbstractTest; 4 | import com.samourai.whirlpool.client.test.CaptureStream; 5 | import java.io.PrintStream; 6 | import org.springframework.boot.SpringBootConfiguration; 7 | 8 | @SpringBootConfiguration 9 | public class AbstractApplicationTest extends AbstractTest { 10 | 11 | private CaptureStream outContent; 12 | private CaptureStream errContent; 13 | private PrintStream outOrig = System.out; 14 | private PrintStream errOrig = System.err; 15 | 16 | protected void captureSystem() { 17 | outContent = new CaptureStream(System.out); 18 | errContent = new CaptureStream(System.err); 19 | System.setOut(new PrintStream(outContent)); 20 | System.setErr(new PrintStream(errContent)); 21 | } 22 | 23 | protected void resetSystem() { 24 | System.setOut(outOrig); 25 | System.setErr(errOrig); 26 | } 27 | 28 | protected String getOut() { 29 | return outContent.toString(); 30 | } 31 | 32 | protected String getErr() { 33 | return errContent.toString(); 34 | } 35 | 36 | public void setup() throws Exception { 37 | super.setup(); 38 | captureSystem(); 39 | } 40 | 41 | public void tearDown() throws Exception { 42 | super.tearDown(); 43 | resetSystem(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/wallet/UtxosListController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.wallet; 2 | 3 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiWalletUtxosResponse; 6 | import com.samourai.whirlpool.cli.services.CliWalletService; 7 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.web.bind.annotation.RequestHeader; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | @RestController 16 | public class UtxosListController extends AbstractRestController { 17 | @Autowired private CliWalletService cliWalletService; 18 | 19 | @RequestMapping(value = CliApiEndpoint.REST_UTXOS, method = RequestMethod.GET) 20 | public ApiWalletUtxosResponse wallet(@RequestHeader HttpHeaders headers) throws Exception { 21 | checkHeaders(headers); 22 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 23 | return new ApiWalletUtxosResponse(whirlpoolWallet); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/AbstractRestController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.CliApi; 4 | import com.samourai.whirlpool.cli.config.CliConfig; 5 | import com.samourai.whirlpool.client.exception.NotifiableException; 6 | import org.apache.logging.log4j.util.Strings; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpHeaders; 9 | 10 | public abstract class AbstractRestController { 11 | @Autowired private CliConfig cliConfig; 12 | 13 | public AbstractRestController() {} 14 | 15 | protected void checkHeaders(HttpHeaders httpHeaders) throws Exception { 16 | // check apiVersion 17 | String requestApiVersion = httpHeaders.getFirst(CliApi.HEADER_API_VERSION); 18 | if (!Strings.isEmpty(requestApiVersion) && !CliApi.API_VERSION.equals(requestApiVersion)) { 19 | throw new NotifiableException( 20 | "API version mismatch: requestVersion=" 21 | + requestApiVersion 22 | + ", cliVersion=" 23 | + CliApi.API_VERSION); 24 | } 25 | 26 | // check apiKey 27 | if (!Strings.isEmpty(cliConfig.getApiKey())) { 28 | String requestApiKey = httpHeaders.getFirst(CliApi.HEADER_API_KEY); 29 | if (!cliConfig.getApiKey().equals(requestApiKey)) { 30 | throw new NotifiableException("API key rejected"); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers; 2 | 3 | import com.samourai.whirlpool.client.exception.NotifiableException; 4 | import com.samourai.whirlpool.protocol.rest.RestErrorResponse; 5 | import java.lang.invoke.MethodHandles; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.ControllerAdvice; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 13 | 14 | @ControllerAdvice 15 | public class RestExceptionHandler extends ResponseEntityExceptionHandler { 16 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 17 | 18 | @ExceptionHandler(value = {Exception.class}) 19 | protected ResponseEntity handleException(Exception e) { 20 | NotifiableException notifiable = NotifiableException.computeNotifiableException(e); 21 | if (log.isDebugEnabled()) { 22 | log.debug("RestExceptionHandler: " + notifiable.getMessage()); 23 | } 24 | RestErrorResponse restErrorResponse = new RestErrorResponse(notifiable.getMessage()); 25 | HttpStatus httpStatus = HttpStatus.valueOf(notifiable.getStatus()); 26 | return new ResponseEntity<>(restErrorResponse, httpStatus); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiPoolsResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiPool; 4 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 5 | import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget; 6 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 7 | import java.util.Collection; 8 | import java.util.Map; 9 | import java.util.stream.Collectors; 10 | 11 | public class ApiPoolsResponse { 12 | private Collection pools; 13 | 14 | public ApiPoolsResponse( 15 | Collection pools, 16 | Tx0FeeTarget feeTarget, 17 | WhirlpoolWallet whirlpoolWallet, 18 | Map overspendPerPool) { 19 | this.pools = 20 | pools 21 | .stream() 22 | .map( 23 | pool -> { 24 | Long overspendOrNull = 25 | overspendPerPool != null ? overspendPerPool.get(pool.getPoolId()) : null; 26 | return computeApiPool(pool, feeTarget, whirlpoolWallet, overspendOrNull); 27 | }) 28 | .collect(Collectors.toList()); 29 | } 30 | 31 | private ApiPool computeApiPool( 32 | Pool pool, Tx0FeeTarget feeTarget, WhirlpoolWallet whirlpoolWallet, Long overspendOrNull) { 33 | long tx0BalanceMin = 34 | whirlpoolWallet.computeTx0SpendFromBalanceMin(pool, feeTarget, 1, overspendOrNull); 35 | return new ApiPool(pool, tx0BalanceMin); 36 | } 37 | 38 | public Collection getPools() { 39 | return pools; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/wallet/DepositController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.wallet; 2 | 3 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiDepositResponse; 6 | import com.samourai.whirlpool.cli.services.CliWalletService; 7 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.web.bind.annotation.RequestHeader; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | @RestController 17 | public class DepositController extends AbstractRestController { 18 | @Autowired private CliWalletService cliWalletService; 19 | 20 | @RequestMapping(value = CliApiEndpoint.REST_WALLET_DEPOSIT, method = RequestMethod.GET) 21 | public ApiDepositResponse wallet( 22 | @RequestParam(value = "increment", defaultValue = "false") boolean increment, 23 | @RequestHeader HttpHeaders headers) 24 | throws Exception { 25 | checkHeaders(headers); 26 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 27 | String depositAddress = whirlpoolWallet.getDepositAddress(increment); 28 | return new ApiDepositResponse(depositAddress); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/stomp/client/JavaStompFrameHandler.java: -------------------------------------------------------------------------------- 1 | package com.samourai.stomp.client; 2 | 3 | import com.samourai.whirlpool.client.utils.MessageErrorListener; 4 | import com.samourai.whirlpool.protocol.WhirlpoolProtocol; 5 | import java.lang.invoke.MethodHandles; 6 | import java.lang.reflect.Type; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.messaging.simp.stomp.StompHeaders; 10 | 11 | public class JavaStompFrameHandler 12 | implements org.springframework.messaging.simp.stomp.StompFrameHandler { 13 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 14 | private final MessageErrorListener onMessageOnErrorListener; 15 | 16 | public JavaStompFrameHandler( 17 | MessageErrorListener onMessageOnErrorListener) { 18 | this.onMessageOnErrorListener = onMessageOnErrorListener; 19 | } 20 | 21 | @Override 22 | public Type getPayloadType(StompHeaders headers) { 23 | String messageType = headers.get(WhirlpoolProtocol.HEADER_MESSAGE_TYPE).get(0); 24 | try { 25 | return Class.forName(messageType); 26 | } catch (ClassNotFoundException e) { 27 | log.error("unknown message type: " + messageType, e); 28 | this.onMessageOnErrorListener.onError("unknown message type: " + messageType); 29 | return null; 30 | } 31 | } 32 | 33 | @Override 34 | public void handleFrame(StompHeaders headers, Object payload) { 35 | IStompMessage stompMessage = new JavaStompMessage(headers, payload); 36 | 37 | // payload already deserialized by StompFrameHandler 38 | onMessageOnErrorListener.onMessage(stompMessage); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/CliProxy.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import java.util.Arrays; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.eclipse.jetty.client.HttpProxy; 6 | import org.eclipse.jetty.client.ProxyConfiguration; 7 | import org.eclipse.jetty.client.Socks4Proxy; 8 | 9 | public class CliProxy { 10 | private CliProxyProtocol protocol; 11 | private String host; 12 | private int port; 13 | 14 | public CliProxy(CliProxyProtocol protocol, String host, int port) { 15 | this.protocol = protocol; 16 | this.host = host; 17 | this.port = port; 18 | } 19 | 20 | public CliProxyProtocol getProtocol() { 21 | return protocol; 22 | } 23 | 24 | public String getHost() { 25 | return host; 26 | } 27 | 28 | public int getPort() { 29 | return port; 30 | } 31 | 32 | public static boolean validate(String proxy) { 33 | // check protocol 34 | String[] protocols = 35 | Arrays.stream(CliProxyProtocol.values()).map(p -> p.name()).toArray(String[]::new); 36 | String regex = "^(" + StringUtils.join(protocols, "|").toLowerCase() + ")://(.+?):([0-9]+)"; 37 | return proxy.trim().toLowerCase().matches(regex); 38 | } 39 | 40 | public ProxyConfiguration.Proxy computeJettyProxy() { 41 | ProxyConfiguration.Proxy jettyProxy = null; 42 | switch (getProtocol()) { 43 | case SOCKS: 44 | jettyProxy = new Socks4Proxy(getHost(), getPort()); 45 | break; 46 | 47 | case HTTP: 48 | jettyProxy = new HttpProxy(getHost(), getPort()); 49 | break; 50 | } 51 | return jettyProxy; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return protocol + "://" + host + ":" + port; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/pools/PoolsController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.pools; 2 | 3 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiPoolsResponse; 6 | import com.samourai.whirlpool.cli.config.CliConfig; 7 | import com.samourai.whirlpool.cli.services.CliWalletService; 8 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 9 | import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget; 10 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 11 | import java.util.Collection; 12 | import java.util.Map; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | @RestController 18 | public class PoolsController extends AbstractRestController { 19 | @Autowired private CliWalletService cliWalletService; 20 | @Autowired private CliConfig cliConfig; 21 | 22 | @RequestMapping(value = CliApiEndpoint.REST_POOLS, method = RequestMethod.GET) 23 | public ApiPoolsResponse pools( 24 | @RequestParam(value = "tx0FeeTarget", defaultValue = "BLOCKS_24") 25 | Tx0FeeTarget tx0FeeTarget, // Tx0FeeTarget.MIN 26 | @RequestHeader HttpHeaders headers) 27 | throws Exception { 28 | checkHeaders(headers); 29 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 30 | Collection pools = whirlpoolWallet.getPools(false); 31 | Map overspendPerPool = cliConfig.getMix().getOverspend(); 32 | return new ApiPoolsResponse(pools, tx0FeeTarget, whirlpoolWallet, overspendPerPool); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/config/security/CliWebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.config.security; 2 | 3 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 4 | import com.samourai.whirlpool.cli.config.CliConfig; 5 | import java.lang.invoke.MethodHandles; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | 14 | @Configuration 15 | @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 16 | @EnableWebSecurity 17 | public class CliWebSecurityConfig extends WebSecurityConfigurerAdapter { 18 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 19 | 20 | private CliConfig cliConfig; 21 | 22 | public CliWebSecurityConfig(CliConfig cliConfig) { 23 | this.cliConfig = cliConfig; 24 | } 25 | 26 | @Override 27 | protected void configure(HttpSecurity http) throws Exception { 28 | boolean httpEnable = cliConfig.getApi().isHttpEnable(); 29 | if (log.isDebugEnabled()) { 30 | log.debug("Configuring REST API: httpEnable=" + httpEnable); 31 | } 32 | 33 | // disable CSRF 34 | http.csrf() 35 | .disable() 36 | 37 | // authorize REST API 38 | .authorizeRequests() 39 | .antMatchers(CliApiEndpoint.REST_ENDPOINTS) 40 | .permitAll() 41 | 42 | // reject others 43 | .anyRequest() 44 | .denyAll(); 45 | 46 | if (!httpEnable) { 47 | http.requiresChannel().anyRequest().requiresSecure(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/RunListPools.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.wallet.CliWallet; 4 | import com.samourai.whirlpool.client.utils.ClientUtils; 5 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 6 | import java.lang.invoke.MethodHandles; 7 | import java.util.Collection; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class RunListPools { 12 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 13 | 14 | private CliWallet cliWallet; 15 | 16 | public RunListPools(CliWallet cliWallet) { 17 | this.cliWallet = cliWallet; 18 | } 19 | 20 | public void run() throws Exception { 21 | Collection pools = cliWallet.getPools(true); 22 | 23 | // show available pools 24 | String lineFormat = "| %15s | %6s | %15s | %14s | %12s | %15s | %23s |\n"; 25 | StringBuilder sb = new StringBuilder(); 26 | sb.append( 27 | String.format( 28 | lineFormat, 29 | "POOL ID", 30 | "DENOM.", 31 | "STATUS", 32 | "USERS", 33 | "LAST MIX", 34 | "ANONYMITY SET", 35 | "MUSTMIX BALANCE")); 36 | sb.append( 37 | String.format( 38 | lineFormat, "", "(btc)", "", "(confir/reg)", "", "(target/min)", "min-max (sat)")); 39 | for (Pool pool : pools) { 40 | sb.append( 41 | String.format( 42 | lineFormat, 43 | pool.getPoolId(), 44 | ClientUtils.satToBtc(pool.getDenomination()), 45 | pool.getMixStatus(), 46 | pool.getNbConfirmed() + " / " + pool.getNbRegistered(), 47 | pool.getElapsedTime() / 1000 + "s", 48 | pool.getMixAnonymitySet() + " / " + pool.getMinAnonymitySet(), 49 | pool.getMustMixBalanceMin() + " - " + pool.getMustMixBalanceMax())); 50 | } 51 | log.info("\n" + sb.toString()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/mix/MixController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.mix; 2 | 3 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiWalletStateResponse; 6 | import com.samourai.whirlpool.cli.services.CliWalletService; 7 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 8 | import com.samourai.whirlpool.client.wallet.beans.MixingState; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.web.bind.annotation.RequestHeader; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | @RestController 17 | public class MixController extends AbstractRestController { 18 | @Autowired private CliWalletService cliWalletService; 19 | 20 | @RequestMapping(value = CliApiEndpoint.REST_MIX, method = RequestMethod.GET) 21 | public ApiWalletStateResponse wallet(@RequestHeader HttpHeaders headers) throws Exception { 22 | checkHeaders(headers); 23 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 24 | MixingState mixingState = whirlpoolWallet.getMixingState(); 25 | return new ApiWalletStateResponse(mixingState, whirlpoolWallet.getConfig().getMixsTarget()); 26 | } 27 | 28 | @RequestMapping(value = CliApiEndpoint.REST_MIX_START, method = RequestMethod.POST) 29 | public void start(@RequestHeader HttpHeaders headers) throws Exception { 30 | checkHeaders(headers); 31 | cliWalletService.getSessionWallet().start(); 32 | } 33 | 34 | @RequestMapping(value = CliApiEndpoint.REST_MIX_STOP, method = RequestMethod.POST) 35 | public void stop(@RequestHeader HttpHeaders headers) throws Exception { 36 | checkHeaders(headers); 37 | cliWalletService.getSessionWallet().stop(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/CliApiEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol; 2 | 3 | public class CliApiEndpoint { 4 | public static final String REST_PREFIX = "/rest/"; 5 | 6 | public static final String REST_CLI = REST_PREFIX + "cli"; 7 | public static final String REST_CLI_INIT = REST_PREFIX + "cli/init"; 8 | public static final String REST_CLI_LOGIN = REST_PREFIX + "cli/login"; 9 | public static final String REST_CLI_LOGOUT = REST_PREFIX + "cli/logout"; 10 | public static final String REST_CLI_CONFIG = REST_PREFIX + "cli/config"; 11 | 12 | public static final String REST_POOLS = REST_PREFIX + "pools"; 13 | 14 | public static final String REST_WALLET_DEPOSIT = REST_PREFIX + "wallet/deposit"; 15 | 16 | public static final String REST_MIX = REST_PREFIX + "mix"; 17 | public static final String REST_MIX_START = REST_PREFIX + "mix/start"; 18 | public static final String REST_MIX_STOP = REST_PREFIX + "mix/stop"; 19 | 20 | public static final String REST_UTXOS = REST_PREFIX + "utxos"; 21 | public static final String REST_UTXO_CONFIGURE = REST_PREFIX + "utxos/{hash}:{index}"; 22 | public static final String REST_UTXO_TX0 = REST_PREFIX + "utxos/{hash}:{index}/tx0"; 23 | public static final String REST_UTXO_TX0_PREVIEW = 24 | REST_PREFIX + "utxos/{hash}:{index}/tx0Preview"; 25 | public static final String REST_UTXO_STARTMIX = REST_PREFIX + "utxos/{hash}:{index}/startMix"; 26 | public static final String REST_UTXO_STOPMIX = REST_PREFIX + "utxos/{hash}:{index}/stopMix"; 27 | 28 | public static final String[] REST_ENDPOINTS = 29 | new String[] { 30 | REST_CLI, 31 | REST_CLI_INIT, 32 | REST_CLI_LOGIN, 33 | REST_CLI_LOGOUT, 34 | REST_CLI_CONFIG, 35 | REST_POOLS, 36 | REST_WALLET_DEPOSIT, 37 | REST_MIX, 38 | REST_MIX_START, 39 | REST_MIX_STOP, 40 | REST_UTXOS, 41 | REST_UTXO_CONFIGURE, 42 | REST_UTXO_TX0_PREVIEW, 43 | REST_UTXO_TX0, 44 | REST_UTXO_STARTMIX, 45 | REST_UTXO_STOPMIX 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiCliStateResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.samourai.whirlpool.cli.beans.CliState; 4 | import com.samourai.whirlpool.cli.beans.CliStatus; 5 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolServer; 6 | 7 | public class ApiCliStateResponse { 8 | private CliStatus cliStatus; 9 | private String cliMessage; 10 | private boolean loggedIn; 11 | private Integer torProgress; 12 | 13 | private String network; 14 | private String serverUrl; 15 | private String serverName; 16 | private String dojoUrl; 17 | private boolean tor; 18 | private boolean dojo; 19 | 20 | public ApiCliStateResponse( 21 | CliState cliState, 22 | WhirlpoolServer server, 23 | String serverUrl, 24 | String dojoUrl, 25 | boolean tor, 26 | boolean dojo) { 27 | this.cliStatus = cliState.getCliStatus(); 28 | this.cliMessage = cliState.getCliMessage(); 29 | this.loggedIn = cliState.isLoggedIn(); 30 | this.torProgress = cliState.getTorProgress(); 31 | 32 | this.network = server.getParams().getPaymentProtocolId(); 33 | this.serverUrl = serverUrl; 34 | this.serverName = server.name(); 35 | this.dojoUrl = dojoUrl; 36 | this.tor = tor; 37 | this.dojo = dojo; 38 | } 39 | 40 | public CliStatus getCliStatus() { 41 | return cliStatus; 42 | } 43 | 44 | public String getCliMessage() { 45 | return cliMessage; 46 | } 47 | 48 | public boolean isLoggedIn() { 49 | return loggedIn; 50 | } 51 | 52 | public Integer getTorProgress() { 53 | return torProgress; 54 | } 55 | 56 | public String getNetwork() { 57 | return network; 58 | } 59 | 60 | public String getServerUrl() { 61 | return serverUrl; 62 | } 63 | 64 | public String getServerName() { 65 | return serverName; 66 | } 67 | 68 | public String getDojoUrl() { 69 | return dojoUrl; 70 | } 71 | 72 | public boolean isTor() { 73 | return tor; 74 | } 75 | 76 | public boolean isDojo() { 77 | return dojo; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Samourai-Wallet/whirlpool-client-cli.svg?branch=develop)](https://travis-ci.org/Samourai-Wallet/whirlpool-client-cli) 2 | [![](https://jitpack.io/v/Samourai-Wallet/whirlpool-client-cli.svg)](https://jitpack.io/#Samourai-Wallet/whirlpool-client-cli) 3 | 4 | # whirlpool-client-cli 5 | 6 | # This repository is now maintained at https://code.samourai.io/whirlpool/whirlpool-client-cli 7 | 8 | Command line client for [Whirlpool](https://github.com/Samourai-Wallet/Whirlpool) by Samourai-Wallet. 9 | 10 | ## Getting started 11 | 12 | #### Download and verify CLI 13 | - Download whirlpool-client-cli-\[version\]-run.jar from [releases](https://github.com/Samourai-Wallet/whirlpool-client-cli/releases) 14 | - Verify sha256 hash of the jar with signed message in whirlpool-client-cli-\[version\]-run.jar.sig 15 | - Verify signature with [@SamouraiDev](https://github.com/SamouraiDev) 's key 16 | 17 | #### Initial setup 18 | You can setup whirlpool-client-cli in 2 ways: 19 | - command-line: run CLI with ```--init``` 20 | - remotely: run CLI with ```--listen```, then use GUI or API 21 | 22 | #### Run 23 | ``` 24 | java -jar target/whirlpool-client-version-run.jar 25 | ``` 26 | 27 | Optional arguments: 28 | - ```--listen```: enable API for remote commands & GUI. Authentication on startup is optional, but you can authenticate on startup with --authenticate 29 | - ```--mixs-target```: minimum number of mixs to achieve per UTXO 30 | - ```--authenticate```: will ask for your passphrase at startup 31 | - ```--list-pools```: list pools and exit 32 | 33 | 34 | #### Advanced 35 | See [README-API.md](README-API.md) to manage whirlpool-client-cli remotely with REST API. 36 | 37 | See [README-ADVANCED.md](README-ADVANCED.md) for advanced usage, integration and development. 38 | 39 | 40 | ## Resources 41 | * [whirlpool](https://github.com/Samourai-Wallet/Whirlpool) 42 | * [whirlpool-protocol](https://github.com/Samourai-Wallet/whirlpool-protocol) 43 | * [whirlpool-client](https://github.com/Samourai-Wallet/whirlpool-client) 44 | * [whirlpool-server](https://github.com/Samourai-Wallet/whirlpool-server) 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/cli/CliConfigController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.cli; 2 | 3 | import com.samourai.whirlpool.cli.Application; 4 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 5 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 6 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig; 7 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliConfigRequest; 8 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliConfigResponse; 9 | import com.samourai.whirlpool.cli.config.CliConfig; 10 | import com.samourai.whirlpool.cli.services.CliConfigService; 11 | import javax.validation.Valid; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestHeader; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestMethod; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | @RestController 21 | public class CliConfigController extends AbstractRestController { 22 | @Autowired private CliConfig cliConfig; 23 | @Autowired private CliConfigService cliConfigService; 24 | 25 | @RequestMapping(value = CliApiEndpoint.REST_CLI_CONFIG, method = RequestMethod.GET) 26 | public ApiCliConfigResponse getCliConfig(@RequestHeader HttpHeaders headers) throws Exception { 27 | checkHeaders(headers); 28 | 29 | ApiCliConfigResponse response = new ApiCliConfigResponse(cliConfig); 30 | return response; 31 | } 32 | 33 | @RequestMapping(value = CliApiEndpoint.REST_CLI_CONFIG, method = RequestMethod.POST) 34 | public void setCliConfig( 35 | @RequestHeader HttpHeaders headers, @Valid @RequestBody ApiCliConfigRequest payload) 36 | throws Exception { 37 | checkHeaders(headers); 38 | 39 | ApiCliConfig apiCliConfig = payload.getConfig(); 40 | cliConfigService.setApiConfig(apiCliConfig); 41 | } 42 | 43 | @RequestMapping(value = CliApiEndpoint.REST_CLI_CONFIG, method = RequestMethod.DELETE) 44 | public void resetCliConfig(@RequestHeader HttpHeaders headers) throws Exception { 45 | checkHeaders(headers); 46 | 47 | cliConfigService.resetConfiguration(); 48 | Application.restart(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/RunCliCommand.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.wallet.client.Bip84ApiWallet; 4 | import com.samourai.whirlpool.cli.ApplicationArgs; 5 | import com.samourai.whirlpool.cli.config.CliConfig; 6 | import com.samourai.whirlpool.cli.services.CliWalletService; 7 | import com.samourai.whirlpool.cli.services.WalletAggregateService; 8 | import com.samourai.whirlpool.cli.wallet.CliWallet; 9 | import java.lang.invoke.MethodHandles; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | public class RunCliCommand { 14 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 15 | 16 | private ApplicationArgs appArgs; 17 | private CliWalletService cliWalletService; 18 | private WalletAggregateService walletAggregateService; 19 | 20 | public RunCliCommand( 21 | ApplicationArgs appArgs, 22 | CliWalletService cliWalletService, 23 | WalletAggregateService walletAggregateService) { 24 | this.appArgs = appArgs; 25 | this.cliWalletService = cliWalletService; 26 | this.walletAggregateService = walletAggregateService; 27 | } 28 | 29 | public void run() throws Exception { 30 | if (appArgs.isDumpPayload()) { 31 | new RunDumpPayload(cliWalletService).run(); 32 | } else if (appArgs.isAggregatePostmix()) { 33 | CliWallet cliWallet = cliWalletService.getSessionWallet(); 34 | 35 | // go aggregate and consolidate 36 | walletAggregateService.consolidateWallet(cliWallet); 37 | 38 | // should we move to a specific address? 39 | String toAddress = appArgs.getAggregatePostmix(); 40 | if (toAddress != null && !"true".equals(toAddress)) { 41 | Bip84ApiWallet depositWallet = cliWallet.getWalletDeposit(); 42 | log.info(" • Moving funds to: " + toAddress); 43 | walletAggregateService.toAddress(depositWallet, toAddress, cliWallet); 44 | } 45 | } else if (appArgs.isListPools()) { 46 | CliWallet cliWallet = cliWalletService.getSessionWallet(); 47 | new RunListPools(cliWallet).run(); 48 | } else { 49 | throw new Exception("Unknown command."); 50 | } 51 | 52 | if (log.isDebugEnabled()) { 53 | log.debug("RunCliCommand success."); 54 | } 55 | } 56 | 57 | public static boolean hasCommandToRun(ApplicationArgs appArgs, CliConfig cliConfig) { 58 | return appArgs.isDumpPayload() || appArgs.isAggregatePostmix() || appArgs.isListPools(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/beans/ApiPool.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.beans; 2 | 3 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 4 | import com.samourai.whirlpool.protocol.websocket.notifications.MixStatus; 5 | 6 | public class ApiPool { 7 | private String poolId; 8 | private long denomination; 9 | private long feeValue; 10 | private long mustMixBalanceMin; 11 | private long mustMixBalanceCap; 12 | private long mustMixBalanceMax; 13 | private int minAnonymitySet; 14 | private int nbRegistered; 15 | private int mixAnonymitySet; 16 | private MixStatus mixStatus; 17 | private long elapsedTime; 18 | private int nbConfirmed; 19 | private long tx0BalanceMin; 20 | 21 | public ApiPool() {} 22 | 23 | public ApiPool(Pool pool, long tx0BalanceMin) { 24 | this.poolId = pool.getPoolId(); 25 | this.denomination = pool.getDenomination(); 26 | this.feeValue = pool.getFeeValue(); 27 | this.mustMixBalanceMin = pool.getMustMixBalanceMin(); 28 | this.mustMixBalanceCap = pool.getMustMixBalanceCap(); 29 | this.mustMixBalanceMax = pool.getMustMixBalanceMax(); 30 | this.minAnonymitySet = pool.getMinAnonymitySet(); 31 | this.nbRegistered = pool.getNbRegistered(); 32 | this.mixAnonymitySet = pool.getMixAnonymitySet(); 33 | this.mixStatus = pool.getMixStatus(); 34 | this.elapsedTime = pool.getElapsedTime(); 35 | this.nbConfirmed = pool.getNbConfirmed(); 36 | this.tx0BalanceMin = tx0BalanceMin; 37 | } 38 | 39 | public String getPoolId() { 40 | return poolId; 41 | } 42 | 43 | public long getDenomination() { 44 | return denomination; 45 | } 46 | 47 | public long getFeeValue() { 48 | return feeValue; 49 | } 50 | 51 | public long getMustMixBalanceMin() { 52 | return mustMixBalanceMin; 53 | } 54 | 55 | public long getMustMixBalanceCap() { 56 | return mustMixBalanceCap; 57 | } 58 | 59 | public long getMustMixBalanceMax() { 60 | return mustMixBalanceMax; 61 | } 62 | 63 | public int getMinAnonymitySet() { 64 | return minAnonymitySet; 65 | } 66 | 67 | public int getNbRegistered() { 68 | return nbRegistered; 69 | } 70 | 71 | public int getMixAnonymitySet() { 72 | return mixAnonymitySet; 73 | } 74 | 75 | public MixStatus getMixStatus() { 76 | return mixStatus; 77 | } 78 | 79 | public long getElapsedTime() { 80 | return elapsedTime; 81 | } 82 | 83 | public int getNbConfirmed() { 84 | return nbConfirmed; 85 | } 86 | 87 | public long getTx0BalanceMin() { 88 | return tx0BalanceMin; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/rest/ApiWalletUtxosResponse.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.rest; 2 | 3 | import com.google.common.primitives.Ints; 4 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiWallet; 5 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 6 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; 7 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxoState; 8 | import java.util.Comparator; 9 | import java8.lang.Longs; 10 | 11 | public class ApiWalletUtxosResponse { 12 | private ApiWallet deposit; 13 | private ApiWallet premix; 14 | private ApiWallet postmix; 15 | 16 | public ApiWalletUtxosResponse(WhirlpoolWallet whirlpoolWallet) throws Exception { 17 | Comparator comparator = 18 | (o1, o2) -> { 19 | // last activity first 20 | WhirlpoolUtxoState s1 = o1.getUtxoState(); 21 | WhirlpoolUtxoState s2 = o2.getUtxoState(); 22 | if (s1.getLastActivity() != null || s2.getLastActivity() != null) { 23 | if (s1.getLastActivity() != null && s2.getLastActivity() == null) { 24 | return -1; 25 | } 26 | if (s2.getLastActivity() != null && s1.getLastActivity() == null) { 27 | return 1; 28 | } 29 | int compare = Longs.compare(s2.getLastActivity(), s1.getLastActivity()); 30 | if (compare != 0) { 31 | return compare; 32 | } 33 | } 34 | 35 | // last confirmed 36 | return Ints.compare(o1.getUtxo().confirmations, o2.getUtxo().confirmations); 37 | }; 38 | int mixsTargetMin = whirlpoolWallet.getConfig().getMixsTarget(); 39 | this.deposit = 40 | new ApiWallet( 41 | whirlpoolWallet.getUtxosDeposit(), 42 | whirlpoolWallet.getZpubDeposit(), 43 | comparator, 44 | mixsTargetMin); 45 | this.premix = 46 | new ApiWallet( 47 | whirlpoolWallet.getUtxosPremix(), 48 | whirlpoolWallet.getZpubPremix(), 49 | comparator, 50 | mixsTargetMin); 51 | this.postmix = 52 | new ApiWallet( 53 | whirlpoolWallet.getUtxosPostmix(), 54 | whirlpoolWallet.getZpubPostmix(), 55 | comparator, 56 | mixsTargetMin); 57 | } 58 | 59 | public ApiWallet getDeposit() { 60 | return deposit; 61 | } 62 | 63 | public ApiWallet getPremix() { 64 | return premix; 65 | } 66 | 67 | public ApiWallet getPostmix() { 68 | return postmix; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/services/JavaHttpClientService.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.http.client.IHttpClientService; 5 | import com.samourai.http.client.JavaHttpClient; 6 | import com.samourai.whirlpool.cli.beans.CliProxy; 7 | import com.samourai.whirlpool.cli.config.CliConfig; 8 | import com.samourai.whirlpool.cli.utils.CliUtils; 9 | import java.lang.invoke.MethodHandles; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import org.eclipse.jetty.client.HttpClient; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.stereotype.Service; 17 | 18 | @Service 19 | public class JavaHttpClientService implements IHttpClientService { 20 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 21 | 22 | private CliTorClientService torClientService; 23 | private CliConfig cliConfig; 24 | 25 | private Map httpClients; 26 | 27 | public JavaHttpClientService(CliTorClientService torClientService, CliConfig cliConfig) { 28 | this.torClientService = torClientService; 29 | this.cliConfig = cliConfig; 30 | this.httpClients = new ConcurrentHashMap<>(); 31 | } 32 | 33 | public JavaHttpClient getHttpClient(HttpUsage httpUsage) { 34 | JavaHttpClient httpClient = httpClients.get(httpUsage); 35 | if (httpClient == null) { 36 | if (log.isDebugEnabled()) { 37 | log.debug("+httpClient[" + httpUsage + "]"); 38 | } 39 | httpClient = computeHttpClient(httpUsage); 40 | httpClients.put(httpUsage, httpClient); 41 | } 42 | return httpClient; 43 | } 44 | 45 | private JavaHttpClient computeHttpClient(HttpUsage httpUsage) { 46 | // use Tor proxy if any 47 | Optional cliProxy = torClientService.getTorProxy(httpUsage); 48 | // or default proxy 49 | if (!cliProxy.isPresent()) { 50 | cliProxy = cliConfig.getCliProxy(); 51 | } 52 | HttpClient httpClient = CliUtils.computeHttpClient(cliProxy); 53 | return new JavaHttpClient(httpUsage, httpClient, cliConfig.getRequestTimeout()); 54 | } 55 | 56 | public void changeIdentityRest() { 57 | for (JavaHttpClient httpClient : httpClients.values()) { 58 | // restart REST clients 59 | if (httpClient.getHttpUsage().isRest()) { 60 | if (httpClient != null) { 61 | httpClient.restart(); 62 | } 63 | } 64 | } 65 | // don't break non-REST connexions, it will be renewed on next connexion 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/client/run/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.client.run; 2 | 3 | import com.samourai.whirlpool.cli.Application; 4 | import org.junit.After; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Ignore; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.boot.ApplicationArguments; 11 | import org.springframework.boot.DefaultApplicationArguments; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest 17 | @Ignore 18 | public class ApplicationTest extends AbstractApplicationTest { 19 | 20 | @Before 21 | @Override 22 | public void setup() throws Exception { 23 | super.setup(); 24 | } 25 | 26 | @After 27 | @Override 28 | public void tearDown() throws Exception { 29 | super.tearDown(); 30 | } 31 | 32 | @Test 33 | public void runListPools() { 34 | String[] args = new String[] {"--debug"}; 35 | ApplicationArguments appArgs = new DefaultApplicationArguments(args); 36 | 37 | new Application().run(appArgs); 38 | 39 | Assert.assertTrue(getOut().contains(" • Fetching pools...")); 40 | Assert.assertTrue(getErr().isEmpty()); 41 | } 42 | 43 | @Test 44 | public void runApp() { 45 | String[] args = 46 | new String[] { 47 | "--listen", 48 | "--authenticate", 49 | "--debug-client", 50 | "--debug", 51 | "--clients=5", 52 | "--auto-tx0=0.01btc", 53 | "--tx0-max-outputs=15" 54 | }; 55 | Application.main(args); 56 | while (true) { 57 | try { 58 | Thread.sleep(100000); 59 | } catch (InterruptedException e) { 60 | } 61 | } 62 | } 63 | 64 | @Test 65 | public void runWhirlpool() { 66 | String[] args = 67 | new String[] { 68 | "--utxo=733a1bcb4145e3dd0ea3e6709bef9504fd252c9a26b254508539e3636db659c2-1", 69 | "--utxo-key=cUe6J7Fs5mxg6jLwXE27xcDpaTPXfQZ9oKDbxs5PP6EpYMFHab2T", 70 | "--utxo-balance=1000102", 71 | "--seed-passphrase=w0", 72 | "--seed-words=all all all all all all all all all all all all", 73 | "--mixs=5", 74 | "--debug", 75 | "--test-mode" 76 | }; 77 | ApplicationArguments appArgs = new DefaultApplicationArguments(args); 78 | 79 | captureSystem(); 80 | new Application().run(appArgs); // TODO mock server 81 | resetSystem(); 82 | 83 | Assert.assertTrue(getOut().contains(" • connecting to ")); 84 | Assert.assertTrue(getErr().isEmpty()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/tor/client/JavaTorSettings.java: -------------------------------------------------------------------------------- 1 | package com.samourai.tor.client; 2 | 3 | import com.msopentech.thali.toronionproxy.DefaultSettings; 4 | import com.samourai.whirlpool.cli.beans.CliProxy; 5 | import com.samourai.whirlpool.cli.beans.CliProxyProtocol; 6 | import java.util.Optional; 7 | 8 | public class JavaTorSettings extends DefaultSettings { 9 | private CliProxy cliProxy; 10 | private String customTorrc; 11 | 12 | public JavaTorSettings(Optional cliProxy, String customTorrc) { 13 | this.cliProxy = cliProxy.orElse(null); 14 | this.customTorrc = customTorrc; 15 | } 16 | 17 | @Override 18 | public String getCustomTorrc() { 19 | return customTorrc; 20 | } 21 | 22 | @Override 23 | public String dnsPort() { 24 | return "auto"; 25 | } 26 | 27 | @Override 28 | public int getHttpTunnelPort() { 29 | return 0; 30 | } 31 | 32 | @Override 33 | public int getRelayPort() { 34 | return 0; 35 | } 36 | 37 | @Override 38 | public String getSocksPort() { 39 | return "auto"; 40 | } 41 | 42 | @Override 43 | public String transPort() { 44 | // not available on mac 45 | return "0"; 46 | } 47 | 48 | @Override 49 | public boolean runAsDaemon() { 50 | return false; 51 | } 52 | 53 | @Override 54 | public boolean hasSafeSocks() { 55 | // remote DNS resolving is not supported by Java 56 | return false; 57 | } 58 | 59 | @Override 60 | public String getProxyHost() { 61 | if (cliProxy != null && CliProxyProtocol.HTTP.equals(cliProxy.getProtocol())) { 62 | return cliProxy.getHost(); 63 | } 64 | return null; 65 | } 66 | 67 | @Override 68 | public String getProxyPort() { 69 | if (cliProxy != null && CliProxyProtocol.HTTP.equals(cliProxy.getProtocol())) { 70 | return Integer.toString(cliProxy.getPort()); 71 | } 72 | return null; 73 | } 74 | 75 | @Override 76 | public String getProxySocks5Host() { 77 | if (cliProxy != null && CliProxyProtocol.SOCKS.equals(cliProxy.getProtocol())) { 78 | return cliProxy.getHost(); 79 | } 80 | return null; 81 | } 82 | 83 | @Override 84 | public String getProxySocks5ServerPort() { 85 | if (cliProxy != null && CliProxyProtocol.SOCKS.equals(cliProxy.getProtocol())) { 86 | return Integer.toString(cliProxy.getPort()); 87 | } 88 | return null; 89 | } 90 | 91 | @Override 92 | public boolean disableNetwork() { 93 | return false; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return "TorSettings[proxy=" + (cliProxy != null ? cliProxy : "null") + "]"; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/RunCliInit.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.ApplicationArgs; 4 | import com.samourai.whirlpool.cli.beans.WhirlpoolPairingPayload; 5 | import com.samourai.whirlpool.cli.services.CliConfigService; 6 | import com.samourai.whirlpool.cli.services.CliWalletService; 7 | import com.samourai.whirlpool.cli.utils.CliUtils; 8 | import java.lang.invoke.MethodHandles; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class RunCliInit { 13 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 14 | 15 | private ApplicationArgs appArgs; 16 | private CliConfigService cliConfigService; 17 | private CliWalletService cliWalletService; 18 | 19 | public RunCliInit( 20 | ApplicationArgs appArgs, 21 | CliConfigService cliConfigService, 22 | CliWalletService cliWalletService) { 23 | this.appArgs = appArgs; 24 | this.cliConfigService = cliConfigService; 25 | this.cliWalletService = cliWalletService; 26 | } 27 | 28 | public void run() throws Exception { 29 | log.info(CliUtils.LOG_SEPARATOR); 30 | log.info("⣿ CLI INITIALIZATION"); 31 | log.info("⣿ This will intialize CLI and connect it to your existing Samourai Wallet."); 32 | log.info("⣿ "); 33 | 34 | // pairing payload 35 | log.info( 36 | "⣿ Get your pairing payload in Samourai Wallet, go to 'Settings/Transactions/Experimental'"); 37 | log.info("⣿ • Paste your pairing payload here:"); 38 | String pairingPayload = CliUtils.readUserInputRequired("Pairing payload?", false); 39 | log.info("⣿ "); 40 | WhirlpoolPairingPayload pairing = cliConfigService.parsePairingPayload(pairingPayload); 41 | 42 | // Tor 43 | boolean tor; 44 | if (pairing.getDojo() != null) { 45 | // dojo => Tor enabled 46 | log.info("⣿ Pairing with Dojo => Tor enabled."); 47 | tor = true; 48 | } else { 49 | // samourai backend => Tor optional 50 | log.info("⣿ • Enable Tor? (you can change this later)"); 51 | String torStr = 52 | CliUtils.readUserInputRequired( 53 | "Enable Tor? (y/n)", false, new String[] {"y", "n", "Y", "N"}); 54 | tor = torStr.toLowerCase().equals("y"); 55 | log.info("⣿ "); 56 | } 57 | 58 | // init 59 | String apiKey = cliConfigService.initialize(pairing, tor, null); 60 | 61 | log.info(CliUtils.LOG_SEPARATOR); 62 | log.info("⣿ INITIALIZATION SUCCESS"); 63 | log.info("⣿ Take note of your API Key, to connect remotely from GUI or API."); 64 | log.info("⣿ Your API key is: " + apiKey); 65 | log.info("⣿ "); 66 | log.info("⣿ Restarting CLI..."); 67 | log.info(CliUtils.LOG_SEPARATOR); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/beans/WhirlpoolPairingPayload.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import com.samourai.wallet.api.pairing.PairingNetwork; 4 | import com.samourai.wallet.api.pairing.PairingPayload; 5 | import com.samourai.wallet.api.pairing.PairingType; 6 | import com.samourai.wallet.api.pairing.PairingVersion; 7 | import com.samourai.whirlpool.client.exception.NotifiableException; 8 | import com.samourai.whirlpool.client.utils.ClientUtils; 9 | import java.lang.invoke.MethodHandles; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | public class WhirlpoolPairingPayload extends PairingPayload { 14 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 15 | 16 | public WhirlpoolPairingPayload() { 17 | super(); 18 | } 19 | 20 | public WhirlpoolPairingPayload( 21 | PairingVersion version, 22 | PairingNetwork network, 23 | String mnemonic, 24 | Boolean passphrase, 25 | PairingDojo dojo) { 26 | super(PairingType.WHIRLPOOL_GUI, version, network, mnemonic, passphrase, dojo); 27 | } 28 | 29 | public static WhirlpoolPairingPayload parse(String pairingPayloadStr) throws NotifiableException { 30 | WhirlpoolPairingPayload pairingPayload; 31 | try { 32 | pairingPayload = ClientUtils.fromJson(pairingPayloadStr, WhirlpoolPairingPayload.class); 33 | } catch (NotifiableException e) { 34 | throw e; 35 | } catch (Exception e) { 36 | log.error("", e); 37 | throw new NotifiableException("Invalid pairing payload"); 38 | } 39 | 40 | // passphrase=true for V1 41 | if (pairingPayload.getPairing().getPassphrase() == null 42 | && PairingVersion.V1_0_0.equals(pairingPayload.getPairing().getVersion())) { 43 | pairingPayload.getPairing().setPassphrase(true); 44 | } 45 | pairingPayload.validate(); 46 | return pairingPayload; 47 | } 48 | 49 | protected void validate() throws NotifiableException { 50 | // main validation 51 | try { 52 | super.validate(); 53 | } catch (Exception e) { 54 | throw new NotifiableException(e.getMessage()); 55 | } 56 | 57 | // whirlpool validation 58 | if (!PairingType.WHIRLPOOL_GUI.equals(getPairing().getType())) { 59 | throw new NotifiableException("Unsupported pairing.type"); 60 | } 61 | if (!PairingVersion.V1_0_0.equals(getPairing().getVersion()) 62 | && !PairingVersion.V2_0_0.equals(getPairing().getVersion()) 63 | && !PairingVersion.V3_0_0.equals(getPairing().getVersion())) { 64 | throw new NotifiableException("Unsupported pairing.version"); 65 | } 66 | if (getPairing().getPassphrase() == null) { 67 | throw new NotifiableException("Invalid pairing.passphrase"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/services/CliTorClientService.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.tor.client.JavaTorClient; 5 | import com.samourai.whirlpool.cli.beans.CliProxy; 6 | import com.samourai.whirlpool.cli.config.CliConfig; 7 | import com.samourai.whirlpool.client.exception.NotifiableException; 8 | import java.lang.invoke.MethodHandles; 9 | import java.util.Collection; 10 | import java.util.Optional; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.stereotype.Service; 14 | 15 | // JavaTorClient wrapper for watching for cliConfig changes 16 | @Service 17 | public class CliTorClientService { 18 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 19 | 20 | private Optional torClient; 21 | private CliConfig cliConfig; 22 | private Collection torHttpUsages; 23 | 24 | public CliTorClientService(CliConfig cliConfig) { 25 | this.torClient = Optional.empty(); 26 | this.cliConfig = cliConfig; 27 | this.torHttpUsages = cliConfig.computeTorHttpUsages(); 28 | } 29 | 30 | public void setup() throws Exception { 31 | if (cliConfig.getTor()) { 32 | if (!torClient.isPresent()) { 33 | if (log.isDebugEnabled()) { 34 | log.debug("Enabling Tor for: " + torHttpUsages); 35 | } 36 | // instanciate & initialize 37 | JavaTorClient tc = new JavaTorClient(cliConfig, torHttpUsages); 38 | tc.setup(); // throws 39 | torClient = Optional.of(tc); 40 | if (log.isDebugEnabled()) { 41 | log.debug("Tor is enabled."); 42 | } 43 | } 44 | } else { 45 | if (log.isDebugEnabled()) { 46 | log.debug("Tor is disabled."); 47 | } 48 | } 49 | } 50 | 51 | public void connect() { 52 | if (torClient.isPresent()) { 53 | torClient.get().connect(); 54 | } 55 | } 56 | 57 | public void waitReady() throws NotifiableException { 58 | if (torClient.isPresent()) { 59 | torClient.get().waitReady(); 60 | } 61 | } 62 | 63 | public void shutdown() { 64 | if (torClient.isPresent()) { 65 | torClient.get().shutdown(); 66 | } 67 | } 68 | 69 | public void changeIdentity() { 70 | if (torClient.isPresent()) { 71 | torClient.get().changeIdentity(); 72 | } 73 | } 74 | 75 | public Optional getTorProxy(HttpUsage httpUsage) { 76 | boolean isTorUsage = torHttpUsages.contains(httpUsage); 77 | if (isTorUsage && torClient.isPresent()) { 78 | return torClient.get().getTorProxy(httpUsage); 79 | } 80 | return Optional.empty(); 81 | } 82 | 83 | public Optional getProgress() { 84 | if (!torClient.isPresent()) { 85 | if (cliConfig.getTor()) { 86 | return Optional.of(0); // Tor is initializing 87 | } 88 | return Optional.empty(); // Tor is disabled 89 | } 90 | 91 | int progress = torClient.get().getProgress(); 92 | return Optional.of(progress); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/cli/utils/EncryptUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.utils; 2 | 3 | import com.samourai.whirlpool.cli.beans.Encrypted; 4 | import java.lang.invoke.MethodHandles; 5 | import org.bouncycastle.util.encoders.Base64; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class EncryptUtilsTest { 12 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 13 | 14 | private static final String SERIALIZED = 15 | "mD37gOLQe2BVzOe0OATJGF3oTcCzjkNplCBj/IxN8IucuRN55CL1Wolk6noorpFQGTJtUl4MB/W7WxypUSDeZZKAkCpqMe2VkajaONNKIfydkwWhrRFmL6J5iBI0sR92zqhWmIywfrCZjWNWz5I+yv/GizMeu/xZ8apAswftp6r+tSo="; 16 | private static final String IV = "mD37gOLQe2BVzOe0OATJGA=="; 17 | private static final String SALT = "XehNwLOOQ2k="; 18 | private static final String CT = 19 | "lCBj/IxN8IucuRN55CL1Wolk6noorpFQGTJtUl4MB/W7WxypUSDeZZKAkCpqMe2VkajaONNKIfydkwWhrRFmL6J5iBI0sR92zqhWmIywfrCZjWNWz5I+yv/GizMeu/xZ8apAswftp6r+tSo="; 20 | 21 | private static final String KEY = "test"; 22 | private static final String PLAIN = 23 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon ability absorb acid"; 24 | 25 | @Test 26 | public void encrypt() throws Exception { 27 | Encrypted encrypted = EncryptUtils.encrypt(KEY, PLAIN); 28 | 29 | String decrypted = EncryptUtils.decrypt(KEY, encrypted); 30 | 31 | Assert.assertEquals(PLAIN, decrypted); 32 | } 33 | 34 | @Test 35 | public void decrypt() throws Exception { 36 | String iv = IV; 37 | String salt = SALT; 38 | String ct = CT; 39 | Encrypted encrypted = new Encrypted(iv, salt, ct); 40 | String decryptedSeedWords = EncryptUtils.decrypt(KEY, encrypted); 41 | 42 | Assert.assertEquals(PLAIN, decryptedSeedWords); 43 | } 44 | 45 | @Test 46 | public void serializeEncrypted() throws Exception { 47 | String iv = IV; 48 | String salt = SALT; 49 | String ct = CT; 50 | Encrypted encrypted = new Encrypted(iv, salt, ct); 51 | 52 | String serializeEncrypted = EncryptUtils.serializeEncrypted(encrypted); 53 | 54 | Assert.assertEquals(SERIALIZED, serializeEncrypted); 55 | 56 | Encrypted encryptedBis = EncryptUtils.unserializeEncrypted(serializeEncrypted); 57 | Assert.assertArrayEquals(encrypted.getIv(), encryptedBis.getIv()); 58 | Assert.assertArrayEquals(encrypted.getSalt(), encryptedBis.getSalt()); 59 | Assert.assertArrayEquals(encrypted.getCt(), encryptedBis.getCt()); 60 | } 61 | 62 | @Test 63 | public void unserializeEncrypted() throws Exception { 64 | Encrypted encrypted = EncryptUtils.unserializeEncrypted(SERIALIZED); 65 | 66 | // expected 67 | byte[] iv = Base64.decode(IV); 68 | byte[] salt = Base64.decode(SALT); 69 | byte[] ct = Base64.decode(CT); 70 | 71 | Assert.assertArrayEquals(iv, encrypted.getIv()); 72 | Assert.assertArrayEquals(salt, encrypted.getSalt()); 73 | Assert.assertArrayEquals(ct, encrypted.getCt()); 74 | 75 | String serializedBis = EncryptUtils.serializeEncrypted(encrypted); 76 | Assert.assertEquals(SERIALIZED, serializedBis); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/config/CliServicesConfig.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.config; 2 | 3 | import com.samourai.wallet.hd.java.HD_WalletFactoryJava; 4 | import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric; 5 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig; 6 | import java.lang.invoke.MethodHandles; 7 | import org.apache.catalina.connector.Connector; 8 | import org.bitcoinj.core.NetworkParameters; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 12 | import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; 13 | import org.springframework.boot.web.servlet.server.ServletWebServerFactory; 14 | import org.springframework.cache.annotation.EnableCaching; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.core.task.SimpleAsyncTaskExecutor; 18 | import org.springframework.core.task.TaskExecutor; 19 | import org.springframework.web.cors.CorsConfiguration; 20 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 21 | import org.springframework.web.filter.CorsFilter; 22 | 23 | @Configuration 24 | @EnableCaching 25 | public class CliServicesConfig { 26 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 27 | 28 | public CliServicesConfig() {} 29 | 30 | @Bean 31 | TaskExecutor taskExecutor() { 32 | return new SimpleAsyncTaskExecutor(); 33 | } 34 | 35 | @Bean 36 | HD_WalletFactoryJava hdWalletFactory() { 37 | return HD_WalletFactoryJava.getInstance(); 38 | } 39 | 40 | @Bean 41 | Bech32UtilGeneric bech32Util() { 42 | return Bech32UtilGeneric.getInstance(); 43 | } 44 | 45 | @Bean 46 | NetworkParameters networkParameters(CliConfig cliConfig) { 47 | return cliConfig.getServer().getParams(); 48 | } 49 | 50 | @Bean 51 | public CorsFilter corsFilter() { 52 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 53 | CorsConfiguration config = new CorsConfiguration(); 54 | config.setAllowCredentials(true); 55 | config.addAllowedOrigin("*"); 56 | config.addAllowedHeader("*"); 57 | config.addAllowedMethod("OPTIONS"); 58 | config.addAllowedMethod("HEAD"); 59 | config.addAllowedMethod("GET"); 60 | config.addAllowedMethod("PUT"); 61 | config.addAllowedMethod("POST"); 62 | config.addAllowedMethod("DELETE"); 63 | config.addAllowedMethod("PATCH"); 64 | source.registerCorsConfiguration("/**", config); 65 | return new CorsFilter(source); 66 | } 67 | 68 | @Bean 69 | @ConditionalOnProperty(name = ApiCliConfig.KEY_API_HTTP_ENABLE, havingValue = "true") 70 | public ServletWebServerFactory httpServer(CliConfig cliConfig) { 71 | // https not required => configure HTTP server 72 | int httpPort = cliConfig.getApi().getHttpPort(); 73 | if (log.isDebugEnabled()) { 74 | log.debug("Enabling API over HTTP... httpPort=" + httpPort); 75 | } 76 | Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); 77 | connector.setPort(httpPort); 78 | TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); 79 | factory.addAdditionalTomcatConnectors(connector); 80 | return factory; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/CliStatusOrchestrator.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.config.CliConfig; 4 | import com.samourai.whirlpool.cli.exception.NoSessionWalletException; 5 | import com.samourai.whirlpool.cli.services.CliWalletService; 6 | import com.samourai.whirlpool.cli.utils.CliUtils; 7 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 8 | import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; 9 | import com.samourai.whirlpool.client.wallet.beans.MixingState; 10 | import com.samourai.whirlpool.client.wallet.orchestrator.AbstractOrchestrator; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class CliStatusOrchestrator extends AbstractOrchestrator { 15 | private static final Logger log = LoggerFactory.getLogger(CliStatusOrchestrator.class); 16 | 17 | private CliStatusInteractiveOrchestrator statusInteractiveOrchestrator; 18 | private CliWalletService cliWalletService; 19 | private CliConfig cliConfig; 20 | 21 | public CliStatusOrchestrator( 22 | int loopDelay, CliWalletService cliWalletService, CliConfig cliConfig) { 23 | super(loopDelay); 24 | if (CliUtils.hasConsole()) { 25 | this.statusInteractiveOrchestrator = 26 | new CliStatusInteractiveOrchestrator(loopDelay, cliWalletService, cliConfig); 27 | } 28 | this.cliWalletService = cliWalletService; 29 | this.cliConfig = cliConfig; 30 | } 31 | 32 | @Override 33 | public synchronized void start(boolean daemon) { 34 | super.start(daemon); 35 | if (statusInteractiveOrchestrator != null) { 36 | statusInteractiveOrchestrator.start(true); 37 | } 38 | } 39 | 40 | @Override 41 | public synchronized void stop() { 42 | super.stop(); 43 | if (statusInteractiveOrchestrator != null) { 44 | statusInteractiveOrchestrator.stop(); 45 | } 46 | } 47 | 48 | @Override 49 | protected void runOrchestrator() { 50 | printState(); 51 | } 52 | 53 | private void printState() { 54 | try { 55 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 56 | MixingState mixingState = whirlpoolWallet.getMixingState(); 57 | WhirlpoolWalletConfig walletConfig = whirlpoolWallet.getConfig(); 58 | 59 | System.out.print( 60 | "⣿ Wallet OPENED, mix " 61 | + (mixingState.isStarted() ? "STARTED" : "STOPPED") 62 | + (walletConfig.isAutoTx0() ? " +autoTx0=" + walletConfig.getAutoTx0PoolId() : "") 63 | + (walletConfig.isAutoMix() ? " +autoMix" : "") 64 | + (cliConfig.getTor() ? " +Tor" : "") 65 | + (cliConfig.isDojoEnabled() ? " +Dojo" : "") 66 | + ", " 67 | + mixingState.getNbMixing() 68 | + " mixing (" 69 | + mixingState.getNbMixingMustMix() 70 | + "+" 71 | + mixingState.getNbMixingLiquidity() 72 | + "), " 73 | + mixingState.getNbQueued() 74 | + " queued (" 75 | + mixingState.getNbQueuedMustMix() 76 | + "+" 77 | + mixingState.getNbQueuedLiquidity() 78 | + "). Commands: [T]hreads, [D]eposit, [P]remix, P[O]stmix, [S]ystem\r"); 79 | } catch (NoSessionWalletException e) { 80 | System.out.print("⣿ Wallet CLOSED\r"); 81 | } catch (Exception e) { 82 | log.error("", e); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README-ADVANCED.md: -------------------------------------------------------------------------------- 1 | # whirlpool-client-cli for advanced users 2 | 3 | 4 | ## Advanced usage 5 | 6 | #### Debugging 7 | - ```--debug```: debug logs 8 | - ```--debug-client```: more debug logs 9 | - ```--dump-payload```: dump pairing-payload of current wallet and exit 10 | 11 | Any problem with a remote CLI? Test it locally: 12 | - Configure CLI manually: ```java -jar whirlpool-client-cli-xxx-run.jar --debug --init``` 13 | - Then start it with manual authentication: ```java -jar whirlpool-client-cli-xxx-run.jar --debug --authenticate``` 14 | 15 | #### Log file 16 | You can configure a log file in whirlpool-cli-config.properties: 17 | ``` 18 | logging.file = /tmp/whirlpool-cli.log 19 | ``` 20 | 21 | #### Testing loop 22 | You can run CLI in loop mode on testnet to generate liquidity on testnet server: 23 | - run TX0 while possible 24 | - mix while possible 25 | - consolidate wallet when PREMIX is empty and start again 26 | ``` 27 | --clients=5 --auto-tx0=0.01btc --tx0-max-outputs=15 --mixs-target=100 --scode= 28 | ``` 29 | 30 | Adjust mixing rate with ```cli.mix.clientDelay = 60``` 31 | Generate simultaneous liquidity with ```cli.mix.clientsPerPool = 5``` 32 | 33 | 34 | 35 | ## Whirlpool integration 36 | 37 | 38 | #### Authenticate on startup 39 | You can authenticate in several ways: 40 | - ```--authenticate```: manually type your passphrase on startup 41 | - ```--listen```: use the GUI or API to authenticate remotely 42 | 43 | 44 | For security reasons, you should not store your passphrase anywhere. If you really need to automate authentication process, use this at your own risk: 45 | ``` 46 | export PP="mypassphrase" 47 | echo $PP|java -jar whirlpool-client-cli-x-run.jar --authenticate 48 | ``` 49 | 50 | 51 | #### Configuration override 52 | Configuration can be overriden in whirlpool-cli-config.properties (see default configuration in [src/main/resources/application.properties]). 53 | Or with equivalent argument: 54 | ``` 55 | --cli.tor=true --cli.apiKey=foo... 56 | ``` 57 | 58 | Or with following arguments: 59 | - ```--scode=```: scode to use for tx0 60 | - ```--tx0-max-outputs=```: tx0 outputs limit 61 | - ```--auto-tx0=[poolId]```: run tx0 from deposit utxos automatically 62 | - ```--auto-mix=[true/false]```: mix premix utxos automatically 63 | 64 | 65 | #### Custom Tor configuration 66 | Tor should be automatically detected, installed or configured. You can customize it for your needs: 67 | ``` 68 | cli.torConfig.executable = /path/to/bin/tor 69 | ``` 70 | - Use `auto` to use embedded tor, or detecting a local Tor install when your system is not supported. 71 | - Use `local` to detect a local tor install. 72 | - Use custom path to `tor` binary to use your own tor build. 73 | 74 | Custom config can be appended to Torrc with: 75 | ``` 76 | cli.torConfig.customTorrc = /path/to/torrc 77 | ``` 78 | 79 | Tor can be enabled with: 80 | ``` 81 | cli.tor = true # global toggle 82 | cli.torConfig.coordinator.enabled = true # enable for whirlpool coordinator 83 | cli.torConfig.backend.enabled = true # enable for wallet backend 84 | ``` 85 | 86 | Tor mode can be customized with: 87 | ``` 88 | cli.torConfig.coordinator.onion = true # whirlpool server 89 | cli.torConfig.backend.onion = true # wallet backend 90 | ``` 91 | - `true`: Tor hidden services 92 | - `false`: clearnet over Tor 93 | 94 | Tor startup timeout can be customized with: 95 | ``` 96 | cli.torConfig.fileCreationTimeout = 20 # 20s 97 | ``` 98 | 99 | ## Build instructions 100 | Build with maven: 101 | 102 | ``` 103 | cd whirlpool-client-cli 104 | mvn clean install -Dmaven.test.skip=true 105 | ``` -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/beans/ApiUtxo.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.beans; 2 | 3 | import com.samourai.wallet.api.backend.beans.UnspentResponse; 4 | import com.samourai.whirlpool.client.mix.listener.MixStep; 5 | import com.samourai.whirlpool.client.wallet.beans.*; 6 | 7 | public class ApiUtxo { 8 | private String hash; 9 | private int index; 10 | private long value; 11 | private int confirmations; 12 | private String path; 13 | 14 | private WhirlpoolAccount account; 15 | private WhirlpoolUtxoStatus status; 16 | private MixStep mixStep; 17 | private MixableStatus mixableStatus; 18 | private Integer progressPercent; 19 | private String poolId; 20 | private Integer mixsTarget; 21 | private Integer mixsTargetOrDefault; 22 | private int mixsDone; 23 | private String message; 24 | private String error; 25 | private Long lastActivityElapsed; 26 | 27 | public ApiUtxo(WhirlpoolUtxo whirlpoolUtxo, int mixsTargetMin) { 28 | UnspentResponse.UnspentOutput utxo = whirlpoolUtxo.getUtxo(); 29 | this.hash = utxo.tx_hash; 30 | this.index = utxo.tx_output_n; 31 | this.value = utxo.value; 32 | this.confirmations = utxo.confirmations; 33 | this.path = utxo.xpub.path; 34 | 35 | this.account = whirlpoolUtxo.getAccount(); 36 | WhirlpoolUtxoConfig utxoConfig = whirlpoolUtxo.getUtxoConfig(); 37 | WhirlpoolUtxoState utxoState = whirlpoolUtxo.getUtxoState(); 38 | this.status = utxoState.getStatus(); 39 | this.mixStep = 40 | utxoState.getMixProgress() != null ? utxoState.getMixProgress().getMixStep() : null; 41 | this.mixableStatus = utxoState.getMixableStatus(); 42 | this.progressPercent = 43 | utxoState.getMixProgress() != null ? utxoState.getMixProgress().getProgressPercent() : null; 44 | this.poolId = utxoConfig.getPoolId(); 45 | this.mixsTarget = utxoConfig.getMixsTarget(); 46 | this.mixsTargetOrDefault = utxoConfig.getMixsTargetOrDefault(mixsTargetMin); 47 | this.mixsDone = utxoConfig.getMixsDone(); 48 | this.message = utxoState.getMessage(); 49 | this.error = utxoState.getError(); 50 | this.lastActivityElapsed = 51 | utxoState.getLastActivity() != null 52 | ? System.currentTimeMillis() - utxoState.getLastActivity() 53 | : null; 54 | } 55 | 56 | public String getHash() { 57 | return hash; 58 | } 59 | 60 | public int getIndex() { 61 | return index; 62 | } 63 | 64 | public long getValue() { 65 | return value; 66 | } 67 | 68 | public int getConfirmations() { 69 | return confirmations; 70 | } 71 | 72 | public String getPath() { 73 | return path; 74 | } 75 | 76 | public WhirlpoolAccount getAccount() { 77 | return account; 78 | } 79 | 80 | public WhirlpoolUtxoStatus getStatus() { 81 | return status; 82 | } 83 | 84 | public MixStep getMixStep() { 85 | return mixStep; 86 | } 87 | 88 | public MixableStatus getMixableStatus() { 89 | return mixableStatus; 90 | } 91 | 92 | public Integer getProgressPercent() { 93 | return progressPercent; 94 | } 95 | 96 | public String getPoolId() { 97 | return poolId; 98 | } 99 | 100 | public Integer getMixsTarget() { 101 | return mixsTarget; 102 | } 103 | 104 | public Integer getMixsTargetOrDefault() { 105 | return mixsTargetOrDefault; 106 | } 107 | 108 | public int getMixsDone() { 109 | return mixsDone; 110 | } 111 | 112 | public String getMessage() { 113 | return message; 114 | } 115 | 116 | public String getError() { 117 | return error; 118 | } 119 | 120 | public Long getLastActivityElapsed() { 121 | return lastActivityElapsed; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/msopentech/thali/toronionproxy/OsData.java: -------------------------------------------------------------------------------- 1 | // TODO https://github.com/thaliproject/Tor_Onion_Proxy_Library/pull/126 2 | // TODO https://github.com/thaliproject/Tor_Onion_Proxy_Library/pull/125 3 | /* 4 | Copyright (C) 2011-2014 Sublime Software Ltd 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | /* 20 | Copyright (c) Microsoft Open Technologies, Inc. 21 | All Rights Reserved 22 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 23 | License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 24 | 25 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, 26 | INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 27 | MERCHANTABLITY OR NON-INFRINGEMENT. 28 | 29 | See the Apache 2 License for the specific language governing permissions and limitations under the License. 30 | */ 31 | 32 | package com.msopentech.thali.toronionproxy; 33 | 34 | import java.io.IOException; 35 | import java.util.Scanner; 36 | 37 | public class OsData { 38 | public enum OsType { 39 | WINDOWS, 40 | LINUX_32, 41 | LINUX_64, 42 | MAC, 43 | ANDROID, 44 | UNSUPPORTED 45 | } 46 | 47 | private static OsType detectedType = null; 48 | 49 | public static OsType getOsType() { 50 | if (detectedType == null) { 51 | detectedType = actualGetOsType(); 52 | } 53 | 54 | return detectedType; 55 | } 56 | 57 | /** 58 | * Yes, I should use a proper memoization abstract class but, um, next time. 59 | * 60 | * @return Type of OS we are running on 61 | */ 62 | protected static OsType actualGetOsType() { 63 | 64 | if (System.getProperty("java.vm.name").contains("Dalvik")) { 65 | return OsType.ANDROID; 66 | } 67 | 68 | String osName = System.getProperty("os.name"); 69 | if (osName.contains("Windows")) { 70 | return OsType.WINDOWS; 71 | } else if (osName.contains("Mac")) { 72 | return OsType.MAC; 73 | } else if (osName.contains("Linux")) { 74 | return getLinuxType(); 75 | } 76 | return OsType.UNSUPPORTED; 77 | } 78 | 79 | protected static OsType getLinuxType() { 80 | String[] cmd = {"uname", "-m"}; 81 | Process unameProcess = null; 82 | Scanner scanner = null; 83 | try { 84 | String unameOutput; 85 | unameProcess = Runtime.getRuntime().exec(cmd); 86 | 87 | scanner = new Scanner(unameProcess.getInputStream()); 88 | if (scanner.hasNextLine()) { 89 | unameOutput = scanner.nextLine(); 90 | } else { 91 | throw new RuntimeException("Couldn't get output from uname call"); 92 | } 93 | 94 | int exit = unameProcess.waitFor(); 95 | if (exit != 0) { 96 | throw new RuntimeException("Uname returned error code " + exit); 97 | } 98 | 99 | if (unameOutput.compareTo("i686") == 0) { 100 | return OsType.LINUX_32; 101 | } 102 | if (unameOutput.compareTo("x86_64") == 0) { 103 | return OsType.LINUX_64; 104 | } 105 | return OsType.UNSUPPORTED; 106 | } catch (IOException e) { 107 | throw new RuntimeException("Uname failure", e); 108 | } catch (InterruptedException e) { 109 | throw new RuntimeException("Uname failure", e); 110 | } finally { 111 | if (unameProcess != null) { 112 | scanner.close(); 113 | unameProcess.destroy(); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/xmanager/client/XManagerClientTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.xmanager.client; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.http.client.JavaHttpClient; 5 | import com.samourai.whirlpool.cli.utils.CliUtils; 6 | import com.samourai.whirlpool.client.test.AbstractTest; 7 | import com.samourai.xmanager.protocol.XManagerService; 8 | import com.samourai.xmanager.protocol.rest.AddressIndexResponse; 9 | import java.util.Optional; 10 | import org.eclipse.jetty.client.HttpClient; 11 | import org.junit.Assert; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class XManagerClientTest extends AbstractTest { 16 | private static final boolean testnet = true; 17 | private static final long requestTimeout = 5000; 18 | 19 | private XManagerClient xManagerClient; 20 | private XManagerClient xManagerClientFailing; 21 | 22 | public XManagerClientTest() throws Exception { 23 | HttpClient jettyHttpClient = CliUtils.computeHttpClient(Optional.empty(), "whirlpool-cli/test"); 24 | JavaHttpClient httpClient = 25 | new JavaHttpClient(HttpUsage.BACKEND, jettyHttpClient, requestTimeout); 26 | xManagerClient = new XManagerClient(testnet, false, httpClient); 27 | 28 | JavaHttpClient httpClientFailing = new JavaHttpClient(HttpUsage.BACKEND, null, requestTimeout); 29 | xManagerClientFailing = new XManagerClient(testnet, false, httpClientFailing); 30 | } 31 | 32 | @Test 33 | public void getAddressOrDefault() throws Exception { 34 | String address = xManagerClient.getAddressOrDefault(XManagerService.WHIRLPOOL); 35 | Assertions.assertNotNull(address); 36 | Assertions.assertNotEquals(XManagerService.WHIRLPOOL.getDefaultAddress(testnet), address); 37 | } 38 | 39 | @Test 40 | public void getAddressOrDefault_failure() throws Exception { 41 | String address = xManagerClientFailing.getAddressOrDefault(XManagerService.WHIRLPOOL); 42 | Assertions.assertEquals(XManagerService.WHIRLPOOL.getDefaultAddress(testnet), address); 43 | } 44 | 45 | @Test 46 | public void getAddressIndexOrDefault() throws Exception { 47 | AddressIndexResponse addressIndexResponse = 48 | xManagerClient.getAddressIndexOrDefault(XManagerService.WHIRLPOOL); 49 | Assertions.assertNotNull(addressIndexResponse); 50 | Assertions.assertNotEquals( 51 | XManagerService.WHIRLPOOL.getDefaultAddress(testnet), addressIndexResponse.address); 52 | Assertions.assertTrue(addressIndexResponse.index > 0); 53 | } 54 | 55 | @Test 56 | public void getAddressIndexOrDefault_failure() throws Exception { 57 | AddressIndexResponse addressIndexResponse = 58 | xManagerClientFailing.getAddressIndexOrDefault(XManagerService.WHIRLPOOL); 59 | Assertions.assertEquals( 60 | XManagerService.WHIRLPOOL.getDefaultAddress(testnet), addressIndexResponse.address); 61 | Assertions.assertEquals(0, addressIndexResponse.index); 62 | } 63 | 64 | @Test 65 | public void verifyAddressIndexResponseOrException() throws Exception { 66 | Assertions.assertTrue( 67 | xManagerClient.verifyAddressIndexResponseOrException( 68 | XManagerService.WHIRLPOOL, "tb1q6m3urxjc8j2l8fltqj93jarmzn0975nnxuymnx", 0)); 69 | Assertions.assertFalse( 70 | xManagerClient.verifyAddressIndexResponseOrException( 71 | XManagerService.WHIRLPOOL, "tb1qz84ma37y3d759sdy7mvq3u4vsxlg2qahw3lm23", 0)); 72 | 73 | Assertions.assertTrue( 74 | xManagerClient.verifyAddressIndexResponseOrException( 75 | XManagerService.WHIRLPOOL, "tb1qcaerxclcmu9llc7ugh65hemqg6raaz4sul535f", 1)); 76 | Assertions.assertFalse( 77 | xManagerClient.verifyAddressIndexResponseOrException( 78 | XManagerService.WHIRLPOOL, "tb1qcfgn9nlgxu0ycj446prdkg0p36qy5a39pcf74v", 1)); 79 | } 80 | 81 | @Test 82 | public void verifyAddressIndexResponseOrException_failure() throws Exception { 83 | try { 84 | xManagerClientFailing.verifyAddressIndexResponseOrException( 85 | XManagerService.WHIRLPOOL, "tb1qcfgn9nlgxu0ycj446prdkg0p36qy5a39pcf74v", 0); 86 | Assert.assertTrue(false); // exception expected 87 | } catch (RuntimeException e) { 88 | // ok 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/services/TxAggregateService.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.wallet.bip69.BIP69InputComparator; 4 | import com.samourai.wallet.hd.HD_Address; 5 | import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric; 6 | import com.samourai.wallet.util.FeeUtil; 7 | import com.samourai.wallet.util.TxUtil; 8 | import java.lang.invoke.MethodHandles; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import org.bitcoinj.core.ECKey; 15 | import org.bitcoinj.core.NetworkParameters; 16 | import org.bitcoinj.core.Transaction; 17 | import org.bitcoinj.core.TransactionInput; 18 | import org.bitcoinj.core.TransactionOutPoint; 19 | import org.bitcoinj.core.TransactionOutput; 20 | import org.bouncycastle.util.encoders.Hex; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.stereotype.Service; 24 | 25 | @Service 26 | public class TxAggregateService { 27 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 28 | private final NetworkParameters params; 29 | private Bech32UtilGeneric bech32Util; 30 | 31 | public TxAggregateService(NetworkParameters params, Bech32UtilGeneric bech32Util) { 32 | this.params = params; 33 | this.bech32Util = bech32Util; 34 | } 35 | 36 | public Transaction txAggregate( 37 | List spendFromOutpoints, 38 | List spendFromAddresses, 39 | String toAddress, 40 | long feeSatPerByte) 41 | throws Exception { 42 | 43 | long inputsValue = spendFromOutpoints.stream().mapToLong(o -> o.getValue().getValue()).sum(); 44 | 45 | Transaction tx = new Transaction(params); 46 | long minerFee = 47 | FeeUtil.getInstance() 48 | .estimatedFeeSegwit(spendFromOutpoints.size(), 0, 0, 1, 0, feeSatPerByte); 49 | long destinationValue = inputsValue - minerFee; 50 | 51 | // 1 output 52 | if (log.isDebugEnabled()) { 53 | log.debug("Tx out: address=" + toAddress + " (" + destinationValue + " sats)"); 54 | } 55 | 56 | TransactionOutput output = bech32Util.getTransactionOutput(toAddress, destinationValue, params); 57 | tx.addOutput(output); 58 | 59 | // prepare N inputs 60 | List inputs = new ArrayList<>(); 61 | Map keysByInput = new HashMap<>(); 62 | for (int i = 0; i < spendFromOutpoints.size(); i++) { 63 | TransactionOutPoint spendFromOutpoint = spendFromOutpoints.get(i); 64 | HD_Address spendFromAddress = spendFromAddresses.get(i); 65 | String spendFromAddressBech32 = bech32Util.toBech32(spendFromAddress, params); 66 | ECKey spendFromKey = spendFromAddress.getECKey(); 67 | 68 | // final Script segwitPubkeyScript = ScriptBuilder.createP2WPKHOutputScript(spendFromKey); 69 | new Transaction(params); 70 | TransactionInput txInput = 71 | new TransactionInput( 72 | params, null, new byte[] {}, spendFromOutpoint, spendFromOutpoint.getValue()); 73 | inputs.add(txInput); 74 | keysByInput.put(txInput, spendFromKey); 75 | if (log.isDebugEnabled()) { 76 | log.debug( 77 | "Tx in: address=" 78 | + spendFromAddressBech32 79 | + ", utxo=" 80 | + spendFromOutpoint 81 | + ", path=" 82 | + spendFromAddress.toJSON().get("path") 83 | + " (" 84 | + spendFromOutpoint.getValue().getValue() 85 | + " sats)"); 86 | } 87 | } 88 | 89 | // sort inputs & add 90 | Collections.sort(inputs, new BIP69InputComparator()); 91 | for (TransactionInput ti : inputs) { 92 | tx.addInput(ti); 93 | } 94 | 95 | // sign inputs 96 | for (TransactionInput txInput : inputs) { 97 | ECKey spendFromKey = keysByInput.get(txInput); 98 | TransactionOutPoint txo = txInput.getOutpoint(); 99 | int inputIndex = 100 | TxUtil.getInstance().findInputIndex(tx, txo.getHash().toString(), txo.getIndex()); 101 | TxUtil.getInstance() 102 | .signInputSegwit(tx, inputIndex, spendFromKey, txInput.getValue().getValue(), params); 103 | } 104 | 105 | final String hexTx = new String(Hex.encode(tx.bitcoinSerialize())); 106 | final String strTxHash = tx.getHashAsString(); 107 | 108 | tx.verify(); 109 | if (log.isDebugEnabled()) { 110 | log.debug("Tx hash: " + strTxHash); 111 | log.debug("Tx hex: " + hexTx + "\n"); 112 | } 113 | return tx; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/cli/CliController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.cli; 2 | 3 | import com.samourai.whirlpool.cli.Application; 4 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 5 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 6 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliInitRequest; 7 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliInitResponse; 8 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliLoginRequest; 9 | import com.samourai.whirlpool.cli.api.protocol.rest.ApiCliStateResponse; 10 | import com.samourai.whirlpool.cli.beans.CliStatus; 11 | import com.samourai.whirlpool.cli.beans.WhirlpoolPairingPayload; 12 | import com.samourai.whirlpool.cli.config.CliConfig; 13 | import com.samourai.whirlpool.cli.services.CliConfigService; 14 | import com.samourai.whirlpool.cli.services.CliWalletService; 15 | import com.samourai.whirlpool.client.exception.NotifiableException; 16 | import java.lang.invoke.MethodHandles; 17 | import javax.validation.Valid; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.core.task.TaskExecutor; 22 | import org.springframework.http.HttpHeaders; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestHeader; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.RequestMethod; 27 | import org.springframework.web.bind.annotation.RestController; 28 | 29 | @RestController 30 | public class CliController extends AbstractRestController { 31 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 32 | 33 | @Autowired private CliConfigService cliConfigService; 34 | @Autowired private CliWalletService cliWalletService; 35 | @Autowired private CliConfig cliConfig; 36 | @Autowired private TaskExecutor taskExecutor; 37 | 38 | @RequestMapping(value = CliApiEndpoint.REST_CLI, method = RequestMethod.GET) 39 | public ApiCliStateResponse state(@RequestHeader HttpHeaders headers) throws Exception { 40 | checkHeaders(headers); 41 | ApiCliStateResponse response = 42 | new ApiCliStateResponse( 43 | cliWalletService.getCliState(), 44 | cliConfig.getServer(), 45 | cliConfig.computeServerUrl(), 46 | cliConfig.getDojo().getUrl(), 47 | cliConfig.getTor(), 48 | cliConfig.isDojoEnabled()); 49 | return response; 50 | } 51 | 52 | @RequestMapping(value = CliApiEndpoint.REST_CLI_INIT, method = RequestMethod.POST) 53 | public ApiCliInitResponse init( 54 | @RequestHeader HttpHeaders headers, @Valid @RequestBody ApiCliInitRequest payload) 55 | throws Exception { 56 | checkHeaders(headers); 57 | 58 | // security: check not already initialized 59 | if (!CliStatus.NOT_INITIALIZED.equals(cliConfigService.getCliStatus())) { 60 | throw new NotifiableException("CLI is already initialized."); 61 | } 62 | 63 | // init 64 | String pairingPayload = payload.pairingPayload; 65 | boolean tor = payload.tor; 66 | boolean dojo = payload.dojo; 67 | WhirlpoolPairingPayload pairing = cliConfigService.parsePairingPayload(pairingPayload); 68 | String apiKey = cliConfigService.initialize(pairing, tor, dojo); 69 | 70 | ApiCliInitResponse response = new ApiCliInitResponse(apiKey); 71 | 72 | // restart CLI *AFTER* response reply 73 | taskExecutor.execute( 74 | new Runnable() { 75 | @Override 76 | public void run() { 77 | try { 78 | Thread.sleep(1000); 79 | } catch (Exception e) { 80 | log.error("", e); 81 | } 82 | Application.restart(); 83 | } 84 | }); 85 | return response; 86 | } 87 | 88 | @RequestMapping(value = CliApiEndpoint.REST_CLI_LOGIN, method = RequestMethod.POST) 89 | public ApiCliStateResponse login( 90 | @RequestHeader HttpHeaders headers, @Valid @RequestBody ApiCliLoginRequest payload) 91 | throws Exception { 92 | checkHeaders(headers); 93 | 94 | cliWalletService.openWallet(payload.seedPassphrase).start(); 95 | 96 | // success 97 | return state(headers); 98 | } 99 | 100 | @RequestMapping(value = CliApiEndpoint.REST_CLI_LOGOUT, method = RequestMethod.POST) 101 | public ApiCliStateResponse logout(@RequestHeader HttpHeaders headers) throws Exception { 102 | checkHeaders(headers); 103 | 104 | cliWalletService.closeWallet(); 105 | 106 | // success 107 | return state(headers); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/config/CliConfig.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.config; 2 | 3 | import com.samourai.http.client.HttpUsage; 4 | import com.samourai.http.client.IHttpClientService; 5 | import com.samourai.stomp.client.IStompClientService; 6 | import com.samourai.wallet.api.backend.BackendApi; 7 | import com.samourai.wallet.api.backend.BackendServer; 8 | import com.samourai.wallet.util.FormatsUtilGeneric; 9 | import com.samourai.whirlpool.client.utils.ClientUtils; 10 | import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; 11 | import com.samourai.whirlpool.client.wallet.persist.WhirlpoolWalletPersistHandler; 12 | import java.util.Collection; 13 | import java.util.LinkedList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.util.StringUtils; 18 | 19 | @Service 20 | public class CliConfig extends CliConfigFile { 21 | private boolean autoAggregatePostmix; 22 | private String autoTx0PoolId; 23 | 24 | public CliConfig() { 25 | super(); 26 | } 27 | 28 | @Override 29 | public WhirlpoolWalletConfig computeWhirlpoolWalletConfig( 30 | IHttpClientService httpClientService, 31 | IStompClientService stompClientService, 32 | WhirlpoolWalletPersistHandler persistHandler, 33 | BackendApi backendApi) { 34 | 35 | // check valid 36 | if (autoAggregatePostmix && StringUtils.isEmpty(autoTx0PoolId)) { 37 | throw new RuntimeException("--auto-tx0 is required for --auto-aggregate-postmix"); 38 | } 39 | 40 | WhirlpoolWalletConfig config = 41 | super.computeWhirlpoolWalletConfig( 42 | httpClientService, stompClientService, persistHandler, backendApi); 43 | config.setAutoTx0PoolId(autoTx0PoolId); 44 | return config; 45 | } 46 | 47 | public boolean isAutoAggregatePostmix() { 48 | return autoAggregatePostmix; 49 | } 50 | 51 | public void setAutoAggregatePostmix(boolean autoAggregatePostmix) { 52 | this.autoAggregatePostmix = autoAggregatePostmix; 53 | } 54 | 55 | public String getAutoTx0PoolId() { 56 | return autoTx0PoolId; 57 | } 58 | 59 | public void setAutoTx0PoolId(String autoTx0PoolId) { 60 | this.autoTx0PoolId = autoTx0PoolId; 61 | } 62 | 63 | @Override 64 | public Map getConfigInfo() { 65 | Map configInfo = super.getConfigInfo(); 66 | 67 | configInfo.put("cli/version", Integer.toString(getVersion())); 68 | configInfo.put("cli/tor", Boolean.toString(getTor())); 69 | 70 | String apiKey = getApiKey(); 71 | configInfo.put( 72 | "cli/apiKey", 73 | !org.apache.commons.lang3.StringUtils.isEmpty(apiKey) 74 | ? ClientUtils.maskString(apiKey) 75 | : "null"); 76 | configInfo.put( 77 | "cli/proxy", getCliProxy().isPresent() ? getCliProxy().get().toString() : "null"); 78 | configInfo.put("cli/autoAggregatePostmix", Boolean.toString(autoAggregatePostmix)); 79 | configInfo.put("cli/autoTx0PoolId", autoTx0PoolId != null ? autoTx0PoolId : "null"); 80 | return configInfo; 81 | } 82 | 83 | // 84 | 85 | public String computeBackendUrl() { 86 | if (getDojo().isEnabled()) { 87 | // use dojo 88 | return getDojo().getUrl(); 89 | } 90 | // use Samourai backend 91 | return computeBackendUrlSamourai(); 92 | } 93 | 94 | public boolean isDojoEnabled() { 95 | return getDojo() != null && getDojo().isEnabled(); 96 | } 97 | 98 | public String computeBackendApiKey() { 99 | if (isDojoEnabled()) { 100 | // dojo: use apiKey 101 | return getDojo().getApiKey(); 102 | } 103 | // Samourai backend: no apiKey 104 | return null; 105 | } 106 | 107 | private String computeBackendUrlSamourai() { 108 | boolean isTestnet = FormatsUtilGeneric.getInstance().isTestNet(getServer().getParams()); 109 | BackendServer backendServer = BackendServer.get(isTestnet); 110 | boolean useOnion = 111 | getTor() 112 | && getTorConfig().getBackend().isEnabled() 113 | && getTorConfig().getBackend().isOnion(); 114 | String backendUrl = backendServer.getBackendUrl(useOnion); 115 | return backendUrl; 116 | } 117 | 118 | public Collection computeTorHttpUsages() { 119 | List httpUsages = new LinkedList<>(); 120 | if (!getTor()) { 121 | // tor is disabled 122 | return httpUsages; 123 | } 124 | 125 | // backend 126 | if (getTorConfig().getBackend().isEnabled()) { 127 | httpUsages.add(HttpUsage.BACKEND); 128 | } 129 | 130 | // coordinator 131 | if (getTorConfig().getCoordinator().isEnabled()) { 132 | httpUsages.add(HttpUsage.COORDINATOR_WEBSOCKET); 133 | httpUsages.add(HttpUsage.COORDINATOR_REST); 134 | httpUsages.add(HttpUsage.COORDINATOR_REGISTER_OUTPUT); 135 | } 136 | return httpUsages; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/http/client/JavaHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.samourai.http.client; 2 | 3 | import com.samourai.wallet.api.backend.beans.HttpException; 4 | import com.samourai.whirlpool.client.utils.ClientUtils; 5 | import java.lang.invoke.MethodHandles; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Map; 8 | import java.util.concurrent.TimeUnit; 9 | import org.eclipse.jetty.client.HttpClient; 10 | import org.eclipse.jetty.client.api.ContentResponse; 11 | import org.eclipse.jetty.client.api.Request; 12 | import org.eclipse.jetty.client.util.FormContentProvider; 13 | import org.eclipse.jetty.client.util.StringContentProvider; 14 | import org.eclipse.jetty.http.HttpMethod; 15 | import org.eclipse.jetty.http.HttpStatus; 16 | import org.eclipse.jetty.util.Fields; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.http.MediaType; 20 | 21 | public class JavaHttpClient extends JacksonHttpClient { 22 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 23 | 24 | private HttpUsage httpUsage; 25 | private HttpClient httpClient; 26 | private long requestTimeout; 27 | 28 | public JavaHttpClient(HttpUsage httpUsage, HttpClient httpClient, long requestTimeout) { 29 | super(); 30 | log = ClientUtils.prefixLogger(log, httpUsage.name()); 31 | this.httpUsage = httpUsage; 32 | this.httpClient = httpClient; 33 | this.requestTimeout = requestTimeout; 34 | } 35 | 36 | @Override 37 | public void connect() throws Exception { 38 | if (!httpClient.isRunning()) { 39 | httpClient.start(); 40 | } 41 | } 42 | 43 | public void restart() { 44 | try { 45 | if (log.isDebugEnabled()) { 46 | log.debug("restart"); 47 | } 48 | if (httpClient.isRunning()) { 49 | httpClient.stop(); 50 | } 51 | httpClient.start(); 52 | } catch (Exception e) { 53 | log.error("", e); 54 | } 55 | } 56 | 57 | @Override 58 | protected String requestJsonGet(String urlStr, Map headers) throws Exception { 59 | Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers); 60 | return requestJson(req); 61 | } 62 | 63 | @Override 64 | protected String requestJsonPost(String urlStr, Map headers, String jsonBody) 65 | throws Exception { 66 | Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers); 67 | req.content( 68 | new StringContentProvider( 69 | MediaType.APPLICATION_JSON_VALUE, jsonBody, StandardCharsets.UTF_8)); 70 | return requestJson(req); 71 | } 72 | 73 | @Override 74 | protected String requestJsonPostUrlEncoded( 75 | String urlStr, Map headers, Map body) throws Exception { 76 | Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers); 77 | req.content(new FormContentProvider(computeBodyFields(body))); 78 | return requestJson(req); 79 | } 80 | 81 | private Fields computeBodyFields(Map body) { 82 | Fields fields = new Fields(); 83 | for (Map.Entry entry : body.entrySet()) { 84 | fields.put(entry.getKey(), entry.getValue()); 85 | } 86 | return fields; 87 | } 88 | 89 | private String requestJson(Request req) throws Exception { 90 | ContentResponse response = req.send(); 91 | if (response.getStatus() != HttpStatus.OK_200) { 92 | String responseBody = response.getContentAsString(); 93 | log.error( 94 | "Http query failed: status=" + response.getStatus() + ", responseBody=" + responseBody); 95 | throw new HttpException( 96 | new Exception("Http query failed: status=" + response.getStatus()), responseBody); 97 | } 98 | String responseContent = response.getContentAsString(); 99 | return responseContent; 100 | } 101 | 102 | public HttpClient getJettyHttpClient() throws Exception { 103 | connect(); 104 | return httpClient; 105 | } 106 | 107 | private Request computeHttpRequest(String url, HttpMethod method, Map headers) 108 | throws Exception { 109 | if (log.isDebugEnabled()) { 110 | String headersStr = headers != null ? " (" + headers.keySet() + ")" : ""; 111 | log.debug("+" + method + ": " + url + headersStr); 112 | } 113 | Request req = getJettyHttpClient().newRequest(url); 114 | req.method(method); 115 | if (headers != null) { 116 | for (Map.Entry entry : headers.entrySet()) { 117 | req.header(entry.getKey(), entry.getValue()); 118 | } 119 | } 120 | req.timeout(requestTimeout, TimeUnit.MILLISECONDS); 121 | return req; 122 | } 123 | 124 | @Override 125 | protected void onRequestError(Exception e) { 126 | super.onRequestError(e); 127 | restart(); 128 | } 129 | 130 | public HttpUsage getHttpUsage() { 131 | return httpUsage; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/run/CliStatusInteractiveOrchestrator.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.run; 2 | 3 | import com.samourai.whirlpool.cli.config.CliConfig; 4 | import com.samourai.whirlpool.cli.exception.NoSessionWalletException; 5 | import com.samourai.whirlpool.cli.services.CliWalletService; 6 | import com.samourai.whirlpool.cli.utils.CliUtils; 7 | import com.samourai.whirlpool.client.utils.ClientUtils; 8 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 9 | import com.samourai.whirlpool.client.wallet.beans.MixingState; 10 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; 11 | import com.samourai.whirlpool.client.wallet.orchestrator.AbstractOrchestrator; 12 | import java.util.Collection; 13 | import java.util.Comparator; 14 | import java.util.stream.Collectors; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | public class CliStatusInteractiveOrchestrator extends AbstractOrchestrator { 20 | private static final Logger log = LoggerFactory.getLogger(CliStatusInteractiveOrchestrator.class); 21 | 22 | private CliWalletService cliWalletService; 23 | private CliConfig cliConfig; 24 | 25 | public CliStatusInteractiveOrchestrator( 26 | int loopDelay, CliWalletService cliWalletService, CliConfig cliConfig) { 27 | super(loopDelay); 28 | this.cliWalletService = cliWalletService; 29 | this.cliConfig = cliConfig; 30 | } 31 | 32 | @Override 33 | protected void runOrchestrator() { 34 | interactive(); 35 | } 36 | 37 | private void interactive() { 38 | while (isStarted()) { 39 | try { 40 | Character car = CliUtils.readChar(); 41 | if (car != null) { 42 | if (car.equals('T')) { 43 | printThreads(); 44 | } else if (car.equals('D')) { 45 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 46 | printUtxos("DEPOSIT", whirlpoolWallet.getUtxosDeposit()); 47 | } else if (car.equals('P')) { 48 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 49 | printUtxos("PREMIX", whirlpoolWallet.getUtxosPremix()); 50 | } else if (car.equals('O')) { 51 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 52 | printUtxos("POSTMIX", whirlpoolWallet.getUtxosPostmix()); 53 | } else if (car.equals('S')) { 54 | printSystem(); 55 | } 56 | } else { 57 | if (log.isDebugEnabled()) { 58 | log.debug("console input was null"); 59 | } 60 | } 61 | } catch (Exception e) { 62 | log.error("", e); 63 | } 64 | } 65 | } 66 | 67 | private void printThreads() { 68 | try { 69 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 70 | MixingState mixingState = whirlpoolWallet.getMixingState(); 71 | log.info("⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿"); 72 | log.info("⣿ MIXING THREADS:"); 73 | int i = 0; 74 | for (WhirlpoolUtxo whirlpoolUtxo : mixingState.getUtxosMixing()) { 75 | log.info( 76 | "⣿ Thread #" 77 | + (i + 1) 78 | + ": MIXING " 79 | + whirlpoolUtxo.toString() 80 | + " ; " 81 | + whirlpoolUtxo.getUtxoConfig()); 82 | i++; 83 | } 84 | } catch (NoSessionWalletException e) { 85 | System.out.print("⣿ Wallet CLOSED\r"); 86 | } catch (Exception e) { 87 | log.error("", e); 88 | } 89 | } 90 | 91 | private void printSystem() { 92 | ThreadGroup tg = Thread.currentThread().getThreadGroup(); 93 | Collection threadSet = 94 | Thread.getAllStackTraces() 95 | .keySet() 96 | .stream() 97 | .filter(t -> t.getThreadGroup() == tg) 98 | .sorted(Comparator.comparing(o -> o.getName().toLowerCase())) 99 | .collect(Collectors.toList()); 100 | log.info("⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿"); 101 | log.info("⣿ SYSTEM THREADS:"); 102 | int i = 0; 103 | for (Thread t : threadSet) { 104 | log.info("#" + i + " " + t + ":" + "" + t.getState()); 105 | // show trace for BLOCKED 106 | if (Thread.State.BLOCKED.equals(t.getState())) { 107 | log.info(StringUtils.join(t.getStackTrace(), "\n")); 108 | } 109 | i++; 110 | } 111 | 112 | // memory 113 | Runtime rt = Runtime.getRuntime(); 114 | long total = rt.totalMemory(); 115 | long free = rt.freeMemory(); 116 | long used = total - free; 117 | log.info("⣿ MEM USE: " + CliUtils.bytesToMB(used) + "M/" + CliUtils.bytesToMB(total) + "M"); 118 | } 119 | 120 | private void printUtxos(String account, Collection utxos) { 121 | try { 122 | log.info("⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿"); 123 | log.info("⣿ " + account + " UTXOS:"); 124 | ClientUtils.logWhirlpoolUtxos(utxos, cliConfig.getMix().getMixsTarget()); 125 | 126 | } catch (Exception e) { 127 | log.error("", e); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/tor/client/utils/WhirlpoolTorInstaller.java: -------------------------------------------------------------------------------- 1 | // TODO https://github.com/thaliproject/Tor_Onion_Proxy_Library/pull/127 2 | // 3 | // Source code recreated from a .class file by IntelliJ IDEA 4 | // (powered by Fernflower decompiler) 5 | // 6 | 7 | package com.samourai.tor.client.utils; 8 | 9 | import com.msopentech.thali.toronionproxy.FileUtilities; 10 | import com.msopentech.thali.toronionproxy.OsData; 11 | import com.msopentech.thali.toronionproxy.TorConfig; 12 | import com.msopentech.thali.toronionproxy.TorInstaller; 13 | import com.samourai.whirlpool.cli.utils.CliUtils; 14 | import java.io.*; 15 | import java.nio.file.Files; 16 | import java.util.Optional; 17 | import java.util.concurrent.TimeoutException; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | public final class WhirlpoolTorInstaller extends TorInstaller { 22 | private static final Logger LOG = LoggerFactory.getLogger(WhirlpoolTorInstaller.class); 23 | private final TorConfig config; 24 | private boolean useExecutableFromZip; 25 | 26 | public WhirlpoolTorInstaller(String torDir, Optional torExecutable, int fileCreationTimeout) 27 | throws Exception { 28 | this.config = computeTorConfig(torDir, torExecutable, fileCreationTimeout); 29 | this.useExecutableFromZip = !torExecutable.isPresent(); 30 | } 31 | 32 | private static TorConfig computeTorConfig( 33 | String dirName, Optional torExecutable, int fileCreationTimeout) throws Exception { 34 | File dir = Files.createTempDirectory(dirName).toFile(); 35 | dir.deleteOnExit(); 36 | 37 | TorConfig.Builder torConfigBuilder = new TorConfig.Builder(dir, dir).homeDir(dir); 38 | 39 | if (torExecutable.isPresent()) { 40 | if (LOG.isDebugEnabled()) { 41 | LOG.debug( 42 | "configuring tor for external executable: " + torExecutable.get().getAbsolutePath()); 43 | } 44 | // use existing local Tor instead of embedded one 45 | torConfigBuilder.torExecutable(torExecutable.get()); 46 | } 47 | torConfigBuilder.fileCreationTimeout(fileCreationTimeout); 48 | 49 | TorConfig torConfig = torConfigBuilder.build(); 50 | return torConfig; 51 | } 52 | 53 | private static String getPathToTorExecutable() { 54 | String path = "native/"; 55 | switch (OsData.getOsType()) { 56 | case WINDOWS: 57 | return path + "windows/x86/"; 58 | case MAC: 59 | return path + "osx/x64/"; 60 | case LINUX_32: 61 | return path + "linux/x86/"; 62 | case LINUX_64: 63 | return path + "linux/x64/"; 64 | default: 65 | throw new RuntimeException("We don't support Tor on this OS"); 66 | } 67 | } 68 | 69 | public void setup() throws IOException { 70 | LOG.info("Setting up tor"); 71 | LOG.info("Installing resources: geoip=" + this.config.getGeoIpFile().getAbsolutePath()); 72 | FileUtilities.cleanInstallOneFile( 73 | this.getAssetOrResourceByName("geoip"), this.config.getGeoIpFile()); 74 | FileUtilities.cleanInstallOneFile( 75 | this.getAssetOrResourceByName("geoip6"), this.config.getGeoIpv6File()); 76 | 77 | if (useExecutableFromZip) { 78 | setupTorExecutable(); 79 | } else { 80 | LOG.info( 81 | "Using existing tor executable: " + this.config.getTorExecutableFile().getAbsolutePath()); 82 | } 83 | } 84 | 85 | protected void setupTorExecutable() throws IOException { 86 | LOG.info("Installing tor executable: " + this.config.getTorExecutableFile().getAbsolutePath()); 87 | File torParent = this.config.getTorExecutableFile().getParentFile(); 88 | FileUtilities.extractContentFromZip( 89 | torParent.exists() ? torParent : this.config.getTorExecutableFile(), 90 | this.getAssetOrResourceByName(getPathToTorExecutable() + "tor.zip")); 91 | FileUtilities.setPerms(this.config.getTorExecutableFile()); 92 | 93 | // detect runtime errors on tor executable (ie "error while loading shared libraries...") 94 | try { 95 | CliUtils.exec(this.config.getTorExecutableFile().getAbsolutePath() + " --help"); 96 | } catch (Exception e) { 97 | throw new IOException("Tor executable error: " + e.getMessage()); 98 | } 99 | } 100 | 101 | public void updateTorConfigCustom(String content) throws IOException, TimeoutException { 102 | PrintWriter printWriter = null; 103 | 104 | try { 105 | LOG.info("Updating torrc file; torrc =" + this.config.getTorrcFile().getAbsolutePath()); 106 | printWriter = 107 | new PrintWriter(new BufferedWriter(new FileWriter(this.config.getTorrcFile(), true))); 108 | printWriter.println( 109 | "PidFile " + (new File(this.config.getDataDir(), "pid")).getAbsolutePath()); 110 | printWriter.print(content); 111 | } finally { 112 | if (printWriter != null) { 113 | printWriter.close(); 114 | } 115 | } 116 | } 117 | 118 | public InputStream openBridgesStream() throws IOException { 119 | throw new UnsupportedOperationException(); 120 | } 121 | 122 | public TorConfig getConfig() { 123 | return config; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/com/samourai/whirlpool/cli/beans/WhirlpoolPairingPayloadTest.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.beans; 2 | 3 | import com.samourai.wallet.api.pairing.PairingNetwork; 4 | import com.samourai.wallet.api.pairing.PairingVersion; 5 | import com.samourai.whirlpool.client.exception.NotifiableException; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | public class WhirlpoolPairingPayloadTest { 10 | 11 | @Test 12 | public void parse_valid() throws Exception { 13 | String payload; 14 | 15 | // valid 16 | payload = 17 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"1.0.0\",\"network\":\"testnet\",\"mnemonic\":\"0qTfDpexBYZ7GM0/F1xCnXctAKLPNcd8+U+GYWNDq7jHxGtsbcfwSeHI0BoVMSm7KrdIgBiKhyUl0XCntfq9drU6nOrtmqo2x1dppnvrLjNI71go2ICospLOtRHiFUac\"}}"; 18 | parse( 19 | payload, 20 | PairingVersion.V1_0_0, 21 | PairingNetwork.TESTNET, 22 | "0qTfDpexBYZ7GM0/F1xCnXctAKLPNcd8+U+GYWNDq7jHxGtsbcfwSeHI0BoVMSm7KrdIgBiKhyUl0XCntfq9drU6nOrtmqo2x1dppnvrLjNI71go2ICospLOtRHiFUac", 23 | true); // passphrase=true for V1 24 | 25 | // valid 26 | payload = 27 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"1.0.0\",\"network\":\"testnet\",\"mnemonic\":\"BcXzXesuQjLKTgS54rzPBdyJ43IgWdvEXwNJo/ZE49hq8U5eLp/ge+4XQibNAeJS+Eng7AY19hiAIoR3vsTdsyCzGdfR0ZBjML4gpoebFT2LD0+eMrbKo/1dZueYHq4j\"}}"; 28 | parse( 29 | payload, 30 | PairingVersion.V1_0_0, 31 | PairingNetwork.TESTNET, 32 | "BcXzXesuQjLKTgS54rzPBdyJ43IgWdvEXwNJo/ZE49hq8U5eLp/ge+4XQibNAeJS+Eng7AY19hiAIoR3vsTdsyCzGdfR0ZBjML4gpoebFT2LD0+eMrbKo/1dZueYHq4j", 33 | true); // passphrase=true for V1 34 | 35 | // valid 36 | payload = 37 | "{\"pairing\": {\"type\": \"whirlpool.gui\",\"version\": \"1.0.0\",\"network\": \"mainnet\",\"mnemonic\": \"rV2e6YUj33akmh6+k32mjVEE0Amm8XrLRDe4Qvi1WZ1PWAWXHxpuaHwbbXZzzzIlwLnLMNJ8fxtMQMAGR77xew==\"}}"; 38 | parse( 39 | payload, 40 | PairingVersion.V1_0_0, 41 | PairingNetwork.MAINNET, 42 | "rV2e6YUj33akmh6+k32mjVEE0Amm8XrLRDe4Qvi1WZ1PWAWXHxpuaHwbbXZzzzIlwLnLMNJ8fxtMQMAGR77xew==", 43 | true); // passphrase=true for V1 44 | 45 | // valid V2 46 | payload = 47 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"2.0.0\",\"network\":\"testnet\",\"mnemonic\":\"O/fIt9AvelDmz3lVLTzdkvjUtO1MZ1knFPSyPfPNgwMDviVzjAKZSE4mIBvaPazs8sJHZxkyJu09mEgOC4n95TXHCMYWTx3R3MsLfki4WHi77jhZhPDScDExGI9uLlNj\",\"passphrase\":true}}"; 48 | parse( 49 | payload, 50 | PairingVersion.V2_0_0, 51 | PairingNetwork.TESTNET, 52 | "O/fIt9AvelDmz3lVLTzdkvjUtO1MZ1knFPSyPfPNgwMDviVzjAKZSE4mIBvaPazs8sJHZxkyJu09mEgOC4n95TXHCMYWTx3R3MsLfki4WHi77jhZhPDScDExGI9uLlNj", 53 | true); 54 | 55 | // valid V2 no passphrase 56 | payload = 57 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"2.0.0\",\"network\":\"testnet\",\"mnemonic\":\"ih6Jz5eNNdJLdVLTK0W4w23qhr/sT1DUhH46k2nI7j0vp+PKK5LDjYFFY8+SC5Phm9tTBQ47UqFxYvlDElXR0Q==\",\"passphrase\":false}}"; 58 | parse( 59 | payload, 60 | PairingVersion.V2_0_0, 61 | PairingNetwork.TESTNET, 62 | "ih6Jz5eNNdJLdVLTK0W4w23qhr/sT1DUhH46k2nI7j0vp+PKK5LDjYFFY8+SC5Phm9tTBQ47UqFxYvlDElXR0Q==", 63 | false); 64 | } 65 | 66 | @Test 67 | public void parse_invalid() throws Exception { 68 | // missing 'pairing' 69 | try { 70 | String payload = 71 | "{\"wrong\":{\"type\":\"whirlpool.gui\",\"version\":\"1.0.0\",\"network\":\"testnet\",\"mnemonic\":\"foo\"}}"; 72 | parse(payload, PairingVersion.V1_0_0, PairingNetwork.TESTNET, "foo", null); 73 | Assert.assertTrue(false); 74 | } catch (NotifiableException e) { 75 | // ok 76 | } 77 | 78 | // invalid type 79 | try { 80 | String payload = 81 | "{\"pairing\":{\"type\":\"foo\",\"version\":\"1.0.0\",\"network\":\"testnet\",\"mnemonic\":\"foo\"}}"; 82 | parse(payload, PairingVersion.V1_0_0, PairingNetwork.TESTNET, "foo", null); 83 | Assert.assertTrue(false); 84 | } catch (NotifiableException e) { 85 | // ok 86 | } 87 | 88 | // invalid version 89 | try { 90 | String payload = 91 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"0.0.0\",\"network\":\"testnet\",\"mnemonic\":\"foo\"}}"; 92 | parse(payload, PairingVersion.V1_0_0, PairingNetwork.TESTNET, "foo", null); 93 | Assert.assertTrue(false); 94 | } catch (NotifiableException e) { 95 | // ok 96 | } 97 | 98 | // invalid network 99 | try { 100 | String payload = 101 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"1.0.0\",\"network\":\"wrong\",\"mnemonic\":\"foo\"}}"; 102 | parse(payload, PairingVersion.V1_0_0, PairingNetwork.TESTNET, "foo", null); 103 | Assert.assertTrue(false); 104 | } catch (NotifiableException e) { 105 | // ok 106 | } 107 | 108 | // invalid mnemonic 109 | try { 110 | String payload = 111 | "{\"pairing\":{\"type\":\"whirlpool.gui\",\"version\":\"1.0.0\",\"network\":\"testnet\",\"mnemonic\":\"\"}}"; 112 | parse(payload, PairingVersion.V1_0_0, PairingNetwork.TESTNET, "foo", null); 113 | Assert.assertTrue(false); 114 | } catch (NotifiableException e) { 115 | // ok 116 | } 117 | } 118 | 119 | private void parse( 120 | String payload, 121 | PairingVersion pairingVersion, 122 | PairingNetwork pairingNetwork, 123 | String mnemonic, 124 | Boolean passphrase) 125 | throws Exception { 126 | WhirlpoolPairingPayload pairingPayload = WhirlpoolPairingPayload.parse(payload); 127 | Assert.assertEquals(pairingNetwork, pairingPayload.getPairing().getNetwork()); 128 | Assert.assertEquals(pairingVersion, pairingPayload.getPairing().getVersion()); 129 | Assert.assertEquals(mnemonic, pairingPayload.getPairing().getMnemonic()); 130 | Assert.assertEquals(passphrase, pairingPayload.getPairing().getPassphrase()); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/controllers/utxo/UtxoController.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.controllers.utxo; 2 | 3 | import com.samourai.whirlpool.cli.api.controllers.AbstractRestController; 4 | import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint; 5 | import com.samourai.whirlpool.cli.api.protocol.beans.ApiUtxo; 6 | import com.samourai.whirlpool.cli.api.protocol.rest.*; 7 | import com.samourai.whirlpool.cli.services.CliWalletService; 8 | import com.samourai.whirlpool.client.exception.NotifiableException; 9 | import com.samourai.whirlpool.client.tx0.Tx0; 10 | import com.samourai.whirlpool.client.tx0.Tx0Config; 11 | import com.samourai.whirlpool.client.tx0.Tx0Preview; 12 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 13 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; 14 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 15 | import java8.util.Lists; 16 | import javax.validation.Valid; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.http.HttpHeaders; 19 | import org.springframework.web.bind.annotation.*; 20 | 21 | @RestController 22 | public class UtxoController extends AbstractRestController { 23 | @Autowired private CliWalletService cliWalletService; 24 | 25 | private WhirlpoolUtxo findUtxo(String utxoHash, int utxoIndex) throws Exception { 26 | // find utxo 27 | WhirlpoolUtxo whirlpoolUtxo = cliWalletService.getSessionWallet().findUtxo(utxoHash, utxoIndex); 28 | if (whirlpoolUtxo == null) { 29 | throw new NotifiableException("Utxo not found: " + utxoHash + ":" + utxoIndex); 30 | } 31 | return whirlpoolUtxo; 32 | } 33 | 34 | @RequestMapping(value = CliApiEndpoint.REST_UTXO_CONFIGURE, method = RequestMethod.POST) 35 | public ApiUtxo configureUtxo( 36 | @RequestHeader HttpHeaders headers, 37 | @PathVariable("hash") String utxoHash, 38 | @PathVariable("index") int utxoIndex, 39 | @Valid @RequestBody ApiUtxoConfigureRequest payload) 40 | throws Exception { 41 | checkHeaders(headers); 42 | 43 | // find utxo 44 | WhirlpoolUtxo whirlpoolUtxo = findUtxo(utxoHash, utxoIndex); 45 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 46 | 47 | // configure pool 48 | whirlpoolWallet.setPool(whirlpoolUtxo, payload.poolId); 49 | 50 | // configure mixsTarget 51 | whirlpoolWallet.setMixsTarget(whirlpoolUtxo, payload.mixsTarget); 52 | 53 | int mixsTargetMin = whirlpoolWallet.getConfig().getMixsTarget(); 54 | ApiUtxo apiUtxo = new ApiUtxo(whirlpoolUtxo, mixsTargetMin); 55 | return apiUtxo; 56 | } 57 | 58 | @RequestMapping(value = CliApiEndpoint.REST_UTXO_TX0_PREVIEW, method = RequestMethod.POST) 59 | public ApiTx0PreviewResponse tx0Preview( 60 | @RequestHeader HttpHeaders headers, 61 | @PathVariable("hash") String utxoHash, 62 | @PathVariable("index") int utxoIndex, 63 | @Valid @RequestBody ApiTx0PreviewRequest payload) 64 | throws Exception { 65 | checkHeaders(headers); 66 | 67 | // find utxo 68 | WhirlpoolUtxo whirlpoolUtxo = findUtxo(utxoHash, utxoIndex); 69 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 70 | 71 | Pool pool = whirlpoolWallet.findPoolById(payload.poolId); 72 | if (pool == null) { 73 | throw new NotifiableException("poolId is not valid"); 74 | } 75 | 76 | // tx0 preview 77 | Tx0Config tx0Config = whirlpoolWallet.getTx0Config(pool); 78 | Tx0Preview tx0Preview = 79 | whirlpoolWallet.tx0Preview(Lists.of(whirlpoolUtxo), pool, tx0Config, payload.feeTarget); 80 | return new ApiTx0PreviewResponse(tx0Preview); 81 | } 82 | 83 | @RequestMapping(value = CliApiEndpoint.REST_UTXO_TX0, method = RequestMethod.POST) 84 | public ApiTx0Response tx0( 85 | @RequestHeader HttpHeaders headers, 86 | @PathVariable("hash") String utxoHash, 87 | @PathVariable("index") int utxoIndex, 88 | @Valid @RequestBody ApiTx0Request payload) 89 | throws Exception { 90 | checkHeaders(headers); 91 | 92 | // find utxo 93 | WhirlpoolUtxo whirlpoolUtxo = findUtxo(utxoHash, utxoIndex); 94 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 95 | 96 | // override utxo settings 97 | if (payload.mixsTarget != null && payload.mixsTarget > 0) { 98 | whirlpoolWallet.setMixsTarget(whirlpoolUtxo, payload.mixsTarget); 99 | } 100 | 101 | Pool pool = whirlpoolWallet.findPoolById(payload.poolId); 102 | if (pool == null) { 103 | throw new NotifiableException("poolId is not valid"); 104 | } 105 | 106 | // tx0 107 | Tx0Config tx0Config = whirlpoolWallet.getTx0Config(pool); 108 | Tx0 tx0 = whirlpoolWallet.tx0(Lists.of(whirlpoolUtxo), pool, payload.feeTarget, tx0Config); 109 | return new ApiTx0Response(tx0); 110 | } 111 | 112 | @RequestMapping(value = CliApiEndpoint.REST_UTXO_STARTMIX, method = RequestMethod.POST) 113 | public void startMix( 114 | @RequestHeader HttpHeaders headers, 115 | @PathVariable("hash") String utxoHash, 116 | @PathVariable("index") int utxoIndex) 117 | throws Exception { 118 | checkHeaders(headers); 119 | 120 | // find utxo 121 | WhirlpoolUtxo whirlpoolUtxo = findUtxo(utxoHash, utxoIndex); 122 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 123 | 124 | // start mix 125 | whirlpoolWallet.mix(whirlpoolUtxo); 126 | } 127 | 128 | @RequestMapping(value = CliApiEndpoint.REST_UTXO_STOPMIX, method = RequestMethod.POST) 129 | public void stopMix( 130 | @RequestHeader HttpHeaders headers, 131 | @PathVariable("hash") String utxoHash, 132 | @PathVariable("index") int utxoIndex) 133 | throws Exception { 134 | checkHeaders(headers); 135 | 136 | // find utxo 137 | WhirlpoolUtxo whirlpoolUtxo = findUtxo(utxoHash, utxoIndex); 138 | WhirlpoolWallet whirlpoolWallet = cliWalletService.getSessionWallet(); 139 | 140 | // stop mix 141 | whirlpoolWallet.mixStop(whirlpoolUtxo); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/utils/EncryptUtils.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.utils; 2 | 3 | import com.samourai.whirlpool.cli.beans.Encrypted; 4 | import java.lang.invoke.MethodHandles; 5 | import java.security.SecureRandom; 6 | import java.security.spec.KeySpec; 7 | import java.util.Arrays; 8 | import javax.crypto.Cipher; 9 | import javax.crypto.SecretKey; 10 | import javax.crypto.SecretKeyFactory; 11 | import javax.crypto.spec.GCMParameterSpec; 12 | import javax.crypto.spec.PBEKeySpec; 13 | import javax.crypto.spec.SecretKeySpec; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | public class EncryptUtils { 18 | private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 19 | 20 | private static final int CHECK_IV_LENGTH = 16; 21 | private static final int CHECK_SALT_LENGTH = 8; 22 | 23 | private static final String CRYPT_SF_ALGORITHM = "PBKDF2WithHmacSHA256"; 24 | private static final String CRYPT_ALGORITHM = "AES"; 25 | private static final String CRYPT_CIPHER = "AES/GCM/NoPadding"; 26 | private static final int CRYPT_KEY_LENGTH = 256; 27 | private static final int CRYPT_TAG_LENGTH = 128; 28 | private static final int CRYPT_ITERATIONS = 10000; 29 | 30 | private static final SecureRandom secureRandom = new SecureRandom(); 31 | 32 | public static Encrypted encrypt(String key, String plain) throws Exception { 33 | Encrypted encrypted = 34 | encrypt( 35 | key.toCharArray(), 36 | plain.getBytes("UTF-8"), 37 | CRYPT_ITERATIONS, 38 | CRYPT_KEY_LENGTH, 39 | CRYPT_TAG_LENGTH, 40 | CHECK_IV_LENGTH, 41 | CHECK_SALT_LENGTH); 42 | return encrypted; 43 | } 44 | 45 | public static String decrypt(String key, String encryptedSerialized) throws Exception { 46 | Encrypted encrypted = unserializeEncrypted(encryptedSerialized); 47 | return decrypt(key, encrypted); 48 | } 49 | 50 | public static String decrypt(String key, Encrypted encrypted) throws Exception { 51 | String plaintext = 52 | decrypt( 53 | key.toCharArray(), 54 | encrypted.getIv(), 55 | encrypted.getSalt(), 56 | encrypted.getCt(), 57 | CRYPT_ITERATIONS, 58 | CRYPT_KEY_LENGTH, 59 | CRYPT_TAG_LENGTH); 60 | return plaintext; 61 | } 62 | 63 | public static Encrypted encrypt( 64 | char[] key, 65 | byte[] plainBytes, 66 | int iterations, 67 | int keyLength, 68 | int tagLength, 69 | int ivLength, 70 | int saltLength) 71 | throws Exception { 72 | 73 | final byte[] ivBytes = new byte[ivLength]; 74 | secureRandom.nextBytes(ivBytes); 75 | 76 | final byte[] saltBytes = new byte[saltLength]; 77 | secureRandom.nextBytes(saltBytes); 78 | 79 | SecretKeyFactory factory = SecretKeyFactory.getInstance(CRYPT_SF_ALGORITHM); 80 | KeySpec spec = new PBEKeySpec(key, saltBytes, iterations, keyLength); 81 | SecretKey tmp = factory.generateSecret(spec); 82 | SecretKeySpec secret = new SecretKeySpec(tmp.getEncoded(), CRYPT_ALGORITHM); 83 | 84 | Cipher cipher = Cipher.getInstance(CRYPT_CIPHER); 85 | GCMParameterSpec ivSpec = new GCMParameterSpec(tagLength, ivBytes); 86 | cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec); 87 | 88 | byte[] ctBytes = cipher.doFinal(plainBytes); 89 | 90 | Encrypted encrypted = new Encrypted(ivBytes, saltBytes, ctBytes); 91 | return encrypted; 92 | } 93 | 94 | public static String decrypt( 95 | char[] key, 96 | byte[] ivBytes, 97 | byte[] saltBytes, 98 | byte[] ctBytes, 99 | int iterations, 100 | int keyLength, 101 | int tagLength) 102 | throws Exception { 103 | SecretKeyFactory factory = SecretKeyFactory.getInstance(CRYPT_SF_ALGORITHM); 104 | KeySpec spec = new PBEKeySpec(key, saltBytes, iterations, keyLength); 105 | SecretKey tmp = factory.generateSecret(spec); 106 | SecretKeySpec secret = new SecretKeySpec(tmp.getEncoded(), CRYPT_ALGORITHM); 107 | 108 | Cipher cipher = Cipher.getInstance(CRYPT_CIPHER); 109 | GCMParameterSpec ivSpec = new GCMParameterSpec(tagLength, ivBytes); 110 | cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec); 111 | 112 | String plaintext = new String(cipher.doFinal(ctBytes), "UTF-8"); 113 | return plaintext; 114 | } 115 | 116 | public static String serializeEncrypted(Encrypted encrypted) throws Exception { 117 | byte[] iv = encrypted.getIv(); 118 | byte[] salt = encrypted.getSalt(); 119 | byte[] ct = encrypted.getCt(); 120 | 121 | if (iv.length != CHECK_IV_LENGTH) { 122 | throw new Exception("Invalid IV length: " + iv.length + " vs " + CHECK_IV_LENGTH); 123 | } 124 | if (salt.length != CHECK_SALT_LENGTH) { 125 | throw new Exception("Invalid SALT length: " + salt.length + " vs " + CHECK_SALT_LENGTH); 126 | } 127 | 128 | byte[] concat = new byte[iv.length + salt.length + ct.length]; 129 | // concat iv 130 | System.arraycopy(iv, 0, concat, 0, iv.length); 131 | // concat salt 132 | System.arraycopy(salt, 0, concat, iv.length, salt.length); 133 | // concat ct 134 | System.arraycopy(ct, 0, concat, iv.length + salt.length, ct.length); 135 | 136 | // base64 encode 137 | return org.bouncycastle.util.encoders.Base64.toBase64String(concat); 138 | } 139 | 140 | protected static Encrypted unserializeEncrypted(String apiEncryptedSerialized) throws Exception { 141 | // base64 decode 142 | byte[] concat = org.bouncycastle.util.encoders.Base64.decode(apiEncryptedSerialized); 143 | 144 | // un-concat 145 | if (concat.length <= CHECK_IV_LENGTH + CHECK_SALT_LENGTH) { 146 | throw new Exception( 147 | "Invalid concat length: " 148 | + concat.length 149 | + " <= " 150 | + (CHECK_IV_LENGTH + CHECK_SALT_LENGTH)); 151 | } 152 | 153 | byte[] iv = Arrays.copyOfRange(concat, 0, CHECK_IV_LENGTH); 154 | byte[] salt = Arrays.copyOfRange(concat, CHECK_IV_LENGTH, CHECK_IV_LENGTH + CHECK_SALT_LENGTH); 155 | byte[] ct = Arrays.copyOfRange(concat, CHECK_IV_LENGTH + CHECK_SALT_LENGTH, concat.length); 156 | 157 | Encrypted encrypted = new Encrypted(iv, salt, ct); 158 | return encrypted; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/Application.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli; 2 | 3 | import com.samourai.whirlpool.cli.beans.CliResult; 4 | import com.samourai.whirlpool.cli.services.CliConfigService; 5 | import com.samourai.whirlpool.cli.services.CliService; 6 | import com.samourai.whirlpool.cli.utils.CliUtils; 7 | import com.samourai.whirlpool.client.exception.NotifiableException; 8 | import com.samourai.whirlpool.protocol.WhirlpoolProtocol; 9 | import java.lang.invoke.MethodHandles; 10 | import java.util.Arrays; 11 | import javax.annotation.PreDestroy; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.ApplicationArguments; 16 | import org.springframework.boot.ApplicationRunner; 17 | import org.springframework.boot.SpringApplication; 18 | import org.springframework.boot.WebApplicationType; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | import org.springframework.boot.builder.SpringApplicationBuilder; 21 | import org.springframework.boot.web.servlet.ServletComponentScan; 22 | import org.springframework.context.ConfigurableApplicationContext; 23 | import org.springframework.core.env.Environment; 24 | 25 | /** Main application. */ 26 | @SpringBootApplication 27 | @ServletComponentScan(value = "com.samourai.whirlpool.cli.config.filters") 28 | public class Application implements ApplicationRunner { 29 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 30 | 31 | private static boolean listen; 32 | private static boolean debug; 33 | private static boolean debugClient; 34 | private static ConfigurableApplicationContext applicationContext; 35 | private static ApplicationArguments applicationArguments; 36 | private static boolean restart; 37 | private static Integer exitCode; 38 | 39 | @Autowired Environment env; 40 | @Autowired CliService cliService; 41 | 42 | public static void main(String... args) { 43 | // override configuration with local file 44 | System.setProperty( 45 | "spring.config.location", 46 | "classpath:application.properties,./" + CliConfigService.CLI_CONFIG_FILENAME); 47 | 48 | // start REST api if --listen 49 | listen = ApplicationArgs.getMainListen(args); 50 | 51 | // enable debug logs with --debug 52 | debug = ApplicationArgs.isMainDebug(args); 53 | debugClient = ApplicationArgs.isMainDebugClient(args); 54 | CliUtils.setLogLevel(debug, debugClient); 55 | 56 | // run 57 | WebApplicationType wat = listen ? WebApplicationType.SERVLET : WebApplicationType.NONE; 58 | applicationContext = 59 | new SpringApplicationBuilder(Application.class) 60 | .logStartupInfo(debugClient) 61 | .web(wat) 62 | .run(args); 63 | 64 | if (restart) { 65 | // restart 66 | restart(); 67 | } else { 68 | // exit 69 | if (exitCode != null) { 70 | // error 71 | exitError(exitCode); 72 | } else { 73 | // success 74 | if (log.isDebugEnabled()) { 75 | log.debug("CLI startup complete."); 76 | } 77 | } 78 | } 79 | } 80 | 81 | @PreDestroy 82 | public void preDestroy() { 83 | cliService.shutdown(); 84 | } 85 | 86 | @Override 87 | public void run(ApplicationArguments applicationArguments) { 88 | restart = false; 89 | 90 | Application.applicationArguments = applicationArguments; 91 | CliUtils.setLogLevel(debug, debugClient); // run twice to fix incorrect log level 92 | 93 | if (log.isDebugEnabled()) { 94 | log.debug("Run... " + Arrays.toString(applicationArguments.getSourceArgs())); 95 | } 96 | if (log.isDebugEnabled()) { 97 | log.debug("[cli/debug] debug=" + debug + ", debugClient=" + debugClient); 98 | log.debug("[cli/protocolVersion] " + WhirlpoolProtocol.PROTOCOL_VERSION); 99 | log.debug("[cli/listen] " + listen); 100 | } 101 | 102 | try { 103 | // setup Tor etc... 104 | cliService.setup(); 105 | 106 | if (env.acceptsProfiles(CliUtils.SPRING_PROFILE_TESTING)) { 107 | log.info("Running unit test..."); 108 | return; 109 | } 110 | 111 | CliResult cliResult = cliService.run(listen); 112 | switch (cliResult) { 113 | case RESTART: 114 | restart = true; 115 | break; 116 | case EXIT_SUCCESS: 117 | exitCode = 0; 118 | break; 119 | case KEEP_RUNNING: 120 | break; 121 | } 122 | } catch (NotifiableException e) { 123 | exitCode = 1; 124 | CliUtils.notifyError(e.getMessage()); 125 | } catch (IllegalArgumentException e) { 126 | exitCode = 1; 127 | log.error("Invalid arguments: " + e.getMessage()); 128 | } catch (Exception e) { 129 | exitCode = 1; 130 | log.error("", e); 131 | } 132 | } 133 | 134 | public static void restart() { 135 | long restartDelay = 1000; 136 | if (log.isDebugEnabled()) { 137 | log.debug("Restarting CLI in " + restartDelay + "ms"); 138 | } 139 | 140 | // wait for restartDelay 141 | try { 142 | Thread.sleep(restartDelay); 143 | } catch (InterruptedException e) { 144 | } 145 | 146 | // restart application 147 | log.info("Restarting CLI..."); 148 | Thread thread = 149 | new Thread( 150 | () -> { 151 | if (applicationContext != null) { 152 | applicationContext.close(); 153 | } 154 | 155 | String[] restartArgs = computeRestartArgs(); 156 | main(restartArgs); 157 | }); 158 | thread.setDaemon(false); 159 | thread.start(); 160 | } 161 | 162 | public static void exitError(int exitCode) { 163 | if (log.isDebugEnabled()) { 164 | log.debug("Exit with error: " + exitCode); 165 | } 166 | if (applicationContext != null) { 167 | SpringApplication.exit(applicationContext, () -> exitCode); 168 | } 169 | System.exit(exitCode); 170 | } 171 | 172 | private static String[] computeRestartArgs() { 173 | return Arrays.stream(applicationArguments.getSourceArgs()) 174 | .filter(a -> !a.toLowerCase().equals("--" + ApplicationArgs.ARG_INIT)) 175 | .toArray(i -> new String[i]); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README-API.md: -------------------------------------------------------------------------------- 1 | # whirlpool-client-cli API 2 | 3 | 4 | ## Using REST API 5 | whirlpool-client-cli exposes a REST API over HTTPS when started with --listen. 6 | HTTPS port is defined in `whirlpool-cli-config.properties`: 7 | ``` 8 | cli.api.port=8899 9 | ``` 10 | 11 | Clear HTTP can be enabled at your own risk: 12 | ``` 13 | cli.api.http-enable=true 14 | cli.api.http-port=8898 15 | ``` 16 | 17 | 18 | #### API KEY 19 | API key is configured in ```whirlpool-cli-config.properties```. 20 | It can be overriden with ```--api-key=``` 21 | 22 | 23 | #### Required headers 24 | * apiVersion (see [CliApi.java](src/main/java/com/samourai/whirlpool/cli/api/protocol/CliApi.java)) 25 | * apiKey 26 | 27 | 28 | #### HTTPS cert 29 | REST API uses a self-signed certificate for HTTPS. It can be downloaded by opening https://CLI-HOST:8899/ with Firefox, then Advanced -> View certificate -> Download PEM. 30 | 31 | You can configure your own cert in `whirlpool-cli-config.properties`: 32 | ``` 33 | server.ssl.key-store-type=PKCS12 or JKS 34 | server.ssl.key-store= 35 | server.ssl.key-store-password= 36 | server.ssl.key-alias= 37 | ``` 38 | 39 | ## Pools 40 | 41 | ### List pools: ```GET /rest/pools[?tx0FeeTarget=BLOCKS_24]``` 42 | Parameters: 43 | * (optional) tx0FeeTarget: tx0 fee target for tx0BalanceMin computation 44 | 45 | Response: 46 | ``` 47 | { 48 | "pools":[ 49 | { 50 | "poolId":"0.1btc", 51 | "denomination":10000000, 52 | "feeValue":5000000, 53 | "mustMixBalanceMin":10000102, 54 | "mustMixBalanceMax":10010000, 55 | "minAnonymitySet":5, 56 | "nbRegistered":0, 57 | "mixAnonymitySet":5, 58 | "mixStatus":"CONFIRM_INPUT", 59 | "elapsedTime":22850502, 60 | "nbConfirmed":0, 61 | "tx0BalanceMin":10020005 62 | } 63 | ] 64 | } 65 | ``` 66 | 67 | ## Wallet 68 | 69 | ### Deposit: ```GET /rest/wallet/deposit[?increment=false]``` 70 | Parameters: 71 | * (optional) Use increment=true make sure this address won't be reused. 72 | 73 | Response: 74 | ``` 75 | { 76 | depositAddress: "tb1qjxzp9z2ax8mg9820dvwasy2qtle4v2q6s0cant" 77 | } 78 | ``` 79 | 80 | ## Global mix control 81 | 82 | ### Mix state: ```GET /rest/mix``` 83 | Response: 84 | ``` 85 | { 86 | 87 | "started":true, 88 | "nbMixing":1, 89 | "nbQueued":17, 90 | "threads":[{ 91 | "hash":"c7f456d5ff002faa89dadec01cc5eb98bb00fdefb92031890324ec127f9d1541", 92 | "index":5, 93 | "value":1000121, 94 | "confirmations":95, 95 | "path":"M/0/166", 96 | "account":"PREMIX", 97 | "status":"MIX_STARTED", 98 | "mixableStatus":"MIXABLE", 99 | "progressPercent":"10", 100 | "progressLabel":"CONNECTING", 101 | "poolId":"0.01btc", 102 | "priority":5, 103 | "mixsTarget":null, 104 | "mixsTargetOrDefault":1, 105 | "mixsDone":0, 106 | "message":" - [MIX 1/1] ▮▮▮▮▮▯▯▯▯▯ (5/10) CONFIRMED_INPUT : joined a mix!", 107 | "error":null, 108 | "lastActivityElapsed": 23001 109 | }] 110 | } 111 | ``` 112 | 113 | ### Start mixing: ```POST /rest/mix/start``` 114 | 115 | ### Stop mixing: ```POST /rest/mix/stop``` 116 | 117 | ## UTXO controls 118 | 119 | ### List utxos: ```GET /rest/utxos``` 120 | Response: 121 | ``` 122 | { 123 | deposit: { 124 | utxos: [(utxos detail)], 125 | balance: 0, 126 | zpub: "" 127 | }, 128 | premix: { 129 | utxos: [(utxos detail)], 130 | balance: 0, 131 | zpub: "" 132 | }, 133 | postmix: { 134 | utxos: [(utxos detail)], 135 | balance: 0, 136 | zpub: "" 137 | } 138 | } 139 | ``` 140 | 141 | ### Configure utxo: ```POST /rest/utxos/{hash}:{index}``` 142 | Parameters: 143 | * hash, index: utxo to configure 144 | 145 | Payload: 146 | * poolId: id of pool to join 147 | * mixsTarget: mixs limit (0 for unlimited) 148 | ``` 149 | { 150 | poolId: "0.01btc", 151 | mixsTarget: 0 152 | } 153 | ``` 154 | 155 | Response: 156 | ``` 157 | { 158 | (utxo detail) 159 | } 160 | ``` 161 | 162 | ### Tx0 preview ```POST /rest/utxos/{hash}:{index}/tx0Preview``` 163 | Parameters: 164 | * hash, index: utxo to spend for tx0 165 | 166 | Payload: 167 | * feeTarget (mandatory): fee target for tx0 168 | * poolId (optional): override utxo's poolId 169 | ``` 170 | { 171 | feeTarget: "BLOCKS_4", 172 | poolId: "0.01btc" 173 | } 174 | ``` 175 | 176 | 177 | Response: 178 | ``` 179 | { 180 | "txid":"aa079c0323349f4abf3fb793bf2ed1ce1e11c53cd22aeced3554872033bfa722" 181 | } 182 | ``` 183 | 184 | ### Tx0 ```POST /rest/utxos/{hash}:{index}/tx0``` 185 | Parameters: 186 | * hash, index: utxo to spend for tx0 187 | 188 | Payload: 189 | * feeTarget (mandatory): fee target for tx0 190 | * poolId (optional): override utxo's poolId 191 | * mixsTarget (optional): override utxo's mixsTarget 192 | ``` 193 | { 194 | feeTarget: "BLOCKS_4", 195 | poolId: "0.01btc", 196 | mixsTarget: 3 197 | } 198 | ``` 199 | 200 | 201 | Response: 202 | ``` 203 | { 204 | "txid":"aa079c0323349f4abf3fb793bf2ed1ce1e11c53cd22aeced3554872033bfa722" 205 | } 206 | ``` 207 | 208 | ### Start mixing UTXO: ```POST /rest/utxos/{hash}:{index}/startMix``` 209 | Parameters: 210 | * hash,index: utxo to mix. 211 | 212 | ### Stop mixing UTXO: ```POST /rest/utxos/{hash}:{index}/stopMix``` 213 | Parameters: 214 | * hash,index: utxo to stop mixing. 215 | 216 | 217 | ## CLI 218 | 219 | ### CLI state: ```GET /rest/cli``` 220 | Response: 221 | ``` 222 | { 223 | "cliStatus": "READY", 224 | "loggedIn": true, 225 | "torProgress": 100, 226 | "cliMessage": "", 227 | "network": "test", 228 | "serverUrl": "", 229 | "serverName": "TESTNET", 230 | "dojoUrl": "", 231 | "tor": true, 232 | "dojo": true 233 | } 234 | ``` 235 | 236 | ### login: ```POST /rest/cli/login``` 237 | Payload: 238 | * seedPassphrase: passphrase of configured wallet 239 | ``` 240 | { 241 | seedPassphrase: "..." 242 | } 243 | ``` 244 | 245 | Response: 246 | ``` 247 | { 248 | "cliStatus": "READY", 249 | "cliMessage": "", 250 | "loggedIn": true 251 | } 252 | ``` 253 | 254 | ### logout: ```POST /rest/cli/logout``` 255 | Response: 256 | ``` 257 | { 258 | "cliStatus": "READY", 259 | "cliMessage": "", 260 | "loggedIn": false 261 | } 262 | ``` 263 | 264 | ### initialize: ```POST /rest/cli/init``` 265 | Payload: 266 | * pairingPayload: pairing payload from Samourai Wallet 267 | * tor: enable Tor 268 | * dojo: enable Dojo (use null to auto-detect from pairingPayload) 269 | ``` 270 | { 271 | pairingPayload: "...", 272 | tor: true, 273 | dojo: true 274 | } 275 | ``` 276 | 277 | ### get config: ```GET /rest/cli/config``` 278 | 279 | ### set config: ```PUT /rest/cli/config``` 280 | 281 | ### reset config: ```DELETE /rest/cli/config``` 282 | 283 | 284 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/services/WalletAggregateService.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.services; 2 | 3 | import com.samourai.wallet.api.backend.BackendApi; 4 | import com.samourai.wallet.api.backend.MinerFeeTarget; 5 | import com.samourai.wallet.api.backend.beans.UnspentResponse; 6 | import com.samourai.wallet.client.Bip84ApiWallet; 7 | import com.samourai.wallet.client.Bip84Wallet; 8 | import com.samourai.wallet.hd.HD_Address; 9 | import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric; 10 | import com.samourai.wallet.util.FormatsUtilGeneric; 11 | import com.samourai.whirlpool.cli.config.CliConfig; 12 | import com.samourai.whirlpool.cli.wallet.CliWallet; 13 | import com.samourai.whirlpool.client.exception.NotifiableException; 14 | import com.samourai.whirlpool.client.utils.ClientUtils; 15 | import java.lang.invoke.MethodHandles; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import org.bitcoinj.core.NetworkParameters; 19 | import org.bitcoinj.core.Transaction; 20 | import org.bitcoinj.core.TransactionOutPoint; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.stereotype.Service; 24 | 25 | @Service 26 | public class WalletAggregateService { 27 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 28 | private static final int AGGREGATED_UTXOS_PER_TX = 600; 29 | private static final FormatsUtilGeneric formatUtils = FormatsUtilGeneric.getInstance(); 30 | 31 | private NetworkParameters params; 32 | private CliConfig cliConfig; 33 | private Bech32UtilGeneric bech32Util; 34 | private TxAggregateService txAggregateService; 35 | 36 | public WalletAggregateService( 37 | NetworkParameters params, 38 | CliConfig cliConfig, 39 | Bech32UtilGeneric bech32Util, 40 | TxAggregateService txAggregateService) { 41 | this.params = params; 42 | this.cliConfig = cliConfig; 43 | this.bech32Util = bech32Util; 44 | this.txAggregateService = txAggregateService; 45 | } 46 | 47 | public boolean toWallet( 48 | Bip84ApiWallet sourceWallet, 49 | Bip84Wallet destinationWallet, 50 | int feeSatPerByte, 51 | BackendApi backendApi) 52 | throws Exception { 53 | return doAggregate(sourceWallet, null, destinationWallet, feeSatPerByte, backendApi); 54 | } 55 | 56 | public boolean toAddress( 57 | Bip84ApiWallet sourceWallet, String destinationAddress, CliWallet cliWallet) 58 | throws Exception { 59 | if (!formatUtils.isTestNet(cliConfig.getServer().getParams())) { 60 | throw new NotifiableException( 61 | "aggregate toAddress is disabled on mainnet for security reasons."); 62 | } 63 | 64 | int feeSatPerByte = cliWallet.getFee(MinerFeeTarget.BLOCKS_2); 65 | BackendApi backendApi = cliWallet.getConfig().getBackendApi(); 66 | return doAggregate(sourceWallet, destinationAddress, null, feeSatPerByte, backendApi); 67 | } 68 | 69 | private boolean doAggregate( 70 | Bip84ApiWallet sourceWallet, 71 | String destinationAddress, 72 | Bip84Wallet destinationWallet, 73 | int feeSatPerByte, 74 | BackendApi backendApi) 75 | throws Exception { 76 | List utxos = sourceWallet.fetchUtxos(); 77 | if (utxos.isEmpty()) { 78 | // maybe you need to declare zpub as bip84 with /multiaddr?bip84= 79 | log.info("AggregateWallet result: no utxo to aggregate"); 80 | return false; 81 | } 82 | if (log.isDebugEnabled()) { 83 | log.debug("Found " + utxos.size() + " utxo to aggregate:"); 84 | ClientUtils.logUtxos(utxos); 85 | } 86 | 87 | boolean success = false; 88 | int round = 0; 89 | int offset = 0; 90 | while (offset < utxos.size()) { 91 | List subsetUtxos = new ArrayList<>(); 92 | offset = AGGREGATED_UTXOS_PER_TX * round; 93 | for (int i = offset; i < (offset + AGGREGATED_UTXOS_PER_TX) && i < utxos.size(); i++) { 94 | subsetUtxos.add(utxos.get(i)); 95 | } 96 | if (!subsetUtxos.isEmpty()) { 97 | String toAddress = destinationAddress; 98 | if (toAddress == null) { 99 | toAddress = bech32Util.toBech32(destinationWallet.getNextAddress(), params); 100 | } 101 | 102 | log.info("Aggregating " + subsetUtxos.size() + " utxos (pass #" + round + ")"); 103 | txAggregate(sourceWallet, subsetUtxos, toAddress, feeSatPerByte, backendApi); 104 | success = true; 105 | 106 | ClientUtils.sleepRefreshUtxos(cliConfig.getServer().getParams()); 107 | } 108 | round++; 109 | } 110 | return success; 111 | } 112 | 113 | private void txAggregate( 114 | Bip84ApiWallet sourceWallet, 115 | List postmixUtxos, 116 | String toAddress, 117 | int feeSatPerByte, 118 | BackendApi backendApi) 119 | throws Exception { 120 | List spendFromOutPoints = new ArrayList<>(); 121 | List spendFromAddresses = new ArrayList<>(); 122 | 123 | // spend 124 | for (UnspentResponse.UnspentOutput utxo : postmixUtxos) { 125 | spendFromOutPoints.add(utxo.computeOutpoint(params)); 126 | spendFromAddresses.add(sourceWallet.getAddressAt(utxo)); 127 | } 128 | 129 | // tx 130 | Transaction txAggregate = 131 | txAggregateService.txAggregate( 132 | spendFromOutPoints, spendFromAddresses, toAddress, feeSatPerByte); 133 | 134 | log.info("txAggregate:"); 135 | log.info(txAggregate.toString()); 136 | 137 | // broadcast 138 | log.info(" • Broadcasting TxAggregate..."); 139 | String txHex = ClientUtils.getTxHex(txAggregate); 140 | backendApi.pushTx(txHex); 141 | } 142 | 143 | public boolean consolidateWallet(CliWallet cliWallet) throws Exception { 144 | if (!formatUtils.isTestNet(cliConfig.getServer().getParams())) { 145 | log.warn("You should NOT consolidateWallet on mainnet for privacy reasons!"); 146 | } 147 | 148 | Bip84ApiWallet depositWallet = cliWallet.getWalletDeposit(); 149 | Bip84ApiWallet premixWallet = cliWallet.getWalletPremix(); 150 | Bip84ApiWallet postmixWallet = cliWallet.getWalletPostmix(); 151 | 152 | int feeSatPerByte = cliWallet.getFee(MinerFeeTarget.BLOCKS_2); 153 | BackendApi backendApi = cliWallet.getConfig().getBackendApi(); 154 | 155 | log.info(" • Consolidating postmix -> deposit..."); 156 | toWallet(postmixWallet, depositWallet, feeSatPerByte, backendApi); 157 | 158 | log.info(" • Consolidating premix -> deposit..."); 159 | toWallet(premixWallet, depositWallet, feeSatPerByte, backendApi); 160 | 161 | if (depositWallet.fetchUtxos().size() < 2) { 162 | log.info(" • Consolidating deposit... nothing to aggregate."); 163 | return false; 164 | } 165 | log.info(" • Consolidating deposit..."); 166 | boolean success = toWallet(depositWallet, depositWallet, feeSatPerByte, backendApi); 167 | return success; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/api/protocol/beans/ApiCliConfig.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.api.protocol.beans; 2 | 3 | import com.samourai.whirlpool.cli.beans.CliProxy; 4 | import com.samourai.whirlpool.cli.config.CliConfig; 5 | import com.samourai.whirlpool.cli.config.CliConfigFile; 6 | import com.samourai.whirlpool.cli.services.CliConfigService; 7 | import com.samourai.whirlpool.client.exception.NotifiableException; 8 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolServer; 9 | import java.lang.invoke.MethodHandles; 10 | import java.util.Properties; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | public class ApiCliConfig { 16 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 17 | 18 | private String server; 19 | private String scode; 20 | private Boolean tor; 21 | private Boolean dojo; 22 | private String proxy; 23 | private ApiMixConfig mix; 24 | 25 | public static final String KEY_SERVER = "cli.server"; 26 | private static final String KEY_SCODE = "cli.scode"; 27 | public static final String KEY_TOR = "cli.tor"; 28 | private static final String KEY_PROXY = "cli.proxy"; 29 | private static final String KEY_MIX_CLIENTS_PER_POOL = "cli.mix.clientsPerPool"; 30 | private static final String KEY_MIX_CLIENT_DELAY = "cli.mix.clientDelay"; 31 | private static final String KEY_MIX_TX0_MAX_OUTPUTS = "cli.mix.tx0MaxOutputs"; 32 | private static final String KEY_MIX_AUTO_MIX = "cli.mix.autoMix"; 33 | private static final String KEY_MIX_MIXS_TARGET = "cli.mix.mixsTarget"; 34 | public static final String KEY_API_HTTP_ENABLE = "cli.api.http-enable"; 35 | 36 | public ApiCliConfig() {} 37 | 38 | public ApiCliConfig(CliConfig cliConfig) { 39 | this.server = cliConfig.getServer().name(); 40 | this.scode = cliConfig.getScode(); 41 | this.tor = cliConfig.getTor(); 42 | this.dojo = cliConfig.getDojo().isEnabled(); 43 | this.proxy = cliConfig.getProxy(); 44 | this.mix = new ApiMixConfig(cliConfig.getMix()); 45 | } 46 | 47 | public void toProperties(Properties props) throws NotifiableException { 48 | // server is mandatory 49 | WhirlpoolServer whirlpoolServer = 50 | WhirlpoolServer.find(server) 51 | .orElseThrow(() -> new NotifiableException("Invalid value for: server")); 52 | props.put(KEY_SERVER, whirlpoolServer.name()); 53 | 54 | if (scode != null) { 55 | props.put(KEY_SCODE, scode.trim()); 56 | } 57 | 58 | if (tor != null) { 59 | props.put(KEY_TOR, Boolean.toString(tor)); 60 | } 61 | 62 | if (dojo != null) { 63 | props.put(CliConfigService.KEY_DOJO_ENABLED, Boolean.toString(dojo)); 64 | } 65 | 66 | if (proxy != null) { 67 | if (!StringUtils.isEmpty(proxy) && !CliProxy.validate(proxy)) { 68 | throw new NotifiableException("Invalid value for: proxy"); 69 | } 70 | props.put(KEY_PROXY, proxy.trim()); 71 | } 72 | 73 | if (mix != null) { 74 | mix.toProperties(props); 75 | } 76 | } 77 | 78 | public String getServer() { 79 | return server; 80 | } 81 | 82 | public void setServer(String server) { 83 | this.server = server; 84 | } 85 | 86 | public String getScode() { 87 | return scode; 88 | } 89 | 90 | public void setScode(String scode) { 91 | this.scode = scode; 92 | } 93 | 94 | public Boolean getTor() { 95 | return tor; 96 | } 97 | 98 | public void setTor(Boolean tor) { 99 | this.tor = tor; 100 | } 101 | 102 | public Boolean getDojo() { 103 | return dojo; 104 | } 105 | 106 | public void setDojo(Boolean dojo) { 107 | this.dojo = dojo; 108 | } 109 | 110 | public String getProxy() { 111 | return proxy; 112 | } 113 | 114 | public void setProxy(String proxy) { 115 | this.proxy = proxy; 116 | } 117 | 118 | public ApiMixConfig getMix() { 119 | return mix; 120 | } 121 | 122 | public void setMix(ApiMixConfig mix) { 123 | this.mix = mix; 124 | } 125 | 126 | public static class ApiMixConfig { 127 | private Integer clientsPerPool; 128 | private Integer clientDelay; 129 | private Integer tx0MaxOutputs; 130 | private Boolean autoMix; 131 | private Integer mixsTarget; 132 | 133 | public ApiMixConfig() {} 134 | 135 | public ApiMixConfig(CliConfigFile.MixConfig mixConfig) { 136 | this.clientsPerPool = mixConfig.getClientsPerPool(); 137 | this.clientDelay = mixConfig.getClientDelay(); 138 | this.tx0MaxOutputs = mixConfig.getTx0MaxOutputs(); 139 | this.autoMix = mixConfig.isAutoMix(); 140 | this.mixsTarget = mixConfig.getMixsTarget(); 141 | } 142 | 143 | public void toProperties(Properties props) throws NotifiableException { 144 | if (clientsPerPool != null) { 145 | if (clientsPerPool < 1) { 146 | throw new NotifiableException("mix.clientsPerPool should be > 0"); 147 | } 148 | props.put(KEY_MIX_CLIENTS_PER_POOL, Integer.toString(clientsPerPool)); 149 | } 150 | if (clientDelay != null) { 151 | if (clientDelay < 1) { 152 | throw new NotifiableException("mix.clientDelay should be > 1"); 153 | } 154 | props.put(KEY_MIX_CLIENT_DELAY, Integer.toString(clientDelay)); 155 | } 156 | if (tx0MaxOutputs != null) { 157 | if (tx0MaxOutputs < 0) { 158 | throw new NotifiableException("mix.tx0MaxOutputs should be >= 0"); 159 | } 160 | props.put(KEY_MIX_TX0_MAX_OUTPUTS, Integer.toString(tx0MaxOutputs)); 161 | } 162 | if (autoMix != null) { 163 | props.put(KEY_MIX_AUTO_MIX, Boolean.toString(autoMix)); 164 | } 165 | if (mixsTarget != null) { 166 | if (mixsTarget < 1) { 167 | throw new NotifiableException("mix.mixTargets should be > 0"); 168 | } 169 | props.put(KEY_MIX_MIXS_TARGET, Integer.toString(mixsTarget)); 170 | } 171 | } 172 | 173 | public Integer getClientsPerPool() { 174 | return clientsPerPool; 175 | } 176 | 177 | public void setClientsPerPool(Integer clientsPerPool) { 178 | this.clientsPerPool = clientsPerPool; 179 | } 180 | 181 | public Integer getClientDelay() { 182 | return clientDelay; 183 | } 184 | 185 | public void setClientDelay(Integer clientDelay) { 186 | this.clientDelay = clientDelay; 187 | } 188 | 189 | public Integer getTx0MaxOutputs() { 190 | return tx0MaxOutputs; 191 | } 192 | 193 | public void setTx0MaxOutputs(Integer tx0MaxOutputs) { 194 | this.tx0MaxOutputs = tx0MaxOutputs; 195 | } 196 | 197 | public Boolean getAutoMix() { 198 | return autoMix; 199 | } 200 | 201 | public void setAutoMix(Boolean autoMix) { 202 | this.autoMix = autoMix; 203 | } 204 | 205 | public Integer getMixsTarget() { 206 | return mixsTarget; 207 | } 208 | 209 | public void setMixsTarget(Integer mixsTarget) { 210 | this.mixsTarget = mixsTarget; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/wallet/CliWallet.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli.wallet; 2 | 3 | import com.samourai.wallet.client.Bip84ApiWallet; 4 | import com.samourai.whirlpool.cli.config.CliConfig; 5 | import com.samourai.whirlpool.cli.services.CliConfigService; 6 | import com.samourai.whirlpool.cli.services.CliTorClientService; 7 | import com.samourai.whirlpool.cli.services.JavaHttpClientService; 8 | import com.samourai.whirlpool.cli.services.WalletAggregateService; 9 | import com.samourai.whirlpool.cli.utils.CliUtils; 10 | import com.samourai.whirlpool.client.exception.EmptyWalletException; 11 | import com.samourai.whirlpool.client.exception.NotifiableException; 12 | import com.samourai.whirlpool.client.mix.listener.MixSuccess; 13 | import com.samourai.whirlpool.client.tx0.Tx0Config; 14 | import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; 15 | import com.samourai.whirlpool.client.wallet.beans.MixProgress; 16 | import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; 17 | import com.samourai.whirlpool.client.whirlpool.beans.Pool; 18 | import io.reactivex.Observable; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | public class CliWallet extends WhirlpoolWallet { 23 | private static final Logger log = LoggerFactory.getLogger(CliWallet.class); 24 | 25 | private CliConfig cliConfig; 26 | private CliConfigService cliConfigService; 27 | private WalletAggregateService walletAggregateService; 28 | private CliTorClientService cliTorClientService; 29 | private JavaHttpClientService httpClientService; 30 | 31 | public CliWallet( 32 | WhirlpoolWallet whirlpoolWallet, 33 | CliConfig cliConfig, 34 | CliConfigService cliConfigService, 35 | WalletAggregateService walletAggregateService, 36 | CliTorClientService cliTorClientService, 37 | JavaHttpClientService httpClientService) { 38 | super(whirlpoolWallet); 39 | this.cliConfig = cliConfig; 40 | this.cliConfigService = cliConfigService; 41 | this.walletAggregateService = walletAggregateService; 42 | this.cliTorClientService = cliTorClientService; 43 | this.httpClientService = httpClientService; 44 | } 45 | 46 | @Override 47 | public void start() { 48 | if (!cliConfigService.isCliStatusReady()) { 49 | log.warn("Cannot start wallet: cliStatus is not ready"); 50 | return; 51 | } 52 | // start wallet 53 | super.start(); 54 | } 55 | 56 | @Override 57 | public void stop() { 58 | super.stop(); 59 | } 60 | 61 | @Override 62 | public Observable mix(WhirlpoolUtxo whirlpoolUtxo) throws NotifiableException { 63 | // get Tor ready before mixing 64 | cliTorClientService.waitReady(); 65 | return super.mix(whirlpoolUtxo); 66 | } 67 | 68 | @Override 69 | public void onMixSuccess(WhirlpoolUtxo whirlpoolUtxo, MixSuccess mixSuccess) { 70 | super.onMixSuccess(whirlpoolUtxo, mixSuccess); 71 | 72 | // change Tor identity 73 | if (cliConfig.getTor()) { 74 | cliTorClientService.changeIdentity(); 75 | httpClientService.changeIdentityRest(); 76 | } 77 | } 78 | 79 | @Override 80 | public synchronized void onEmptyWalletException(EmptyWalletException e) { 81 | try { 82 | if (cliConfig.isAutoAggregatePostmix()) { 83 | // run autoAggregatePostmix 84 | autoRefill(e); 85 | } else { 86 | // default management 87 | throw e; 88 | } 89 | } catch (Exception ee) { 90 | // default management 91 | super.onEmptyWalletException(e); 92 | } 93 | } 94 | 95 | private void autoRefill(EmptyWalletException e) throws Exception { 96 | long requiredBalance = e.getBalanceRequired(); 97 | Bip84ApiWallet depositWallet = getWalletDeposit(); 98 | Bip84ApiWallet premixWallet = getWalletPremix(); 99 | Bip84ApiWallet postmixWallet = getWalletPostmix(); 100 | 101 | // check total balance 102 | long depositBalance = depositWallet.fetchBalance(); 103 | long premixBalance = premixWallet.fetchBalance(); 104 | long postmixBalance = postmixWallet.fetchBalance(); 105 | long totalBalance = depositBalance + premixBalance + postmixBalance; 106 | if (log.isDebugEnabled()) { 107 | log.debug("depositBalance=" + depositBalance); 108 | log.debug("premixBalance=" + premixBalance); 109 | log.debug("postmixBalance=" + postmixBalance); 110 | log.debug("totalBalance=" + totalBalance); 111 | } 112 | 113 | long missingBalance = requiredBalance - totalBalance; 114 | if (log.isDebugEnabled()) { 115 | log.debug("requiredBalance=" + requiredBalance + " => missingBalance=" + missingBalance); 116 | } 117 | if (missingBalance > 0) { 118 | // cannot autoAggregatePostmix 119 | throw new EmptyWalletException("Insufficient balance to continue", missingBalance); 120 | } 121 | 122 | // auto aggregate postmix is possible 123 | log.info(" o AutoAggregatePostmix: depositWallet wallet is empty => aggregating"); 124 | Exception aggregateException = null; 125 | try { 126 | boolean success = walletAggregateService.consolidateWallet(this); 127 | if (!success) { 128 | throw new NotifiableException("AutoAggregatePostmix failed (nothing to aggregate?)"); 129 | } 130 | if (log.isDebugEnabled()) { 131 | log.debug("AutoAggregatePostmix SUCCESS. "); 132 | } 133 | } catch (Exception ee) { 134 | // resume wallet before throwing exception (to retry later) 135 | aggregateException = ee; 136 | if (log.isDebugEnabled()) { 137 | log.debug("AutoAggregatePostmix ERROR, will throw error later."); 138 | } 139 | } 140 | 141 | // reset mixing threads to avoid mixing obsolete consolidated utxos 142 | mixOrchestrator.stopMixingClients(); 143 | 144 | clearCache(); 145 | 146 | if (aggregateException != null) { 147 | throw aggregateException; 148 | } 149 | } 150 | 151 | @Override 152 | public Tx0Config getTx0Config(Pool pool) { 153 | Tx0Config tx0Config = super.getTx0Config(pool); 154 | 155 | // maxOutputs 156 | if (cliConfig.getMix().getTx0MaxOutputs() > 0) { 157 | int maxOutputs = cliConfig.getMix().getTx0MaxOutputs(); 158 | tx0Config.setMaxOutputs(maxOutputs); 159 | } 160 | 161 | // overspend 162 | String poolId = pool.getPoolId(); 163 | Long overspendOrNull = 164 | cliConfig.getMix().getOverspend() != null 165 | ? cliConfig.getMix().getOverspend().get(poolId) 166 | : null; 167 | if (overspendOrNull != null) { 168 | tx0Config.setOverspend(overspendOrNull); 169 | } 170 | return tx0Config; 171 | } 172 | 173 | @Override 174 | public void notifyError(String message) { 175 | CliUtils.notifyError(message); 176 | } 177 | 178 | // make public 179 | 180 | @Override 181 | public Bip84ApiWallet getWalletDeposit() { 182 | return super.getWalletDeposit(); 183 | } 184 | 185 | @Override 186 | public Bip84ApiWallet getWalletPremix() { 187 | return super.getWalletPremix(); 188 | } 189 | 190 | @Override 191 | public Bip84ApiWallet getWalletPostmix() { 192 | return super.getWalletPostmix(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/tor/client/TorOnionProxyInstance.java: -------------------------------------------------------------------------------- 1 | package com.samourai.tor.client; 2 | 3 | import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext; 4 | import com.msopentech.thali.toronionproxy.OnionProxyManager; 5 | import com.msopentech.thali.toronionproxy.TorConfig; 6 | import com.msopentech.thali.toronionproxy.TorConfigBuilder; 7 | import com.msopentech.thali.toronionproxy.TorSettings; 8 | import com.samourai.http.client.HttpUsage; 9 | import com.samourai.tor.client.utils.WhirlpoolTorInstaller; 10 | import com.samourai.whirlpool.cli.Application; 11 | import com.samourai.whirlpool.cli.beans.CliProxy; 12 | import com.samourai.whirlpool.cli.beans.CliProxyProtocol; 13 | import com.samourai.whirlpool.client.exception.NotifiableException; 14 | import java.lang.invoke.MethodHandles; 15 | import java.util.Collection; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.util.SocketUtils; 22 | 23 | public class TorOnionProxyInstance { 24 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 25 | private static final int PROGRESS_CONNECTING = 50; 26 | 27 | private OnionProxyManager onionProxyManager; 28 | private Thread startThread; 29 | private boolean torSocksReady = false; 30 | private Map torProxies = null; 31 | private int progress; 32 | 33 | public TorOnionProxyInstance( 34 | WhirlpoolTorInstaller torInstaller, TorSettings torSettings, Collection httpUsages) 35 | throws Exception { 36 | TorConfig torConfig = torInstaller.getConfig(); 37 | if (log.isDebugEnabled()) { 38 | log.debug("new TorOnionProxyInstance: " + torConfig + " ; " + torSettings); 39 | } 40 | 41 | JavaOnionProxyContext context = new JavaOnionProxyContext(torConfig, torInstaller, torSettings); 42 | onionProxyManager = new OnionProxyManager(context); 43 | 44 | TorConfigBuilder builder = onionProxyManager.getContext().newConfigBuilder().updateTorConfig(); 45 | 46 | torProxies = new ConcurrentHashMap<>(); 47 | for (HttpUsage httpUsage : httpUsages) { 48 | int socksPort = SocketUtils.findAvailableTcpPort(); 49 | builder.socksPort(Integer.toString(socksPort), null); 50 | CliProxy torProxy = new CliProxy(CliProxyProtocol.SOCKS, "127.0.0.1", socksPort); 51 | torProxies.put(httpUsage, torProxy); 52 | } 53 | 54 | onionProxyManager.getContext().getInstaller().updateTorConfigCustom(builder.asString()); 55 | onionProxyManager.setup(); 56 | 57 | startThread = null; 58 | progress = 0; 59 | } 60 | 61 | public synchronized void start() { 62 | if (startThread != null) { 63 | log.warn("Tor is already started"); 64 | return; 65 | } 66 | 67 | if (log.isDebugEnabled()) { 68 | log.debug("Starting Tor connexion..."); 69 | } 70 | progress = PROGRESS_CONNECTING; 71 | 72 | startThread = 73 | new Thread( 74 | () -> { 75 | try { 76 | boolean ok = onionProxyManager.startWithRepeat(4 * 60, 5, false); 77 | if (!ok) { 78 | log.error("Couldn't start tor"); 79 | throw new RuntimeException("Couldn't start tor"); 80 | } 81 | waitReady(); 82 | } catch (Exception e) { 83 | log.error("Tor failed to start", e); 84 | Application.exitError(1); 85 | } 86 | }, 87 | "TorOnionProxyInstance-start"); 88 | startThread.setDaemon(true); 89 | startThread.start(); 90 | } 91 | 92 | public void waitReady() throws NotifiableException { 93 | if (progress != 100) { 94 | if (log.isDebugEnabled()) { 95 | log.debug("Waiting for Tor connexion..."); 96 | } 97 | } 98 | while (!checkReady()) { 99 | if (startThread == null) { 100 | throw new NotifiableException("Tor connect failed"); 101 | } 102 | if (log.isDebugEnabled()) { 103 | log.debug("Waiting for Tor circuit... "); 104 | } 105 | try { 106 | Thread.sleep(500); 107 | } catch (InterruptedException e) { 108 | } 109 | } 110 | } 111 | 112 | private boolean checkReady() { 113 | boolean ready = onionProxyManager.isRunning(); 114 | if (ready && progress != 100) { 115 | if (log.isDebugEnabled()) { 116 | log.debug("Tor connected! torProxies=" + torProxies); 117 | } 118 | progress = 100; 119 | } 120 | if (!ready && progress == 100) { 121 | if (log.isDebugEnabled()) { 122 | log.debug("Tor disconnected!"); 123 | } 124 | progress = PROGRESS_CONNECTING; 125 | } 126 | return ready; 127 | } 128 | 129 | public synchronized void stop() { 130 | if (log.isDebugEnabled()) { 131 | log.debug("stopping Tor"); 132 | } 133 | startThread = null; 134 | progress = 0; 135 | 136 | try { 137 | onionProxyManager.stop(); 138 | } catch (Exception e) { 139 | if (log.isDebugEnabled()) { 140 | log.error("", e); 141 | } 142 | } 143 | 144 | torSocksReady = false; 145 | } 146 | 147 | public synchronized void clear() { 148 | if (log.isDebugEnabled()) { 149 | log.debug("clearing Tor"); 150 | } 151 | Thread stopThread = 152 | new Thread( 153 | () -> { 154 | stop(); 155 | }, 156 | "TorOnionProxyInstance-stop"); 157 | stopThread.setDaemon(true); 158 | stopThread.start(); 159 | /*try { 160 | onionProxyManager.killTorProcess(); 161 | } catch (Exception e) { 162 | log.error("", e); 163 | }*/ 164 | onionProxyManager.getContext().getConfig().getInstallDir().delete(); 165 | } 166 | 167 | public void changeIdentity() { 168 | progress = PROGRESS_CONNECTING; 169 | boolean success = onionProxyManager.setNewIdentity(); 170 | if (success) { 171 | // watch tor progress in a new thread 172 | Thread statusThread = 173 | new Thread( 174 | () -> { 175 | try { 176 | waitReady(); // updates progress 177 | } catch (Exception e) { 178 | log.error("", e); 179 | } 180 | }, 181 | "TorOnionProxyInstance-status"); 182 | statusThread.setDaemon(true); 183 | statusThread.start(); 184 | } else { 185 | log.warn("changeIdentity failed, restarting Tor..."); 186 | stop(); 187 | start(); 188 | } 189 | } 190 | 191 | protected int getProgress() { 192 | return progress; 193 | } 194 | 195 | public Optional getTorProxy(HttpUsage httpUsage) { 196 | waitTorSocks(); 197 | CliProxy torProxy = torProxies.get(httpUsage); 198 | if (torProxy == null) { 199 | return Optional.empty(); 200 | } 201 | return Optional.of(torProxy); 202 | } 203 | 204 | private void waitTorSocks() { 205 | while (!torSocksReady && onionProxyManager.isRunning()) { 206 | try { 207 | // we should have a connexion now 208 | onionProxyManager.getIPv4LocalHostSocksPort(); 209 | torSocksReady = true; 210 | log.info("TorSocks started."); 211 | } catch (Exception e) { 212 | log.error("TorSocks error", e); 213 | } 214 | try { 215 | Thread.sleep(500); 216 | } catch (InterruptedException e) { 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.github.Samourai-Wallet 5 | whirlpool-client-cli 6 | develop-SNAPSHOT 7 | whirlpool-client-cli 8 | 9 | 2.1.6.RELEASE 10 | 5.1.6.RELEASE 11 | 5.1.6.RELEASE 12 | 1.8 13 | 1.8 14 | samourai 15 | 16 | 17 | 18 | com.github.Samourai-Wallet 19 | whirlpool-client 20 | develop-SNAPSHOT 21 | compile 22 | 23 | 24 | wf.bitcoin 25 | JavaBitcoindRpcClient 26 | 1.0.0 27 | compile 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | ${spring-boot.version} 33 | compile 34 | 35 | 36 | org.springframework 37 | spring-websocket 38 | ${spring-websocket.version} 39 | compile 40 | 41 | 42 | org.springframework 43 | spring-messaging 44 | ${spring-websocket.version} 45 | compile 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-security 50 | ${spring-boot.version} 51 | compile 52 | 53 | 54 | org.apache.httpcomponents 55 | httpclient 56 | 4.5.8 57 | 58 | 59 | org.eclipse.jetty.websocket 60 | websocket-client 61 | 9.4.19.v20190610 62 | 63 | 64 | com.msopentech.thali 65 | java 66 | 0.0.3 67 | 68 | 69 | com.msopentech.thali 70 | universal 71 | 0.0.3 72 | 73 | 74 | net.freehaven.tor.control 75 | jtorctl 76 | 0.2 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-test 82 | ${spring-boot.version} 83 | test 84 | 85 | 86 | com.vaadin.external.google 87 | android-json 88 | 89 | 90 | 91 | 92 | org.mockito 93 | mockito-core 94 | 2.27.0 95 | test 96 | 97 | 98 | org.junit.platform 99 | junit-platform-launcher 100 | 1.4.2 101 | test 102 | 103 | 104 | org.junit.jupiter 105 | junit-jupiter-engine 106 | 5.4.2 107 | test 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-surefire-plugin 115 | 2.22.1 116 | 117 | 118 | org.springframework.boot 119 | spring-boot-maven-plugin 120 | ${spring-boot.version} 121 | 122 | true 123 | com.samourai.whirlpool.cli.Application 124 | 125 | 126 | 127 | 128 | repackage 129 | 130 | 131 | run 132 | 133 | 134 | 135 | 136 | 137 | com.coveo 138 | fmt-maven-plugin 139 | 2.6.0 140 | 141 | 142 | 143 | format 144 | 145 | 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-release-plugin 151 | 2.5.3 152 | 153 | @{project.version} 154 | develop-SNAPSHOT 155 | 156 | 157 | 158 | 159 | 160 | 161 | jitpack.io 162 | https://jitpack.io 163 | 164 | 165 | 166 | spring-plugins 167 | https://repo.spring.io/plugins-release/ 168 | 169 | 170 | 171 | scm:git:https://github.com/Samourai-Wallet/whirlpool-client-cli.git 172 | HEAD 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/stomp/client/JavaStompClient.java: -------------------------------------------------------------------------------- 1 | package com.samourai.stomp.client; 2 | 3 | import com.samourai.http.client.JavaHttpClient; 4 | import com.samourai.whirlpool.client.utils.ClientUtils; 5 | import com.samourai.whirlpool.client.utils.MessageErrorListener; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Map; 9 | import org.eclipse.jetty.client.HttpClient; 10 | import org.eclipse.jetty.websocket.client.WebSocketClient; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.messaging.converter.MappingJackson2MessageConverter; 14 | import org.springframework.messaging.simp.stomp.StompCommand; 15 | import org.springframework.messaging.simp.stomp.StompHeaders; 16 | import org.springframework.messaging.simp.stomp.StompSession; 17 | import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; 18 | import org.springframework.scheduling.TaskScheduler; 19 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 20 | import org.springframework.web.socket.WebSocketHttpHeaders; 21 | import org.springframework.web.socket.client.jetty.JettyWebSocketClient; 22 | import org.springframework.web.socket.messaging.WebSocketStompClient; 23 | import org.springframework.web.socket.sockjs.client.JettyXhrTransport; 24 | import org.springframework.web.socket.sockjs.client.SockJsClient; 25 | import org.springframework.web.socket.sockjs.client.Transport; 26 | import org.springframework.web.socket.sockjs.client.WebSocketTransport; 27 | 28 | public class JavaStompClient implements IStompClient { 29 | private static final Logger log = LoggerFactory.getLogger(JavaStompClient.class); 30 | private static final int HEARTBEAT_DELAY = 20000; 31 | 32 | private JavaHttpClient httpClient; 33 | private TaskScheduler taskScheduler; 34 | 35 | private WebSocketStompClient stompClient; 36 | private StompSession stompSession; 37 | 38 | public JavaStompClient(JavaHttpClient httpClient, ThreadPoolTaskScheduler taskScheduler) { 39 | this.httpClient = httpClient; 40 | this.taskScheduler = taskScheduler; 41 | } 42 | 43 | @Override 44 | public void connect( 45 | String url, 46 | Map stompHeaders, 47 | MessageErrorListener onConnectOnDisconnectListener) { 48 | 49 | WebSocketHttpHeaders httpHeaders = computeHttpHeaders(); 50 | StompHeaders stompHeadersObj = computeStompHeaders(stompHeaders); 51 | try { 52 | this.stompClient = computeStompClient(); 53 | this.stompSession = 54 | stompClient 55 | .connect( 56 | url, 57 | httpHeaders, 58 | stompHeadersObj, 59 | computeStompSessionHandler(onConnectOnDisconnectListener)) 60 | .get(); 61 | } catch (Exception e) { 62 | // connexion failed 63 | disconnect(); 64 | onConnectOnDisconnectListener.onError(e); 65 | } 66 | } 67 | 68 | @Override 69 | public void subscribe( 70 | Map stompHeaders, 71 | final MessageErrorListener onMessageOnErrorListener) { 72 | StompHeaders stompHeadersObj = computeStompHeaders(stompHeaders); 73 | JavaStompFrameHandler frameHandler = new JavaStompFrameHandler(onMessageOnErrorListener); 74 | stompSession.subscribe(stompHeadersObj, frameHandler); 75 | } 76 | 77 | @Override 78 | public void send(Map stompHeaders, Object payload) { 79 | StompHeaders stompHeadersObj = computeStompHeaders(stompHeaders); 80 | stompSession.send(stompHeadersObj, payload); 81 | } 82 | 83 | @Override 84 | public void disconnect() { 85 | if (stompSession != null) { 86 | try { 87 | stompSession.disconnect(); 88 | } catch (Exception e) { 89 | } 90 | stompSession = null; 91 | } 92 | 93 | if (stompClient != null) { 94 | try { 95 | stompClient.stop(); 96 | } catch (Exception e) { 97 | } 98 | stompClient = null; 99 | } 100 | } 101 | 102 | private StompSessionHandlerAdapter computeStompSessionHandler( 103 | final MessageErrorListener onConnectOnDisconnectListener) { 104 | return new StompSessionHandlerAdapter() { 105 | 106 | @Override 107 | public void afterConnected(StompSession session, StompHeaders connectedHeaders) { 108 | super.afterConnected(session, connectedHeaders); 109 | if (log.isDebugEnabled()) { 110 | log.debug( 111 | "connected, connectedHeaders=" + connectedHeaders + ", stompSession=" + stompSession); 112 | } 113 | // set session twice, as we need it for subscribe 114 | stompSession = session; 115 | // notify connected 116 | onConnectOnDisconnectListener.onMessage(null); 117 | } 118 | 119 | @Override 120 | public void handleException( 121 | StompSession session, 122 | StompCommand command, 123 | StompHeaders headers, 124 | byte[] payload, 125 | Throwable exception) { 126 | super.handleException(session, command, headers, payload, exception); 127 | log.error(" ! transportException", exception); 128 | } 129 | 130 | @Override 131 | public void handleTransportError(StompSession session, Throwable exception) { 132 | super.handleTransportError(session, exception); 133 | log.error( 134 | " ! transportError: " + exception.getClass().getName() + ": " + exception.getMessage()); 135 | disconnect(); 136 | onConnectOnDisconnectListener.onError(exception); 137 | 138 | if (log.isDebugEnabled()) { 139 | log.error("", exception); 140 | } 141 | } 142 | }; 143 | } 144 | 145 | private WebSocketStompClient computeStompClient() throws Exception { 146 | SockJsClient webSocketClient = computeWebSocketClient(); 147 | WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); 148 | stompClient.setMessageConverter(new MappingJackson2MessageConverter()); 149 | // enable heartbeat (mandatory to detect client disconnect) 150 | stompClient.setTaskScheduler(taskScheduler); 151 | stompClient.setDefaultHeartbeat(new long[] {HEARTBEAT_DELAY, HEARTBEAT_DELAY}); 152 | return stompClient; 153 | } 154 | 155 | private SockJsClient computeWebSocketClient() throws Exception { 156 | HttpClient jettyHttpClient = httpClient.getJettyHttpClient(); 157 | 158 | if (log.isDebugEnabled()) { 159 | log.debug("Using websocket transports: Websocket, XHR"); 160 | } 161 | JettyWebSocketClient jettyWebSocketClient = 162 | new JettyWebSocketClient(new WebSocketClient(jettyHttpClient)); 163 | List webSocketTransports = 164 | Arrays.asList( 165 | new WebSocketTransport(jettyWebSocketClient), new JettyXhrTransport(jettyHttpClient)); 166 | 167 | SockJsClient sockJsClient = new SockJsClient(webSocketTransports); 168 | jettyWebSocketClient.start(); 169 | return sockJsClient; 170 | } 171 | 172 | private WebSocketHttpHeaders computeHttpHeaders() { 173 | WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders(); 174 | httpHeaders.set("user-agent", ClientUtils.USER_AGENT); // prevent user-agent tracking 175 | return httpHeaders; 176 | } 177 | 178 | private StompHeaders computeStompHeaders(Map stompHeaders) { 179 | StompHeaders stompHeadersObj = new StompHeaders(); 180 | for (Map.Entry entry : stompHeaders.entrySet()) { 181 | stompHeadersObj.set(entry.getKey(), entry.getValue()); 182 | } 183 | return stompHeadersObj; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/com/samourai/whirlpool/cli/ApplicationArgs.java: -------------------------------------------------------------------------------- 1 | package com.samourai.whirlpool.cli; 2 | 3 | import com.samourai.whirlpool.cli.config.CliConfig; 4 | import java.lang.invoke.MethodHandles; 5 | import java.util.Iterator; 6 | import java.util.Optional; 7 | import java.util.stream.Stream; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.boot.ApplicationArguments; 12 | import org.springframework.stereotype.Service; 13 | 14 | /** Parsing command-line client arguments. */ 15 | @Service 16 | public class ApplicationArgs { 17 | private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 18 | 19 | private static final String ARG_DEBUG = "debug"; 20 | private static final String ARG_DEBUG_CLIENT = "debug-client"; 21 | private static final String ARG_LIST_POOLS = "list-pools"; 22 | private static final String ARG_SCODE = "scode"; 23 | private static final String ARG_CLIENTS = "clients"; 24 | private static final String ARG_CLIENT_DELAY = "client-delay"; 25 | private static final String ARG_TX0_DELAY = "tx0-delay"; 26 | private static final String ARG_TX0_MAX_OUTPUTS = "tx0-max-outputs"; 27 | private static final String ARG_AGGREGATE_POSTMIX = "aggregate-postmix"; 28 | private static final String ARG_AUTO_AGGREGATE_POSTMIX = "auto-aggregate-postmix"; 29 | private static final String ARG_AUTO_TX0 = "auto-tx0"; 30 | private static final String ARG_AUTO_MIX = "auto-mix"; 31 | private static final String ARG_LISTEN = "listen"; 32 | private static final String ARG_API_KEY = "api-key"; 33 | public static final String ARG_INIT = "init"; 34 | private static final String ARG_AUTHENTICATE = "authenticate"; 35 | private static final String ARG_MIXS_TARGET = "mixs-target"; 36 | private static final String ARG_DUMP_PAYLOAD = "dump-payload"; 37 | 38 | private ApplicationArguments args; 39 | 40 | public ApplicationArgs(ApplicationArguments args) { 41 | this.args = args; 42 | } 43 | 44 | public void override(CliConfig cliConfig) { 45 | String value; 46 | Boolean valueBool; 47 | Integer valueInt; 48 | 49 | value = optionalOption(ARG_SCODE); 50 | if (value != null) { 51 | cliConfig.setScode(value); 52 | } 53 | 54 | valueBool = optionalBoolean(ARG_AUTO_MIX); 55 | if (valueBool != null) { 56 | cliConfig.getMix().setAutoMix(valueBool); 57 | } 58 | 59 | valueInt = optionalInt(ARG_CLIENTS); 60 | if (valueInt != null) { 61 | cliConfig.getMix().setClients(valueInt); 62 | } 63 | 64 | valueInt = optionalInt(ARG_CLIENT_DELAY); 65 | if (valueInt != null) { 66 | cliConfig.getMix().setClientDelay(valueInt); 67 | } 68 | 69 | valueInt = optionalInt(ARG_TX0_DELAY); 70 | if (valueInt != null) { 71 | cliConfig.getMix().setTx0Delay(valueInt); 72 | } 73 | 74 | valueInt = optionalInt(ARG_TX0_MAX_OUTPUTS); 75 | if (valueInt != null) { 76 | cliConfig.getMix().setTx0MaxOutputs(valueInt); 77 | } 78 | 79 | valueBool = optionalBoolean(ARG_AUTO_AGGREGATE_POSTMIX); 80 | if (valueBool != null) { 81 | cliConfig.setAutoAggregatePostmix(valueBool); 82 | } 83 | 84 | value = optionalOption(ARG_AUTO_TX0); 85 | if (value != null) { 86 | cliConfig.setAutoTx0PoolId(value); 87 | } 88 | 89 | value = optionalOption(ARG_API_KEY); 90 | if (value != null) { 91 | cliConfig.setApiKey(value); 92 | } 93 | 94 | valueInt = optionalInt(ARG_MIXS_TARGET); 95 | if (valueInt != null) { 96 | cliConfig.getMix().setMixsTarget(valueInt); 97 | } 98 | } 99 | 100 | public boolean isListPools() { 101 | Boolean listPools = optionalBoolean(ARG_LIST_POOLS); 102 | if (listPools == null) { 103 | listPools = false; 104 | } 105 | return listPools; 106 | } 107 | 108 | public boolean isDumpPayload() { 109 | return args.containsOption(ARG_DUMP_PAYLOAD); 110 | } 111 | 112 | public String getAggregatePostmix() { 113 | return optionalOption(ARG_AGGREGATE_POSTMIX); 114 | } 115 | 116 | public boolean isAggregatePostmix() { 117 | return !StringUtils.isEmpty(getAggregatePostmix()); 118 | } 119 | 120 | public boolean isInit() { 121 | return args.containsOption(ARG_INIT); 122 | } 123 | 124 | public boolean isAuthenticate() { 125 | return args.containsOption(ARG_AUTHENTICATE); 126 | } 127 | 128 | public static boolean getMainListen(String[] mainArgs) { 129 | return mainBoolean(ARG_LISTEN, mainArgs); 130 | } 131 | 132 | public static boolean isMainDebug(String[] mainArgs) { 133 | return mainBoolean(ARG_DEBUG, mainArgs); 134 | } 135 | 136 | public static boolean isMainDebugClient(String[] mainArgs) { 137 | return mainBoolean(ARG_DEBUG_CLIENT, mainArgs); 138 | } 139 | 140 | private String requireOption(String name, String defaultValue) { 141 | // arg not found 142 | if (!args.getOptionNames().contains(name)) { 143 | if (log.isDebugEnabled()) { 144 | log.debug("--" + name + "=" + defaultValue + " (default value)"); 145 | } 146 | return defaultValue; 147 | } 148 | // --param (with no value) => "true" 149 | Iterator iter = args.getOptionValues(name).iterator(); 150 | if (!iter.hasNext()) { 151 | return "true"; 152 | } 153 | return iter.next(); 154 | } 155 | 156 | private String requireOption(String name) { 157 | String value = requireOption(name, null); 158 | if (value == null) { 159 | throw new IllegalArgumentException("Missing required option: " + name); 160 | } 161 | return value; 162 | } 163 | 164 | private String optionalOption(String name) { 165 | return requireOption(name, null); 166 | } 167 | 168 | private Boolean optionalBoolean(String name) { 169 | String value = optionalOption(name); 170 | return value != null ? Boolean.parseBoolean(value) : null; 171 | } 172 | 173 | private Integer optionalInt(String name) { 174 | String value = optionalOption(name); 175 | return value != null ? Integer.parseInt(value) : null; 176 | } 177 | 178 | private static String mainArg( 179 | String name, 180 | String[] mainArgs, 181 | String defaultValueWhenNotPresent, 182 | String defaultValueWhenPresent) { 183 | Optional argFound = 184 | Stream.of(mainArgs).filter(s -> s.startsWith("--" + name)).findFirst(); 185 | 186 | // arg not found 187 | if (!argFound.isPresent()) { 188 | return defaultValueWhenNotPresent; 189 | } 190 | 191 | // --param (with no value) => "true" 192 | String[] argSplit = argFound.get().split("="); 193 | if (argSplit.length == 1) { 194 | return defaultValueWhenPresent; 195 | } 196 | return argSplit[1]; 197 | } 198 | 199 | private static Boolean mainBoolean(String name, String[] mainArgs) { 200 | return Boolean.parseBoolean(mainArg(name, mainArgs, "false", "true")); 201 | } 202 | 203 | private static Integer mainInteger( 204 | String name, 205 | String[] mainArgs, 206 | Integer defaultValueWhenNotPresent, 207 | Integer defaultValueWhenPresent) { 208 | String str = 209 | mainArg( 210 | name, 211 | mainArgs, 212 | defaultValueWhenNotPresent != null 213 | ? Integer.toString(defaultValueWhenNotPresent) 214 | : null, 215 | defaultValueWhenPresent != null ? Integer.toString(defaultValueWhenPresent) : null); 216 | if (str == null) { 217 | return null; 218 | } 219 | return Integer.parseInt(str); 220 | } 221 | 222 | public ApplicationArguments getApplicationArguments() { 223 | return args; 224 | } 225 | } 226 | --------------------------------------------------------------------------------