Add API functions to initialize Haveno account (#216)

Co-authored-by: woodser@protonmail.com
This commit is contained in:
duriancrepe 2022-02-09 01:39:57 -08:00 committed by GitHub
parent dc4692d97a
commit e3b9a9962b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 2755 additions and 1660 deletions

View file

@ -0,0 +1,64 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.app;
import java.util.concurrent.*;
/**
* A cancellable console input reader.
* Derived from https://www.javaspecialists.eu/archive/Issue153-Timeout-on-Console-Input.html
*/
public class ConsoleInput {
private final int tries;
private final int timeout;
private final TimeUnit unit;
private Future<String> future;
public ConsoleInput(int tries, int timeout, TimeUnit unit) {
this.tries = tries;
this.timeout = timeout;
this.unit = unit;
}
public void cancel() {
if (future != null)
future.cancel(true);
}
public String readLine() throws InterruptedException {
ExecutorService ex = Executors.newSingleThreadExecutor();
String input = null;
try {
for (int i = 0; i < tries; i++) {
future = ex.submit(new ConsoleInputReadTask());
try {
input = future.get(timeout, unit);
break;
} catch (ExecutionException e) {
e.getCause().printStackTrace();
} catch (TimeoutException e) {
future.cancel(true);
} finally {
future = null;
}
}
} finally {
ex.shutdownNow();
}
return input;
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.app;
import java.io.*;
import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConsoleInputReadTask implements Callable<String> {
public String call() throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
log.debug("ConsoleInputReadTask run() called.");
String input;
do {
try {
// wait until we have data to complete a readLine()
while (!br.ready()) {
Thread.sleep(100);
}
// readline will always block until an input exists.
input = br.readLine();
} catch (InterruptedException e) {
log.debug("ConsoleInputReadTask() cancelled");
return null;
}
} while ("".equals(input));
return input;
}
}

View file

@ -19,21 +19,24 @@ package bisq.daemon.app;
import bisq.core.app.HavenoHeadlessAppMain;
import bisq.core.app.HavenoSetup;
import bisq.core.api.AccountServiceListener;
import bisq.core.app.CoreModule;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.crypto.IncorrectPasswordException;
import bisq.common.handlers.ResultHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.Console;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import bisq.daemon.grpc.GrpcServer;
@Slf4j
@ -61,7 +64,6 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
@Override
protected void launchApplication() {
headlessApp = new HavenoDaemon();
UserThread.execute(this::onApplicationLaunched);
}
@ -101,15 +103,116 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
@Override
protected void onApplicationStarted() {
super.onApplicationStarted();
grpcServer = injector.getInstance(GrpcServer.class);
grpcServer.start();
}
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
super.gracefulShutDown(resultHandler);
if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early
}
grpcServer.shutdown();
/**
* Start the grpcServer to allow logging in remotely.
*/
@Override
protected boolean loginAccount() {
boolean opened = super.loginAccount();
// Start rpc server in case login is coming in from rpc
grpcServer = injector.getInstance(GrpcServer.class);
grpcServer.start();
if (!opened) {
// Nonblocking, we need to stop if the login occurred through rpc.
// TODO: add a mode to mask password
ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
Thread t = new Thread(() -> {
interactiveLogin(reader);
});
t.start();
// Handle asynchronous account opens.
// Will need to also close and reopen account.
AccountServiceListener accountListener = new AccountServiceListener() {
@Override public void onAccountCreated() { onLogin(); }
@Override public void onAccountOpened() { onLogin(); }
private void onLogin() {
log.info("Logged in successfully");
reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread
}
};
accountService.addListener(accountListener);
try {
// Wait until interactive login or rpc. Check one more time if account is open to close race condition.
if (!accountService.isAccountOpen()) {
log.info("Interactive login required");
t.join();
}
} catch (InterruptedException e) {
// expected
}
accountService.removeListener(accountListener);
opened = accountService.isAccountOpen();
}
return opened;
}
/**
* Asks user for login. TODO: Implement in the desktop app.
* @return True if user logged in interactively.
*/
protected boolean interactiveLogin(ConsoleInput reader) {
Console console = System.console();
if (console == null) {
// The ConsoleInput class reads from system.in, can wait for input without a console.
log.info("No console available, account must be opened through rpc");
try {
// If user logs in through rpc, the reader will be interrupted through the event.
reader.readLine();
} catch (InterruptedException | CancellationException ex) {
log.info("Reader interrupted, continuing startup");
}
return false;
}
String openedOrCreated = "Account unlocked\n";
boolean accountExists = accountService.accountExists();
while (!accountService.isAccountOpen()) {
try {
if (accountExists) {
try {
// readPassword will not return until the user inputs something
// which is not suitable if we are waiting for rpc call which
// could login the account. Must be able to interrupt the read.
//new String(console.readPassword("Password:"));
System.out.printf("Password:\n");
String password = reader.readLine();
accountService.openAccount(password);
} catch (IncorrectPasswordException ipe) {
System.out.printf("Incorrect password\n");
}
} else {
System.out.printf("Creating a new account\n");
System.out.printf("Password:\n");
String password = reader.readLine();
System.out.printf("Confirm:\n");
String passwordConfirm = reader.readLine();
if (password.equals(passwordConfirm)) {
accountService.createAccount(password);
openedOrCreated = "Account created\n";
} else {
System.out.printf("Passwords did not match\n");
}
}
} catch (Exception ex) {
log.debug(ex.getMessage());
return false;
}
}
System.out.printf(openedOrCreated);
return true;
}
}

View file

@ -0,0 +1,286 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.AccountGrpc.getAccountExistsMethod;
import static bisq.proto.grpc.AccountGrpc.getBackupAccountMethod;
import static bisq.proto.grpc.AccountGrpc.getChangePasswordMethod;
import static bisq.proto.grpc.AccountGrpc.getCloseAccountMethod;
import static bisq.proto.grpc.AccountGrpc.getCreateAccountMethod;
import static bisq.proto.grpc.AccountGrpc.getDeleteAccountMethod;
import static bisq.proto.grpc.AccountGrpc.getIsAccountOpenMethod;
import static bisq.proto.grpc.AccountGrpc.getOpenAccountMethod;
import static bisq.proto.grpc.AccountGrpc.getRestoreAccountMethod;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.common.crypto.IncorrectPasswordException;
import bisq.core.api.CoreApi;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
import bisq.proto.grpc.AccountExistsReply;
import bisq.proto.grpc.AccountExistsRequest;
import bisq.proto.grpc.AccountGrpc.AccountImplBase;
import bisq.proto.grpc.BackupAccountReply;
import bisq.proto.grpc.BackupAccountRequest;
import bisq.proto.grpc.ChangePasswordReply;
import bisq.proto.grpc.ChangePasswordRequest;
import bisq.proto.grpc.CloseAccountReply;
import bisq.proto.grpc.CloseAccountRequest;
import bisq.proto.grpc.CreateAccountReply;
import bisq.proto.grpc.CreateAccountRequest;
import bisq.proto.grpc.DeleteAccountReply;
import bisq.proto.grpc.DeleteAccountRequest;
import bisq.proto.grpc.IsAccountOpenReply;
import bisq.proto.grpc.IsAccountOpenRequest;
import bisq.proto.grpc.IsAppInitializedReply;
import bisq.proto.grpc.IsAppInitializedRequest;
import bisq.proto.grpc.OpenAccountReply;
import bisq.proto.grpc.OpenAccountRequest;
import bisq.proto.grpc.RestoreAccountReply;
import bisq.proto.grpc.RestoreAccountRequest;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Optional;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
@VisibleForTesting
@Slf4j
public class GrpcAccountService extends AccountImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
private ByteArrayOutputStream restoreStream; // in memory stream for restoring account
@Inject
public GrpcAccountService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void accountExists(AccountExistsRequest req, StreamObserver<AccountExistsReply> responseObserver) {
try {
var reply = AccountExistsReply.newBuilder()
.setAccountExists(coreApi.accountExists())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void isAccountOpen(IsAccountOpenRequest req, StreamObserver<IsAccountOpenReply> responseObserver) {
try {
var reply = IsAccountOpenReply.newBuilder()
.setIsAccountOpen(coreApi.isAccountOpen())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void createAccount(CreateAccountRequest req, StreamObserver<CreateAccountReply> responseObserver) {
try {
coreApi.createAccount(req.getPassword());
var reply = CreateAccountReply.newBuilder()
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void openAccount(OpenAccountRequest req, StreamObserver<OpenAccountReply> responseObserver) {
try {
coreApi.openAccount(req.getPassword());
var reply = OpenAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
if (cause instanceof IncorrectPasswordException) cause = new IllegalStateException(cause);
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void isAppInitialized(IsAppInitializedRequest req, StreamObserver<IsAppInitializedReply> responseObserver) {
try {
var reply = IsAppInitializedReply.newBuilder().setIsAppInitialized(coreApi.isAppInitialized()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void changePassword(ChangePasswordRequest req, StreamObserver<ChangePasswordReply> responseObserver) {
try {
coreApi.changePassword(req.getPassword());
var reply = ChangePasswordReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void closeAccount(CloseAccountRequest req, StreamObserver<CloseAccountReply> responseObserver) {
try {
coreApi.closeAccount();
var reply = CloseAccountReply.newBuilder()
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void deleteAccount(DeleteAccountRequest req, StreamObserver<DeleteAccountReply> responseObserver) {
try {
coreApi.deleteAccount(() -> {
var reply = DeleteAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted(); // reply after shutdown
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void backupAccount(BackupAccountRequest req, StreamObserver<BackupAccountReply> responseObserver) {
// Send in large chunks to reduce unnecessary overhead. Typical backup will not be more than a few MB.
// From current testing it appears that client gRPC-web is slow in processing the bytes on download.
try {
int bufferSize = 1024 * 1024 * 8;
coreApi.backupAccount(bufferSize, (stream) -> {
try {
log.info("Sending bytes in chunks of: " + bufferSize);
byte[] buffer = new byte[bufferSize];
int length;
int total = 0;
while ((length = stream.read(buffer, 0, bufferSize)) != -1) {
total += length;
var reply = BackupAccountReply.newBuilder()
.setZipBytes(ByteString.copyFrom(buffer, 0, length))
.build();
responseObserver.onNext(reply);
}
log.info("Completed backup account total sent: " + total);
stream.close();
responseObserver.onCompleted();
} catch (Exception ex) {
exceptionHandler.handleException(log, ex, responseObserver);
}
}, (ex) -> exceptionHandler.handleException(log, ex, responseObserver));
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void restoreAccount(RestoreAccountRequest req, StreamObserver<RestoreAccountReply> responseObserver) {
try {
// Fail fast since uploading and processing bytes takes resources.
if (coreApi.accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
// If the entire zip is in memory, no need to write to disk.
// Restore the account directly from the zip stream.
if (!req.getHasMore() && req.getOffset() == 0) {
var inputStream = req.getZipBytes().newInput();
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
var reply = RestoreAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted(); // reply after shutdown
});
} else {
if (req.getOffset() == 0) {
log.info("RestoreAccount starting new chunked zip");
restoreStream = new ByteArrayOutputStream((int) req.getTotalLength());
}
if (restoreStream.size() != req.getOffset()) {
log.warn("Stream offset doesn't match current position");
IllegalStateException cause = new IllegalStateException("Stream offset doesn't match current position");
exceptionHandler.handleException(log, cause, responseObserver);
} else {
log.info("RestoreAccount writing chunk size " + req.getZipBytes().size());
req.getZipBytes().writeTo(restoreStream);
}
if (!req.getHasMore()) {
var inputStream = new ByteArrayInputStream(restoreStream.toByteArray());
restoreStream.close();
restoreStream = null;
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
var reply = RestoreAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted(); // reply after shutdown
});
} else {
var reply = RestoreAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
put(getAccountExistsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getBackupAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
put(getChangePasswordMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getCloseAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getCreateAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getDeleteAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getIsAccountOpenMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getOpenAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getRestoreAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
}}
)));
}
}

View file

@ -42,9 +42,9 @@ import bisq.proto.grpc.StartCheckingConnectionsReply;
import bisq.proto.grpc.StartCheckingConnectionsRequest;
import bisq.proto.grpc.StopCheckingConnectionsReply;
import bisq.proto.grpc.StopCheckingConnectionsRequest;
import bisq.proto.grpc.UriConnection;
import java.net.URI;
import java.net.URISyntaxException;
import bisq.proto.grpc.UrlConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
@ -84,7 +84,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
public void removeConnection(RemoveConnectionRequest request,
StreamObserver<RemoveConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.removeMoneroConnection(validateUri(request.getUri()));
coreApi.removeMoneroConnection(validateUri(request.getUrl()));
return RemoveConnectionReply.newBuilder().build();
});
}
@ -93,7 +93,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
public void getConnection(GetConnectionRequest request,
StreamObserver<GetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
UriConnection replyConnection = toUriConnection(coreApi.getMoneroConnection());
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection());
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
@ -107,8 +107,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
StreamObserver<GetConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
List<MoneroRpcConnection> connections = coreApi.getMoneroConnections();
List<UriConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
List<UrlConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
return GetConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
});
}
@ -117,8 +117,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
public void setConnection(SetConnectionRequest request,
StreamObserver<SetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
if (request.getUri() != null && !request.getUri().isEmpty())
coreApi.setMoneroConnection(validateUri(request.getUri()));
if (request.getUrl() != null && !request.getUrl().isEmpty())
coreApi.setMoneroConnection(validateUri(request.getUrl()));
else if (request.hasConnection())
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
@ -131,7 +131,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
StreamObserver<CheckConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
UriConnection replyConnection = toUriConnection(connection);
UrlConnection replyConnection = toUrlConnection(connection);
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
@ -145,8 +145,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
StreamObserver<CheckConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
List<MoneroRpcConnection> connections = coreApi.checkMoneroConnections();
List<UriConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
List<UrlConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
return CheckConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
});
}
@ -176,7 +176,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
UriConnection replyConnection = toUriConnection(connection);
UrlConnection replyConnection = toUrlConnection(connection);
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
@ -211,43 +211,40 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
}
private static UriConnection toUriConnection(MoneroRpcConnection rpcConnection) {
private static UrlConnection toUrlConnection(MoneroRpcConnection rpcConnection) {
if (rpcConnection == null) return null;
return UriConnection.newBuilder()
.setUri(rpcConnection.getUri())
return UrlConnection.newBuilder()
.setUrl(rpcConnection.getUri())
.setPriority(rpcConnection.getPriority())
.setOnlineStatus(toOnlineStatus(rpcConnection.isOnline()))
.setAuthenticationStatus(toAuthenticationStatus(rpcConnection.isAuthenticated()))
.build();
}
private static UriConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
if (authenticated == null) return UriConnection.AuthenticationStatus.NO_AUTHENTICATION;
else if (authenticated) return UriConnection.AuthenticationStatus.AUTHENTICATED;
else return UriConnection.AuthenticationStatus.NOT_AUTHENTICATED;
private static UrlConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
if (authenticated == null) return UrlConnection.AuthenticationStatus.NO_AUTHENTICATION;
else if (authenticated) return UrlConnection.AuthenticationStatus.AUTHENTICATED;
else return UrlConnection.AuthenticationStatus.NOT_AUTHENTICATED;
}
private static UriConnection.OnlineStatus toOnlineStatus(Boolean online) {
if (online == null) return UriConnection.OnlineStatus.UNKNOWN;
else if (online) return UriConnection.OnlineStatus.ONLINE;
else return UriConnection.OnlineStatus.OFFLINE;
private static UrlConnection.OnlineStatus toOnlineStatus(Boolean online) {
if (online == null) return UrlConnection.OnlineStatus.UNKNOWN;
else if (online) return UrlConnection.OnlineStatus.ONLINE;
else return UrlConnection.OnlineStatus.OFFLINE;
}
private static MoneroRpcConnection toMoneroRpcConnection(UriConnection uriConnection) throws URISyntaxException {
private static MoneroRpcConnection toMoneroRpcConnection(UrlConnection uriConnection) throws MalformedURLException {
if (uriConnection == null) return null;
return new MoneroRpcConnection(
validateUri(uriConnection.getUri()),
validateUri(uriConnection.getUrl()),
nullIfEmpty(uriConnection.getUsername()),
nullIfEmpty(uriConnection.getPassword()))
.setPriority(uriConnection.getPriority());
}
private static String validateUri(String uri) throws URISyntaxException {
if (uri.isEmpty()) {
throw new IllegalArgumentException("URI is required");
}
// Create new URI for validation, internally String is used again
return new URI(uri).toString();
private static String validateUri(String url) throws MalformedURLException {
if (url.isEmpty()) throw new IllegalArgumentException("URL is required");
return new URL(url).toString(); // validate and return
}
private static String nullIfEmpty(String value) {

View file

@ -49,6 +49,7 @@ public class GrpcServer {
public GrpcServer(CoreContext coreContext,
Config config,
PasswordAuthInterceptor passwordAuthInterceptor,
GrpcAccountService accountService,
GrpcDisputeAgentsService disputeAgentsService,
GrpcHelpService helpService,
GrpcOffersService offersService,
@ -63,6 +64,7 @@ public class GrpcServer {
GrpcMoneroConnectionsService moneroConnectionsService) {
this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor())
.addService(interceptForward(accountService, accountService.interceptors()))
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
.addService(interceptForward(helpService, helpService.interceptors()))
.addService(interceptForward(offersService, offersService.interceptors()))

View file

@ -48,9 +48,14 @@ class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase {
StreamObserver<StopReply> responseObserver) {
try {
log.info("Shutdown request received.");
var reply = StopReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
HavenoHeadlessApp.setOnGracefulShutDownHandler(new Runnable() {
@Override
public void run() {
var reply = StopReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
});
UserThread.runAfter(HavenoHeadlessApp.getShutDownHandler(), 500, MILLISECONDS);
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);