Skip to content

Commit

Permalink
Add rpc method 'getfundingaddresses'
Browse files Browse the repository at this point in the history
This addresses task bisq-network#1 in issue bisq-network#4257.

This new gRPC WalletService method displays the BTC wallet's list of
receiving addresses.  The balance and number of confirmations
for the most recent transaction is displayed to the right of each
address.  Instead of returning a gRPC data structure to the client,
the service method returns a formatted String.

If the BTC wallet has no unused addresses, one will be created and
included in the returned list, and it can be used to fund the wallet.

The new method required injection of the BtcWalletService into CoreWalletsService,
and the usual boilerplate changes to grpc.proto, CliMain, and GrpcWalletService.

Some of the next PRs (for bisq-network#4257) will require some common functionality within
CoreWalletsService, so these additional changes were included:

  * a private, class level formatSatoshis function
  * a public getNumConfirmationsForMostRecentTransaction method
  * a public getAddressBalance method
  * a private getAddressEntry method

A unit test that verifies a successful return status was added to cli/test.sh.
  • Loading branch information
ghubstan authored and eigentsmis committed Jun 26, 2020
1 parent 5415eed commit 6ec18a5
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 2 deletions.
10 changes: 9 additions & 1 deletion cli/src/main/java/bisq/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package bisq.cli;

import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.LockWalletRequest;
Expand Down Expand Up @@ -58,6 +59,7 @@ public class CliMain {
private enum Method {
getversion,
getbalance,
getfundingaddresses,
lockwallet,
unlockwallet,
removewalletpassword,
Expand Down Expand Up @@ -152,6 +154,12 @@ public static void run(String[] args) {
out.println(btcBalance);
return;
}
case getfundingaddresses: {
var request = GetFundingAddressesRequest.newBuilder().build();
var reply = walletService.getFundingAddresses(request);
out.println(reply.getFundingAddressesInfo());
return;
}
case lockwallet: {
var request = LockWalletRequest.newBuilder().build();
walletService.lockWallet(request);
Expand Down Expand Up @@ -201,7 +209,7 @@ public static void run(String[] args) {
}
default: {
throw new RuntimeException(format("unhandled method '%s'", method));
}
}
}
} catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
Expand Down
5 changes: 5 additions & 0 deletions cli/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@
[ "$output" = "0.00000000" ]
}

@test "test getfundingaddresses" {
run ./bisq-cli --password=xyz getfundingaddresses
[ "$status" -eq 0 ]
}

@test "test help displayed on stderr if no options or arguments" {
run ./bisq-cli
[ "$status" -eq 1 ]
Expand Down
94 changes: 93 additions & 1 deletion core/src/main/java/bisq/core/grpc/CoreWalletsService.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
package bisq.core.grpc;

import bisq.core.btc.Balances;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.WalletsManager;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.crypto.KeyCrypterScrypt;

import javax.inject.Inject;

import org.spongycastle.crypto.params.KeyParameter;

import java.text.DecimalFormat;

import java.math.BigDecimal;

import java.util.List;
import java.util.Optional;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Function;

import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;

@Slf4j
class CoreWalletsService {

private final Balances balances;
private final WalletsManager walletsManager;
private final BtcWalletService btcWalletService;

@Nullable
private TimerTask lockTask;

@Nullable
private KeyParameter tempAesKey;

private final BigDecimal satoshiDivisor = new BigDecimal(100000000);
private final DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000");
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private final Function<Long, String> formatSatoshis = (sats) ->
btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor));

@Inject
public CoreWalletsService(Balances balances, WalletsManager walletsManager) {
public CoreWalletsService(Balances balances,
WalletsManager walletsManager,
BtcWalletService btcWalletService) {
this.balances = balances;
this.walletsManager = walletsManager;
this.btcWalletService = btcWalletService;
}

public long getAvailableBalance() {
Expand All @@ -50,6 +72,64 @@ public long getAvailableBalance() {
return balance.getValue();
}

public long getAddressBalance(String addressString) {
Address address = getAddressEntry(addressString).getAddress();
return btcWalletService.getBalanceForAddress(address).value;
}

public String getFundingAddresses() {
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");

if (walletsManager.areWalletsEncrypted() && tempAesKey == null)
throw new IllegalStateException("wallet is locked");

// TODO populate a List<Tuple3<String, Long, Integer>> to avoid repeated calls to
// fundingAddress.getAddressString() and getAddressBalance(addressString)
List<AddressEntry> fundingAddresses = btcWalletService.getAvailableAddressEntries();

// Create a new address with a zero balance if no addresses exist.
if (fundingAddresses.size() == 0) {
btcWalletService.getFreshAddressEntry();
fundingAddresses = btcWalletService.getAvailableAddressEntries();
}

// Check to see if at least one of the existing addresses has a 0 balance.
boolean hasZeroBalance = false;
for (AddressEntry fundingAddress : fundingAddresses) {
if (getAddressBalance(fundingAddress.getAddressString()) == 0) {
hasZeroBalance = true;
break;
}
}
if (!hasZeroBalance) {
// None of the existing addresses have a zero balance, create a new one.
btcWalletService.getFreshAddressEntry();
fundingAddresses = btcWalletService.getAvailableAddressEntries();
}

StringBuilder addressInfoBuilder = new StringBuilder();
fundingAddresses.forEach(a -> {
var addressString = a.getAddressString();
var satoshiBalance = getAddressBalance(addressString);
var btcBalance = formatSatoshis.apply(satoshiBalance);
var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString);
String addressInfo = "" + addressString
+ " balance: " + btcBalance
+ ((satoshiBalance > 0) ? (" confirmations: " + numConfirmations) : "")
+ "\n";
addressInfoBuilder.append(addressInfo);
});

return addressInfoBuilder.toString().trim();
}

public int getNumConfirmationsForMostRecentTransaction(String addressString) {
Address address = getAddressEntry(addressString).getAddress();
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
return confidence == null ? 0 : confidence.getDepthInBlocks();
}

public void setWalletPassword(String password, String newPassword) {
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");
Expand Down Expand Up @@ -156,4 +236,16 @@ private KeyCrypterScrypt getKeyCrypterScrypt() {
throw new IllegalStateException("wallet encrypter is not available");
return keyCrypterScrypt;
}

private AddressEntry getAddressEntry(String addressString) {
Optional<AddressEntry> addressEntry =
btcWalletService.getAddressEntryListAsImmutableList().stream()
.filter(e -> addressString.equals(e.getAddressString()))
.findFirst();

if (!addressEntry.isPresent())
throw new IllegalStateException(format("address %s not found in wallet", addressString));

return addressEntry.get();
}
}
17 changes: 17 additions & 0 deletions core/src/main/java/bisq/core/grpc/GrpcWalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import bisq.proto.grpc.GetBalanceReply;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetFundingAddressesReply;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.LockWalletReply;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RemoveWalletPasswordReply;
Expand Down Expand Up @@ -40,6 +42,21 @@ public void getBalance(GetBalanceRequest req, StreamObserver<GetBalanceReply> re
throw ex;
}
}

@Override
public void getFundingAddresses(GetFundingAddressesRequest req,
StreamObserver<GetFundingAddressesReply> responseObserver) {
try {
String result = walletsService.getFundingAddresses();
var reply = GetFundingAddressesReply.newBuilder().setFundingAddressesInfo(result).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}

@Override
public void setWalletPassword(SetWalletPasswordRequest req,
Expand Down
9 changes: 9 additions & 0 deletions proto/src/main/proto/grpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ message PlaceOfferReply {
service Wallet {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) {
}
rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) {
}
rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) {
}
rpc RemoveWalletPassword (RemoveWalletPasswordRequest) returns (RemoveWalletPasswordReply) {
Expand All @@ -136,6 +138,13 @@ message GetBalanceReply {
uint64 balance = 1;
}

message GetFundingAddressesRequest {
}

message GetFundingAddressesReply {
string fundingAddressesInfo = 1;
}

message SetWalletPasswordRequest {
string password = 1;
string newPassword = 2;
Expand Down

0 comments on commit 6ec18a5

Please sign in to comment.