diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index d52f38b3e8f..65ad21a8f9d 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -33,7 +33,7 @@ - + diff --git a/build.gradle b/build.gradle index 2e2341b2cee..d361cd0da22 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ configure(subprojects) { fontawesomefxVersion = '8.0.0' fontawesomefxCommonsVersion = '9.1.2' fontawesomefxMaterialdesignfontVersion = '2.0.26-9.1.2' + grpcVersion = '1.25.0' guavaVersion = '20.0' guiceVersion = '4.2.2' hamcrestVersion = '1.3' @@ -59,7 +60,8 @@ configure(subprojects) { lombokVersion = '1.18.2' mockitoVersion = '3.0.0' netlayerVersion = '0.6.5.2' - protobufVersion = '3.9.1' + protobufVersion = '3.10.0' + protocVersion = protobufVersion pushyVersion = '0.13.2' qrgenVersion = '1.3' sarxosVersion = '0.3.12' @@ -86,7 +88,9 @@ configure(subprojects) { } -configure([project(':desktop'), +configure([project(':cli'), + project(':daemon'), + project(':desktop'), project(':monitor'), project(':relay'), project(':seednode'), @@ -166,7 +170,7 @@ configure(project(':common')) { protobuf { protoc { - artifact = "com.google.protobuf:protoc:$protobufVersion" + artifact = "com.google.protobuf:protoc:$protocVersion" } } @@ -175,7 +179,7 @@ configure(project(':common')) { compile "org.openjfx:javafx-base:$javafxVersion:$os" compile "org.openjfx:javafx-graphics:$javafxVersion:$os" compile "com.google.protobuf:protobuf-java:$protobufVersion" - compile 'com.google.code.gson:gson:2.7' + compile 'com.google.code.gson:gson:2.8.5' compile "org.springframework:spring-core:$springVersion" compile "org.slf4j:slf4j-api:$slf4jVersion" compile "ch.qos.logback:logback-core:$logbackVersion" @@ -225,6 +229,8 @@ configure(project(':p2p')) { configure(project(':core')) { + apply plugin: 'com.google.protobuf' + dependencies { compile project(':assets') compile project(':p2p') @@ -250,7 +256,20 @@ configure(project(':core')) { compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") { exclude(module: 'jackson-annotations') } - + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-protobuf:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + compileOnly "javax.annotation:javax.annotation-api:1.2" + runtimeOnly ("io.grpc:grpc-netty-shaded:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } compileOnly "org.projectlombok:lombok:$lombokVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" @@ -262,11 +281,47 @@ configure(project(':core')) { testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" } + protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protocVersion}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } + } + + sourceSets.main.java.srcDirs += [ + 'build/generated/source/proto/main/grpc', + 'build/generated/source/proto/main/java' + ] + test { systemProperty 'jdk.attach.allowAttachSelf', true } } +configure(project(':cli')) { + mainClassName = 'bisq.cli.app.BisqCliMain' + + dependencies { + compile project(':core') + implementation("io.grpc:grpc-core:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' @@ -402,3 +457,14 @@ configure(project(':statsnode')) { annotationProcessor "org.projectlombok:lombok:$lombokVersion" } } + +configure(project(':daemon')) { + mainClassName = 'bisq.daemon.app.BisqDaemonMain' + + dependencies { + compile project(':core') + compileOnly "org.projectlombok:lombok:$lombokVersion" + compileOnly "javax.annotation:javax.annotation-api:1.2" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java new file mode 100644 index 00000000000..754a47fac43 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -0,0 +1,296 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.app; + +import bisq.core.grpc.GetBalanceGrpc; +import bisq.core.grpc.GetBalanceRequest; +import bisq.core.grpc.GetOffersGrpc; +import bisq.core.grpc.GetOffersRequest; +import bisq.core.grpc.GetPaymentAccountsGrpc; +import bisq.core.grpc.GetPaymentAccountsRequest; +import bisq.core.grpc.GetTradeStatisticsGrpc; +import bisq.core.grpc.GetTradeStatisticsRequest; +import bisq.core.grpc.GetVersionGrpc; +import bisq.core.grpc.GetVersionRequest; +import bisq.core.grpc.PlaceOfferGrpc; +import bisq.core.grpc.PlaceOfferRequest; +import bisq.core.grpc.StopServerGrpc; +import bisq.core.grpc.StopServerRequest; +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.StatusRuntimeException; + +import org.bitcoinj.core.Coin; + +import java.time.Clock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +/** + * gRPC client. + * + * FIXME We get warning 'DEBUG io.grpc.netty.shaded.io.netty.util.internal.PlatformDependent0 - direct buffer constructor: unavailable + * java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled' which is + * related to Java 10 changes. Requests are working but we should find out why we get that warning + */ +@Slf4j +public class BisqCliMain { + + private final ManagedChannel channel; + private final GetVersionGrpc.GetVersionBlockingStub getVersionStub; + private final GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub; + private final StopServerGrpc.StopServerBlockingStub stopServerStub; + private final GetTradeStatisticsGrpc.GetTradeStatisticsBlockingStub getTradeStatisticsStub; + private final GetOffersGrpc.GetOffersBlockingStub getOffersStub; + private final GetPaymentAccountsGrpc.GetPaymentAccountsBlockingStub getPaymentAccountsStub; + private final PlaceOfferGrpc.PlaceOfferBlockingStub placeOfferBlockingStub; + private final CorePersistenceProtoResolver corePersistenceProtoResolver; + + public static void main(String[] args) throws Exception { + new BisqCliMain("localhost", 8888); + } + + private BisqCliMain(String host, int port) { + this(ManagedChannelBuilder.forAddress(host, port) + // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid + // needing certificates. + .usePlaintext(true).build()); + + // Simple input scanner + // TODO use some more sophisticated input processing with validation.... + try (Scanner scanner = new Scanner(System.in);) { + while (true) { + long startTs = System.currentTimeMillis(); + + String[] tokens = scanner.nextLine().split(" "); + if (tokens.length == 0) { + return; + } + String command = tokens[0]; + List params = new ArrayList<>(); + if (tokens.length > 1) { + params.addAll(Arrays.asList(tokens)); + params.remove(0); + } + String result = ""; + + switch (command) { + case "getVersion": + result = getVersion(); + break; + case "getBalance": + result = Coin.valueOf(getBalance()).toFriendlyString(); + break; + case "getTradeStatistics": + List tradeStatistics = getTradeStatistics().stream() + .map(bisq.core.trade.statistics.TradeStatistics2::fromProto) + .collect(Collectors.toList()); + + result = tradeStatistics.toString(); + break; + case "getOffers": + List offers = getOffers().stream() + .map(bisq.core.offer.Offer::fromProto) + .collect(Collectors.toList()); + result = offers.toString(); + break; + case "getPaymentAccounts": + List paymentAccounts = getPaymentAccounts().stream() + .map(proto -> PaymentAccount.fromProto(proto, corePersistenceProtoResolver)) + .collect(Collectors.toList()); + result = paymentAccounts.toString(); + break; + case "placeOffer": + // test input: placeOffer CNY BUY 750000000 true -0.2251 1000000 500000 0.15 5a972121-c30a-4b0e-b519-b17b63795d16 + // payment accountId and currency need to be adopted + + // We expect 9 params + // TODO add basic input validation + try { + checkArgument(params.size() == 9); + String currencyCode = params.get(0); + String directionAsString = params.get(1); + long priceAsLong = Long.parseLong(params.get(2)); + boolean useMarketBasedPrice = Boolean.parseBoolean(params.get(3)); + double marketPriceMargin = Double.parseDouble(params.get(4)); + long amountAsLong = Long.parseLong(params.get(5)); + long minAmountAsLong = Long.parseLong(params.get(6)); + double buyerSecurityDeposit = Double.parseDouble(params.get(7)); + String paymentAccountId = params.get(8); + boolean success = placeOffer(currencyCode, + directionAsString, + priceAsLong, + useMarketBasedPrice, + marketPriceMargin, + amountAsLong, + minAmountAsLong, + buyerSecurityDeposit, + paymentAccountId); + result = String.valueOf(success); + break; + } catch (Throwable t) { + log.error(t.toString(), t); + break; + } + case "stop": + result = "Shut down client"; + try { + shutdown(); + } catch (InterruptedException e) { + log.error(e.toString(), e); + } + break; + case "stopServer": + stopServer(); + result = "Server stopped"; + break; + default: + result = format("Unknown command '%s'", command); + } + + // First response is rather slow (300 ms) but following responses are fast (3-5 ms). + log.info("Request took: {} ms", System.currentTimeMillis() - startTs); + log.info(result); + } + } + } + + /** + * Construct client for accessing server using the existing channel. + */ + private BisqCliMain(ManagedChannel channel) { + this.channel = channel; + + getVersionStub = GetVersionGrpc.newBlockingStub(channel); + getBalanceStub = GetBalanceGrpc.newBlockingStub(channel); + getTradeStatisticsStub = GetTradeStatisticsGrpc.newBlockingStub(channel); + getOffersStub = GetOffersGrpc.newBlockingStub(channel); + getPaymentAccountsStub = GetPaymentAccountsGrpc.newBlockingStub(channel); + placeOfferBlockingStub = PlaceOfferGrpc.newBlockingStub(channel); + stopServerStub = StopServerGrpc.newBlockingStub(channel); + + CoreNetworkProtoResolver coreNetworkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone()); + //TODO + corePersistenceProtoResolver = new CorePersistenceProtoResolver(null, coreNetworkProtoResolver, null, null); + } + + private String getVersion() { + GetVersionRequest request = GetVersionRequest.newBuilder().build(); + try { + return getVersionStub.getVersion(request).getVersion(); + } catch (StatusRuntimeException e) { + return "RPC failed: " + e.getStatus(); + } + } + + private long getBalance() { + GetBalanceRequest request = GetBalanceRequest.newBuilder().build(); + try { + return getBalanceStub.getBalance(request).getBalance(); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + return -1; + } + } + + private List getTradeStatistics() { + GetTradeStatisticsRequest request = GetTradeStatisticsRequest.newBuilder().build(); + try { + return getTradeStatisticsStub.getTradeStatistics(request).getTradeStatisticsList(); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + return null; + } + } + + private List getOffers() { + GetOffersRequest request = GetOffersRequest.newBuilder().build(); + try { + return getOffersStub.getOffers(request).getOffersList(); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + return null; + } + } + + private List getPaymentAccounts() { + GetPaymentAccountsRequest request = GetPaymentAccountsRequest.newBuilder().build(); + try { + return getPaymentAccountsStub.getPaymentAccounts(request).getPaymentAccountsList(); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + return null; + } + } + + private boolean placeOffer(String currencyCode, + String directionAsString, + long priceAsLong, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + String paymentAccountId) { + PlaceOfferRequest request = PlaceOfferRequest.newBuilder() + .setCurrencyCode(currencyCode) + .setDirection(directionAsString) + .setPrice(priceAsLong) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setAmount(amountAsLong) + .setMinAmount(minAmountAsLong) + .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setPaymentAccountId(paymentAccountId) + .build(); + try { + return placeOfferBlockingStub.placeOffer(request).getResult(); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + return false; + } + } + + private void stopServer() { + StopServerRequest request = StopServerRequest.newBuilder().build(); + try { + stopServerStub.stopServer(request); + } catch (StatusRuntimeException e) { + log.warn("RPC failed: {}", e.getStatus()); + } + } + + private void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); + System.exit(0); + } +} diff --git a/core/src/main/java/bisq/core/app/AppOptionKeys.java b/core/src/main/java/bisq/core/app/AppOptionKeys.java index 6c44f7d4f81..b18679cbf0b 100644 --- a/core/src/main/java/bisq/core/app/AppOptionKeys.java +++ b/core/src/main/java/bisq/core/app/AppOptionKeys.java @@ -18,8 +18,6 @@ package bisq.core.app; public class AppOptionKeys { - public static final String DESKTOP_WITH_HTTP_API = "desktopWithHttpApi"; - public static final String DESKTOP_WITH_GRPC_API = "desktopWithGrpcApi"; public static final String APP_NAME_KEY = "appName"; public static final String USER_DATA_DIR_KEY = "userDataDir"; public static final String APP_DATA_DIR_KEY = "appDataDir"; diff --git a/core/src/main/java/bisq/core/app/BisqEnvironment.java b/core/src/main/java/bisq/core/app/BisqEnvironment.java index ddebb407ae1..de8decf27dd 100644 --- a/core/src/main/java/bisq/core/app/BisqEnvironment.java +++ b/core/src/main/java/bisq/core/app/BisqEnvironment.java @@ -186,8 +186,6 @@ public static boolean isDaoActivated(Environment environment) { @Setter protected boolean isBitcoinLocalhostNodeRunning; @Getter - protected String desktopWithHttpApi, desktopWithGrpcApi; - @Getter protected List bannedSeedNodes, bannedBtcNodes, bannedPriceRelayNodes; protected final String btcNodes, seedNodes, ignoreDevMsg, useDevPrivilegeKeys, useDevMode, useTorForBtc, rpcUser, rpcPassword, @@ -219,8 +217,6 @@ public BisqEnvironment(PropertySource commandLineProperties) { appDataDir = getProperty(commandLineProperties, AppOptionKeys.APP_DATA_DIR_KEY, appDataDir(userDataDir, appName)); staticAppDataDir = appDataDir; - desktopWithHttpApi = getProperty(commandLineProperties, AppOptionKeys.DESKTOP_WITH_HTTP_API, "false"); - desktopWithGrpcApi = getProperty(commandLineProperties, AppOptionKeys.DESKTOP_WITH_GRPC_API, "false"); ignoreDevMsg = getProperty(commandLineProperties, AppOptionKeys.IGNORE_DEV_MSG_KEY, ""); useDevPrivilegeKeys = getProperty(commandLineProperties, AppOptionKeys.USE_DEV_PRIVILEGE_KEYS, ""); referralId = getProperty(commandLineProperties, AppOptionKeys.REFERRAL_ID, ""); @@ -398,8 +394,6 @@ private PropertySource defaultProperties() { setProperty(NetworkOptionKeys.SEND_MSG_THROTTLE_SLEEP, sendMsgThrottleSleep); setProperty(AppOptionKeys.APP_DATA_DIR_KEY, appDataDir); - setProperty(AppOptionKeys.DESKTOP_WITH_HTTP_API, desktopWithHttpApi); - setProperty(AppOptionKeys.DESKTOP_WITH_GRPC_API, desktopWithGrpcApi); setProperty(AppOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg); setProperty(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS, useDevPrivilegeKeys); setProperty(AppOptionKeys.REFERRAL_ID, referralId); diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 6a8be7955e4..8dd905bc06e 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -446,16 +446,6 @@ protected void customizeOptionParsing(OptionParser parser) { .withRequiredArg() .ofType(boolean.class); - parser.accepts(AppOptionKeys.DESKTOP_WITH_HTTP_API, - format("If set to true Bisq Desktop starts with Http API (default: %s)", "false")) - .withRequiredArg() - .ofType(boolean.class); - - parser.accepts(AppOptionKeys.DESKTOP_WITH_GRPC_API, - format("If set to true Bisq Desktop starts with gRPC API (default: %s)", "false")) - .withRequiredArg() - .ofType(boolean.class); - parser.accepts(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS, format("If that is true all the privileged features which requires a private key " + "to enable it are overridden by a dev key pair (This is for developers only!) (default: %s)", "false")) diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java index 09e9f451088..e3fe211572f 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java @@ -17,8 +17,6 @@ package bisq.core.app; -import bisq.core.CoreModule; - import bisq.common.UserThread; import bisq.common.app.AppModule; import bisq.common.app.Version; diff --git a/core/src/main/java/bisq/core/CoreModule.java b/core/src/main/java/bisq/core/app/CoreModule.java similarity index 98% rename from core/src/main/java/bisq/core/CoreModule.java rename to core/src/main/java/bisq/core/app/CoreModule.java index 4d921e6501e..431dc1555e3 100644 --- a/core/src/main/java/bisq/core/CoreModule.java +++ b/core/src/main/java/bisq/core/app/CoreModule.java @@ -15,11 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.core; +package bisq.core.app; import bisq.core.alert.AlertModule; -import bisq.core.app.AppOptionKeys; -import bisq.core.app.BisqEnvironment; import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; import bisq.core.filter.FilterModule; diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java new file mode 100644 index 00000000000..55db5011175 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -0,0 +1,203 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.grpc; + +import bisq.core.offer.Offer; +import bisq.core.payment.PaymentAccount; +import bisq.core.trade.handlers.TransactionResultHandler; +import bisq.core.trade.statistics.TradeStatistics2; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; + +import java.io.IOException; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +/** + * gRPC server. Gets a instance of BisqFacade passed to access data from the running Bisq instance. + */ +@Slf4j +public class BisqGrpcServer { + + private Server server; + + private static BisqGrpcServer instance; + private static CoreApi coreApi; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Services + /////////////////////////////////////////////////////////////////////////////////////////// + + static class GetVersionImpl extends GetVersionGrpc.GetVersionImplBase { + @Override + public void getVersion(GetVersionRequest req, StreamObserver responseObserver) { + GetVersionReply reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + } + + static class GetBalanceImpl extends GetBalanceGrpc.GetBalanceImplBase { + @Override + public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { + GetBalanceReply reply = GetBalanceReply.newBuilder().setBalance(coreApi.getAvailableBalance()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + } + + static class GetTradeStatisticsImpl extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase { + @Override + public void getTradeStatistics(GetTradeStatisticsRequest req, + StreamObserver responseObserver) { + List tradeStatistics = coreApi.getTradeStatistics().stream() + .map(TradeStatistics2::toProtoTradeStatistics2) + .collect(Collectors.toList()); + GetTradeStatisticsReply reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + } + + static class GetOffersImpl extends GetOffersGrpc.GetOffersImplBase { + @Override + public void getOffers(GetOffersRequest req, StreamObserver responseObserver) { + + List tradeStatistics = coreApi.getOffers().stream() + .map(Offer::toProtoMessage) + .collect(Collectors.toList()); + + GetOffersReply reply = GetOffersReply.newBuilder().addAllOffers(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + } + + static class GetPaymentAccountsImpl extends GetPaymentAccountsGrpc.GetPaymentAccountsImplBase { + @Override + public void getPaymentAccounts(GetPaymentAccountsRequest req, + StreamObserver responseObserver) { + + List tradeStatistics = coreApi.getPaymentAccounts().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList()); + + GetPaymentAccountsReply reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + } + + static class PlaceOfferImpl extends PlaceOfferGrpc.PlaceOfferImplBase { + @Override + public void placeOffer(PlaceOfferRequest req, StreamObserver responseObserver) { + TransactionResultHandler resultHandler = transaction -> { + PlaceOfferReply reply = PlaceOfferReply.newBuilder().setResult(true).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }; + coreApi.placeOffer( + req.getCurrencyCode(), + req.getDirection(), + req.getPrice(), + req.getUseMarketBasedPrice(), + req.getMarketPriceMargin(), + req.getAmount(), + req.getMinAmount(), + req.getBuyerSecurityDeposit(), + req.getPaymentAccountId(), + resultHandler); + } + } + + static class StopServerImpl extends StopServerGrpc.StopServerImplBase { + @Override + public void stopServer(StopServerRequest req, StreamObserver responseObserver) { + StopServerReply reply = StopServerReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + + instance.stop(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public BisqGrpcServer(CoreApi coreApi) { + instance = this; + + BisqGrpcServer.coreApi = coreApi; + + try { + start(); + + } catch (IOException e) { + log.error(e.toString(), e); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void stop() { + if (server != null) { + server.shutdown(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void start() throws IOException { + // TODO add to options + int port = 8888; + + // Config services + server = ServerBuilder.forPort(port) + .addService(new GetVersionImpl()) + .addService(new GetBalanceImpl()) + .addService(new GetTradeStatisticsImpl()) + .addService(new GetOffersImpl()) + .addService(new GetPaymentAccountsImpl()) + .addService(new PlaceOfferImpl()) + .addService(new StopServerImpl()) + .build() + .start(); + + log.info("Server started, listening on " + port); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + // Use stderr here since the logger may have been reset by its JVM shutdown hook. + log.error("*** shutting down gRPC server since JVM is shutting down"); + BisqGrpcServer.this.stop(); + log.error("*** server shut down"); + })); + } +} diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java new file mode 100644 index 00000000000..2877849147f --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -0,0 +1,163 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.grpc; + +import bisq.core.btc.Balances; +import bisq.core.monetary.Price; +import bisq.core.offer.CreateOfferService; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccount; +import bisq.core.presentation.BalancePresentation; +import bisq.core.trade.handlers.TransactionResultHandler; +import bisq.core.trade.statistics.TradeStatistics2; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.User; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +/** + * Provides high level interface to functionality of core Bisq features. + * E.g. useful for different APIs to access data of different domains of Bisq. + */ +@Slf4j +public class CoreApi { + private final Balances balances; + private final BalancePresentation balancePresentation; + private final OfferBookService offerBookService; + private final TradeStatisticsManager tradeStatisticsManager; + private final CreateOfferService createOfferService; + private final OpenOfferManager openOfferManager; + private final User user; + + @Inject + public CoreApi(Balances balances, + BalancePresentation balancePresentation, + OfferBookService offerBookService, + TradeStatisticsManager tradeStatisticsManager, + CreateOfferService createOfferService, + OpenOfferManager openOfferManager, + User user) { + this.balances = balances; + this.balancePresentation = balancePresentation; + this.offerBookService = offerBookService; + this.tradeStatisticsManager = tradeStatisticsManager; + this.createOfferService = createOfferService; + this.openOfferManager = openOfferManager; + this.user = user; + } + + public String getVersion() { + return Version.VERSION; + } + + public long getAvailableBalance() { + return balances.getAvailableBalance().get().getValue(); + } + + public String getAvailableBalanceAsString() { + return balancePresentation.getAvailableBalance().get(); + } + + public List getTradeStatistics() { + return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); + } + + public List getOffers() { + return offerBookService.getOffers(); + } + + public Set getPaymentAccounts() { + return user.getPaymentAccounts(); + } + + public void placeOffer(String currencyCode, + String directionAsString, + long priceAsLong, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + String paymentAccountId, + TransactionResultHandler resultHandler) { + String offerId = createOfferService.getRandomOfferId(); + OfferPayload.Direction direction = OfferPayload.Direction.valueOf(directionAsString); + Price price = Price.valueOf(currencyCode, priceAsLong); + Coin amount = Coin.valueOf(amountAsLong); + Coin minAmount = Coin.valueOf(minAmountAsLong); + PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); + // We don't support atm funding from external wallet to keep it simple + boolean useSavingsWallet = true; + + placeOffer(offerId, + currencyCode, + direction, + price, + useMarketBasedPrice, + marketPriceMargin, + amount, + minAmount, + buyerSecurityDeposit, + paymentAccount, + useSavingsWallet, + resultHandler); + } + + public void placeOffer(String offerId, + String currencyCode, + OfferPayload.Direction direction, + Price price, + boolean useMarketBasedPrice, + double marketPriceMargin, + Coin amount, + Coin minAmount, + double buyerSecurityDeposit, + PaymentAccount paymentAccount, + boolean useSavingsWallet, + TransactionResultHandler resultHandler) { + Offer offer = createOfferService.createAndGetOffer(offerId, + direction, + currencyCode, + amount, + minAmount, + price, + useMarketBasedPrice, + marketPriceMargin, + buyerSecurityDeposit, + paymentAccount); + + openOfferManager.placeOffer(offer, + buyerSecurityDeposit, + useSavingsWallet, + resultHandler, + log::error); + } +} diff --git a/core/src/main/proto/grpc.proto b/core/src/main/proto/grpc.proto new file mode 100644 index 00000000000..33957fb4124 --- /dev/null +++ b/core/src/main/proto/grpc.proto @@ -0,0 +1,148 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +syntax = "proto3"; +package io.bisq.protobuffer; + +// FIXME: IntelliJ does not recognize the import but the compiler does +import "pb.proto"; + +option java_package = "bisq.core.grpc"; +option java_multiple_files = true; + +/////////////////////////////////////////////////////////////////////////////////////////// +// Version +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetVersion { + rpc GetVersion (GetVersionRequest) returns (GetVersionReply) { + } +} + +message GetVersionRequest { +} + +message GetVersionReply { + string version = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// Balance +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetBalance { + rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { + } +} + +message GetBalanceRequest { +} + +message GetBalanceReply { + uint64 balance = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// TradeStatistics +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetTradeStatistics { + rpc GetTradeStatistics (GetTradeStatisticsRequest) returns (GetTradeStatisticsReply) { + } +} + +message GetTradeStatisticsRequest { +} + +// FIXME: IntelliJ does not recognize the imported TradeStatistics2 but the compiler does +message GetTradeStatisticsReply { + repeated TradeStatistics2 TradeStatistics = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// Offer +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetOffers { + rpc GetOffers (GetOffersRequest) returns (GetOffersReply) { + } +} + +message GetOffersRequest { +} + +// FIXME: IntelliJ does not recognize the imported Offer but the compiler does +message GetOffersReply { + repeated Offer offers = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// PaymentAccount +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetPaymentAccounts { + rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { + } +} + +message GetPaymentAccountsRequest { +} + +// FIXME: IntelliJ does not recognize the imported PaymentAccount but the compiler does +message GetPaymentAccountsReply { + repeated PaymentAccount paymentAccounts = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// PlaceOffer +/////////////////////////////////////////////////////////////////////////////////////////// + +service PlaceOffer { + rpc PlaceOffer (PlaceOfferRequest) returns (PlaceOfferReply) { + } +} + +message PlaceOfferRequest { + string currencyCode = 1; + string direction = 2; + uint64 price = 3; + bool useMarketBasedPrice = 4; + double marketPriceMargin = 5; + uint64 amount = 6; + uint64 minAmount = 7; + double buyerSecurityDeposit = 8; + string paymentAccountId = 9; +} + +message PlaceOfferReply { + bool result = 1; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// StopServer +/////////////////////////////////////////////////////////////////////////////////////////// + +service StopServer { + rpc StopServer (StopServerRequest) returns (StopServerReply) { + } +} + +message StopServerRequest { +} + +message StopServerReply { +} diff --git a/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java b/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java new file mode 100644 index 00000000000..f813f84d3ed --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/BisqDaemon.java @@ -0,0 +1,23 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.daemon.app; + +import bisq.core.app.BisqHeadlessApp; + +public class BisqDaemon extends BisqHeadlessApp { +} diff --git a/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java new file mode 100644 index 00000000000..f25d1be3b14 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java @@ -0,0 +1,111 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.daemon.app; + +import bisq.core.app.BisqExecutable; +import bisq.core.app.BisqHeadlessAppMain; +import bisq.core.app.BisqSetup; +import bisq.core.app.CoreModule; +import bisq.core.grpc.BisqGrpcServer; +import bisq.core.grpc.CoreApi; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.setup.CommonSetup; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.BisqSetupListener { + + public static void main(String[] args) throws Exception { + if (BisqExecutable.setupInitialOptionParser(args)) { + // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. + // In order to work around a bug in JavaFX 8u25 and below, you must include the following code as the first line of your realMain method: + Thread.currentThread().setContextClassLoader(BisqDaemonMain.class.getClassLoader()); + + new BisqDaemonMain().execute(args); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // First synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void configUserThread() { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(this.getClass().getSimpleName()) + .setDaemon(true) + .build(); + UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); + } + + @Override + protected void launchApplication() { + headlessApp = new BisqDaemon(); + CommonSetup.setup(BisqDaemonMain.this.headlessApp); + + UserThread.execute(this::onApplicationLaunched); + } + + @Override + protected void onApplicationLaunched() { + super.onApplicationLaunched(); + headlessApp.setGracefulShutDownHandler(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // We continue with a series of synchronous execution tasks + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected AppModule getModule() { + return new CoreModule(bisqEnvironment); + } + + @Override + protected void applyInjector() { + super.applyInjector(); + + headlessApp.setInjector(injector); + } + + @Override + protected void startApplication() { + // We need to be in user thread! We mapped at launchApplication already... + headlessApp.startApplication(); + + // In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted + onApplicationStarted(); + } + + @Override + protected void onApplicationStarted() { + super.onApplicationStarted(); + + CoreApi coreApi = injector.getInstance(CoreApi.class); + new BisqGrpcServer(coreApi); + } +} diff --git a/daemon/src/main/java/resources/logback.xml b/daemon/src/main/java/resources/logback.xml new file mode 100644 index 00000000000..ac5e6444ea0 --- /dev/null +++ b/daemon/src/main/java/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java index a2494dfbdb1..7f63a3ef696 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -44,11 +44,6 @@ public BisqAppMain() { super("Bisq Desktop", "bisq-desktop", Version.VERSION); } - /* @Nullable - private BisqHttpApiServer bisqHttpApiServer;*/ - /* @Nullable - private BisqGrpcServer bisqGrpcServer; - */ public static void main(String[] args) throws Exception { if (BisqExecutable.setupInitialOptionParser(args)) { // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. @@ -135,20 +130,11 @@ protected void startApplication() { protected void onApplicationStarted() { super.onApplicationStarted(); - /* if (runWithHttpApi()) { - bisqHttpApiServer = new BisqHttpApiServer(); - }*/ /* if (runWithGrpcApi()) { - bisqGrpcServer = new BisqGrpcServer(); - }*/ - } - - private boolean runWithHttpApi() { - return bisqEnvironment.getDesktopWithHttpApi().toLowerCase().equals("true"); - } - - private boolean runWithGrpcApi() { - return bisqEnvironment.getDesktopWithGrpcApi().toLowerCase().equals("true"); + CoreApi coreApi = injector.getInstance(CoreApi.class); + bisqGrpcServer = new BisqGrpcServer(coreApi); + } + */ } } diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java b/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java index 75c389288e3..942c49a718b 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppModule.java @@ -19,7 +19,7 @@ import bisq.desktop.DesktopModule; -import bisq.core.CoreModule; +import bisq.core.app.CoreModule; import bisq.common.app.AppModule; diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 19d1f00cfb4..9be698d7e9e 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -11,7 +11,6 @@ // // See https://github.com/signalapp/gradle-witness#using-witness for further details. - dependencyVerification { verify = [ 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', @@ -32,11 +31,14 @@ dependencyVerification { 'com.github.bisq-network.bitcoinj:bitcoinj-core:f979c2187e61ee3b08dd4cbfc49a149734cff64c045d29ed112f2e12f34068a3', 'com.github.ravn:jsocks:3c71600af027b2b6d4244e4ad14d98ff2352a379410daebefff5d8cd48d742a4', 'com.github.sarxos:webcam-capture:d960b7ea8ec3ddf2df0725ef214c3fccc9699ea7772df37f544e1f8e4fd665f6', + 'com.google.android:annotations:ba734e1e84c09d615af6a09d33034b4f0442f8772dec120efb376d86a565ae15', + 'com.google.api.grpc:proto-google-common-protos:bd60cd7a423b00fb824c27bdd0293aaf4781be1daba6ed256311103fb4b84108', 'com.google.code.findbugs:jsr305:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7', - 'com.google.code.gson:gson:2d43eb5ea9e133d2ee2405cc14f5ee08951b8361302fdd93494a3a997b508d32', + 'com.google.code.gson:gson:233a0149fc365c9f6edbd683cfe266b19bdc773be98eabdaf6b3c924b48e7d81', + 'com.google.errorprone:error_prone_annotations:ec59f1b702d9afc09e8c3929f5c42777dec623a6ea2731ac694332c7d7680f5a', 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:d258ff1bd9b8b527872f8402648226658ad3149f1f40e74b0566d69e7e042fbc', - 'com.google.protobuf:protobuf-java:5a1e5c225791eccd3d67a598922e637406190c90155fb97f38e4eab29719324d', + 'com.google.protobuf:protobuf-java:161d7d61a8cb3970891c299578702fd079646e032329d6c2cabf998d191437c9', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', @@ -53,6 +55,16 @@ dependencyVerification { 'de.jensd:fontawesomefx-materialdesignfont:dbad8dfdd1c85e298d5bbae25b2399aec9e85064db57b2427d10f3815aa98752', 'de.jensd:fontawesomefx:73bacc991a0a6f5cf0f911767c8db161e0949dbca61e8371eb4342e3da96887b', 'io.github.microutils:kotlin-logging:4992504fd3c6ecdf9ed10874b9508e758bb908af9e9d7af19a61e9afb6b7e27a', + 'io.grpc:grpc-api:a269094009588213ab5386a6fb92426b8056a130b2653d3b4e59e971f2f1ef08', + 'io.grpc:grpc-context:f4c8f878c320f6fb56c1c14692618f6df8253314b556176e32727afbc5921a73', + 'io.grpc:grpc-core:d67fa113fd9cc45a02710f9c41dda9c15191448c14e9e96fcc21839a41345d4c', + 'io.grpc:grpc-netty-shaded:9edfd45da473d2efbb5683fc3eaf1857e82d2148033d82dd558a7ac38731ea33', + 'io.grpc:grpc-protobuf-lite:9ba9aaa3e6997a04c707793c25e3ec88c6bad86f8d6f6b8b7a1a0c33ea2429d8', + 'io.grpc:grpc-protobuf:454dae7e246dac25526ed5b795d97a5dafedd3cc2042cfc810f02051d7d3e3cb', + 'io.grpc:grpc-stub:1532e291c0e9fd8230a6416c8ebbd902d99c7e2760241ae638ea761aa3dd5f43', + 'io.opencensus:opencensus-api:8e2cb0f6391d8eb0a1bcd01e7748883f0033b1941754f4ed3f19d2c3e4276fc8', + 'io.opencensus:opencensus-contrib-grpc-metrics:29fc79401082301542cab89d7054d2f0825f184492654c950020553ef4ff0ef8', + 'io.perfmark:perfmark-api:b734ba2149712409a44eabdb799f64768578fee0defe1418bb108fe32ea43e1a', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'net.glxn:qrgen:c85d9d8512d91e8ad11fe56259a7825bd50ce0245447e236cf168d1b17591882', 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', diff --git a/settings.gradle b/settings.gradle index 500b2803c47..b12eb486d4d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,8 @@ include 'assets' include 'common' include 'p2p' include 'core' +include 'cli' +include 'daemon' include 'desktop' include 'monitor' include 'pricenode'