Skip to content

Commit

Permalink
Refactor grpc wallet service
Browse files Browse the repository at this point in the history
Previously, each wallet-related method was implemented with its own grpc
service. There is no need to do this, as a grpc service may declare
multiple rpc methods. This commit refactors everything wallet-related
into a single GrpcWalletService and also extracts a CoreWalletService
from CoreApi in order to avoid the latter becoming overly large.
Ideally, there would be no need for an abstraction in bisq.grpc called
CoreWalletService; we would ideally use such a service implemented in
bisq.core. The closest we have is WalletsManager, but it is not designed
to be used the way we are using it here in the grpc context. Rather than
making changes directly to core (which can be very risky), we will
rather make them here in this layer, designing exactly the "core wallet
service" we need, and can then later see about folding it into the
actual core.
  • Loading branch information
cbeams committed May 2, 2020
1 parent 2a9d1f6 commit 39c868f
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 315 deletions.
27 changes: 10 additions & 17 deletions cli/src/main/java/bisq/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,14 @@

package bisq.cli;

import bisq.proto.grpc.GetBalanceGrpc;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.LockWalletGrpc;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RemoveWalletPasswordGrpc;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SetWalletPasswordGrpc;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletGrpc;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.WalletGrpc;

import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
Expand Down Expand Up @@ -139,26 +135,26 @@ public static void main(String[] args) {
}
}));

var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var walletService = WalletGrpc.newBlockingStub(channel).withCallCredentials(credentials);

try {
switch (method) {
case getversion: {
var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = GetVersionRequest.newBuilder().build();
var version = stub.getVersion(request).getVersion();
var version = versionService.getVersion(request).getVersion();
out.println(version);
exit(EXIT_SUCCESS);
}
case getbalance: {
var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = GetBalanceRequest.newBuilder().build();
var reply = stub.getBalance(request);
var reply = walletService.getBalance(request);
out.println(formatBalance(reply.getBalance()));
exit(EXIT_SUCCESS);
}
case lockwallet: {
var stub = LockWalletGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = LockWalletRequest.newBuilder().build();
stub.lockWallet(request);
walletService.lockWallet(request);
out.println("wallet locked");
exit(EXIT_SUCCESS);
}
Expand All @@ -178,11 +174,10 @@ public static void main(String[] args) {
err.println(nonOptionArgs.get(2) + " is not a number");
exit(EXIT_FAILURE);
}
var stub = UnlockWalletGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = UnlockWalletRequest.newBuilder()
.setPassword(nonOptionArgs.get(1))
.setTimeout(timeout).build();
stub.unlockWallet(request);
walletService.unlockWallet(request);
out.println("wallet unlocked");
exit(EXIT_SUCCESS);
}
Expand All @@ -191,9 +186,8 @@ public static void main(String[] args) {
err.println("Error: no \"password\" specified");
exit(EXIT_FAILURE);
}
var stub = RemoveWalletPasswordGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = RemoveWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build();
stub.removeWalletPassword(request);
walletService.removeWalletPassword(request);
out.println("wallet decrypted");
exit(EXIT_SUCCESS);
}
Expand All @@ -202,11 +196,10 @@ public static void main(String[] args) {
err.println("Error: no \"password\" specified");
exit(EXIT_FAILURE);
}
var stub = SetWalletPasswordGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = (nonOptionArgs.size() == 3)
? SetWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).setNewPassword(nonOptionArgs.get(2)).build()
: SetWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build();
stub.setWalletPassword(request);
walletService.setWalletPassword(request);
out.println("wallet encrypted" + (nonOptionArgs.size() == 2 ? "" : " with new password"));
exit(EXIT_SUCCESS);
}
Expand Down
134 changes: 1 addition & 133 deletions core/src/main/java/bisq/core/grpc/CoreApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

package bisq.core.grpc;

import bisq.core.btc.Balances;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer;
Expand All @@ -32,84 +30,46 @@
import bisq.core.user.User;

import bisq.common.app.Version;
import bisq.common.util.Tuple2;

import org.bitcoinj.core.Coin;
import org.bitcoinj.crypto.KeyCrypterScrypt;

import javax.inject.Inject;

import org.spongycastle.crypto.params.KeyParameter;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;

import static bisq.core.grpc.ApiStatus.*;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
* 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 OfferBookService offerBookService;
private final TradeStatisticsManager tradeStatisticsManager;
private final CreateOfferService createOfferService;
private final OpenOfferManager openOfferManager;
private final WalletsManager walletsManager;
private final User user;

@Nullable
private String tempLockWalletPassword;

@Inject
public CoreApi(Balances balances,
OfferBookService offerBookService,
public CoreApi(OfferBookService offerBookService,
TradeStatisticsManager tradeStatisticsManager,
CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
WalletsManager walletsManager,
User user) {
this.balances = balances;
this.offerBookService = offerBookService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.createOfferService = createOfferService;
this.openOfferManager = openOfferManager;
this.walletsManager = walletsManager;
this.user = user;
}

public String getVersion() {
return Version.VERSION;
}

public Tuple2<Long, ApiStatus> getAvailableBalance() {
if (!walletsManager.areWalletsAvailable())
return new Tuple2<>(-1L, WALLET_NOT_AVAILABLE);

if (walletsManager.areWalletsEncrypted())
return new Tuple2<>(-1L, WALLET_IS_ENCRYPTED_WITH_UNLOCK_INSTRUCTION);

try {
long balance = balances.getAvailableBalance().get().getValue();
return new Tuple2<>(balance, OK);
} catch (Throwable t) {
// TODO Derive new ApiStatus codes from server stack traces.
t.printStackTrace();
// TODO Fix bug causing NPE thrown by getAvailableBalance().
return new Tuple2<>(-1L, INTERNAL);
}
}

public List<TradeStatistics2> getTradeStatistics() {
return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
}
Expand Down Expand Up @@ -185,96 +145,4 @@ public void placeOffer(String offerId,
log::error);
}

// Provided for automated wallet protection method testing, despite the
// security risks exposed by providing users the ability to decrypt their wallets.
public Tuple2<Boolean, ApiStatus> removeWalletPassword(String password) {
if (!walletsManager.areWalletsAvailable())
return new Tuple2<>(false, WALLET_NOT_AVAILABLE);

if (!walletsManager.areWalletsEncrypted())
return new Tuple2<>(false, WALLET_NOT_ENCRYPTED);

KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
if (keyCrypterScrypt == null)
return new Tuple2<>(false, WALLET_ENCRYPTER_NOT_AVAILABLE);

KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
if (!walletsManager.checkAESKey(aesKey))
return new Tuple2<>(false, INCORRECT_WALLET_PASSWORD);

walletsManager.decryptWallets(aesKey);
return new Tuple2<>(true, OK);
}

public Tuple2<Boolean, ApiStatus> setWalletPassword(String password, String newPassword) {
try {
if (!walletsManager.areWalletsAvailable())
return new Tuple2<>(false, WALLET_NOT_AVAILABLE);

KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
if (keyCrypterScrypt == null)
return new Tuple2<>(false, WALLET_ENCRYPTER_NOT_AVAILABLE);

if (newPassword != null && !newPassword.isEmpty()) {
// TODO Validate new password before replacing old password.
if (!walletsManager.areWalletsEncrypted())
return new Tuple2<>(false, WALLET_NOT_ENCRYPTED);

KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
if (!walletsManager.checkAESKey(aesKey))
return new Tuple2<>(false, INCORRECT_OLD_WALLET_PASSWORD);

walletsManager.decryptWallets(aesKey);
aesKey = keyCrypterScrypt.deriveKey(newPassword);
walletsManager.encryptWallets(keyCrypterScrypt, aesKey);
return new Tuple2<>(true, OK);
}

if (walletsManager.areWalletsEncrypted())
return new Tuple2<>(false, WALLET_IS_ENCRYPTED);

// TODO Validate new password.
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
walletsManager.encryptWallets(keyCrypterScrypt, aesKey);
return new Tuple2<>(true, OK);
} catch (Throwable t) {
// TODO Derive new ApiStatus codes from server stack traces.
t.printStackTrace();
return new Tuple2<>(false, INTERNAL);
}
}

public Tuple2<Boolean, ApiStatus> lockWallet() {
if (tempLockWalletPassword != null) {
Tuple2<Boolean, ApiStatus> encrypted = setWalletPassword(tempLockWalletPassword, null);
tempLockWalletPassword = null;
if (!encrypted.second.equals(OK))
return encrypted;

return new Tuple2<>(true, OK);
}
return new Tuple2<>(false, WALLET_ALREADY_LOCKED);
}

public Tuple2<Boolean, ApiStatus> unlockWallet(String password, long timeout) {
Tuple2<Boolean, ApiStatus> decrypted = removeWalletPassword(password);
if (!decrypted.second.equals(OK))
return decrypted;

TimerTask timerTask = new TimerTask() {
@Override
public void run() {
log.info("Locking wallet");
setWalletPassword(password, null);
tempLockWalletPassword = null;
}
};
Timer timer = new Timer("Lock Wallet Timer");
timer.schedule(timerTask, SECONDS.toMillis(timeout));

// Cache wallet password for timeout (secs), in case
// user wants to lock the wallet for timeout expires.
tempLockWalletPassword = password;
return new Tuple2<>(true, OK);
}
}
Loading

0 comments on commit 39c868f

Please sign in to comment.