Skip to content

Commit

Permalink
Merge pull request #4304 from ghubstan/2-getaddressbalance
Browse files Browse the repository at this point in the history
Add rpc method 'getaddressbalance'
  • Loading branch information
sqrrm committed Jun 25, 2020
2 parents d810387 + b1228e5 commit ca98654
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 2 deletions.
24 changes: 23 additions & 1 deletion cli/src/main/java/bisq/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

package bisq.cli;

import bisq.proto.grpc.GetAddressBalanceRequest;
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 +60,8 @@ public class CliMain {
private enum Method {
getversion,
getbalance,
getaddressbalance,
getfundingaddresses,
lockwallet,
unlockwallet,
removewalletpassword,
Expand Down Expand Up @@ -152,6 +156,22 @@ public static void run(String[] args) {
out.println(btcBalance);
return;
}
case getaddressbalance: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no address specified");

var request = GetAddressBalanceRequest.newBuilder()
.setAddress(nonOptionArgs.get(1)).build();
var reply = walletService.getAddressBalance(request);
out.println(reply.getAddressBalanceInfo());
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 +221,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 All @@ -222,6 +242,8 @@ private static void printHelp(OptionParser parser, PrintStream stream) {
stream.format("%-19s%-30s%s%n", "------", "------", "------------");
stream.format("%-19s%-30s%s%n", "getversion", "", "Get server version");
stream.format("%-19s%-30s%s%n", "getbalance", "", "Get server wallet balance");
stream.format("%-19s%-30s%s%n", "getaddressbalance", "", "Get server wallet address balance");
stream.format("%-19s%-30s%s%n", "getfundingaddresses", "", "Get BTC funding addresses");
stream.format("%-19s%-30s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet");
stream.format("%-19s%-30s%s%n", "unlockwallet", "password timeout",
"Store wallet password in memory for timeout seconds");
Expand Down
19 changes: 19 additions & 0 deletions cli/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@
[ "$output" = "0.00000000" ]
}

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

@test "test getaddressbalance missing address argument" {
run ./bisq-cli --password=xyz getaddressbalance
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: no address specified" ]
}

@test "test getaddressbalance bogus address argument" {
run ./bisq-cli --password=xyz getaddressbalance bogus
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: address bogus not found in wallet" ]
}

@test "test help displayed on stderr if no options or arguments" {
run ./bisq-cli
[ "$status" -eq 1 ]
Expand Down
110 changes: 109 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,64 @@
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 bisq.common.util.Tuple3;

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 java.util.stream.Collectors;

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 +75,77 @@ public long getAvailableBalance() {
return balance.getValue();
}

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

public String getAddressBalanceInfo(String addressString) {
var satoshiBalance = getAddressBalance(addressString);
var btcBalance = formatSatoshis.apply(satoshiBalance);
var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString);
return addressString
+ " balance: " + format("%13s", btcBalance)
+ ((numConfirmations > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : "");
}

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");

// Create a new funding address if none exists.
if (btcWalletService.getAvailableAddressEntries().size() == 0)
btcWalletService.getFreshAddressEntry();

// Populate a list of Tuple3<AddressString, Balance, NumConfirmations>
List<Tuple3<String, Long, Integer>> addrBalanceConfirms =
btcWalletService.getAvailableAddressEntries().stream()
.map(a -> new Tuple3<>(a.getAddressString(),
getAddressBalance(a.getAddressString()),
getNumConfirmationsForMostRecentTransaction(a.getAddressString())))
.collect(Collectors.toList());

// Check to see if at least one of the existing addresses has a zero balance.
boolean hasZeroBalance = false;
for (Tuple3<String, Long, Integer> abc : addrBalanceConfirms) {
if (abc.second == 0) {
hasZeroBalance = true;
break;
}
}
if (!hasZeroBalance) {
// None of the existing addresses have a zero balance, create a new address.
addrBalanceConfirms.add(
new Tuple3<>(btcWalletService.getFreshAddressEntry().getAddressString(),
0L,
0));
}

// Iterate the list of Tuple3<AddressString, Balance, NumConfirmations> objects
// and build the formatted info string.
StringBuilder addressInfoBuilder = new StringBuilder();
addrBalanceConfirms.forEach(a -> {
var btcBalance = formatSatoshis.apply(a.second);
var numConfirmations = getNumConfirmationsForMostRecentTransaction(a.first);
String addressInfo = "" + a.first
+ " balance: " + format("%13s", btcBalance)
+ ((a.second > 0) ? (" confirmations: " + format("%6d", 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 +252,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();
}
}
34 changes: 34 additions & 0 deletions core/src/main/java/bisq/core/grpc/GrpcWalletService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package bisq.core.grpc;

import bisq.proto.grpc.GetAddressBalanceReply;
import bisq.proto.grpc.GetAddressBalanceRequest;
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 @@ -41,6 +45,36 @@ public void getBalance(GetBalanceRequest req, StreamObserver<GetBalanceReply> re
}
}

@Override
public void getAddressBalance(GetAddressBalanceRequest req,
StreamObserver<GetAddressBalanceReply> responseObserver) {
try {
String result = walletsService.getAddressBalanceInfo(req.getAddress());
var reply = GetAddressBalanceReply.newBuilder().setAddressBalanceInfo(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 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,
StreamObserver<SetWalletPasswordReply> responseObserver) {
Expand Down
19 changes: 19 additions & 0 deletions proto/src/main/proto/grpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ message PlaceOfferReply {
service Wallet {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) {
}
rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) {
}
rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) {
}
rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) {
}
rpc RemoveWalletPassword (RemoveWalletPasswordRequest) returns (RemoveWalletPasswordReply) {
Expand All @@ -136,6 +140,21 @@ message GetBalanceReply {
uint64 balance = 1;
}

message GetAddressBalanceRequest {
string address = 1;
}

message GetAddressBalanceReply {
string addressBalanceInfo = 1;
}

message GetFundingAddressesRequest {
}

message GetFundingAddressesReply {
string fundingAddressesInfo = 1;
}

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

0 comments on commit ca98654

Please sign in to comment.