Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rpc method 'getaddressbalance' #4304

Merged
merged 5 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
111 changes: 106 additions & 5 deletions cli/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,35 +48,136 @@
run ./bisq-cli --password="xyz" getversion
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "1.3.2" ]
[ "$output" = "1.3.4" ]
}

@test "test getversion" {
run ./bisq-cli --password=xyz getversion
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "1.3.2" ]
[ "$output" = "1.3.4" ]
}

@test "test getbalance (available & unlocked wallet with 0 btc balance)" {
@test "test setwalletpassword \"a b c\"" {
run ./bisq-cli --password=xyz setwalletpassword "a b c"
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet encrypted" ]
sleep 1
}

@test "test unlockwallet without password & timeout args" {
run ./bisq-cli --password=xyz unlockwallet
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: no password specified" ]
}

@test "test unlockwallet without timeout arg" {
run ./bisq-cli --password=xyz unlockwallet "a b c"
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: no unlock timeout specified" ]
}


@test "test unlockwallet \"a b c\" 8" {
run ./bisq-cli --password=xyz unlockwallet "a b c" 8
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet unlocked" ]
}

@test "test getbalance while wallet unlocked for 8s" {
run ./bisq-cli --password=xyz getbalance
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "0.00000000" ]
sleep 8
}

@test "test unlockwallet \"a b c\" 6" {
run ./bisq-cli --password=xyz unlockwallet "a b c" 6
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet unlocked" ]
}

@test "test lockwallet before unlockwallet timeout=6s expires" {
run ./bisq-cli --password=xyz lockwallet
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet locked" ]
}

@test "test setwalletpassword incorrect old pwd error" {
run ./bisq-cli --password=xyz setwalletpassword "z z z" "d e f"
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: incorrect old password" ]
}

@test "test setwalletpassword oldpwd newpwd" {
run ./bisq-cli --password=xyz setwalletpassword "a b c" "d e f"
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet encrypted with new password" ]
sleep 1
}

@test "test getbalance wallet locked error" {
run ./bisq-cli --password=xyz getbalance
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: wallet is locked" ]
}

@test "test removewalletpassword" {
run ./bisq-cli --password=xyz removewalletpassword "d e f"
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "wallet decrypted" ]
sleep 1
}

@test "test getbalance when wallet available & unlocked with 0 btc balance" {
run ./bisq-cli --password=xyz getbalance
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$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 ]
[ "${lines[0]}" = "Bisq RPC Client" ]
[ "${lines[1]}" = "Usage: bisq-cli [options] <method>" ]
[ "${lines[1]}" = "Usage: bisq-cli [options] <method> [params]" ]
# TODO add asserts after help text is modified for new endpoints
}

@test "test --help option" {
run ./bisq-cli --help
[ "$status" -eq 0 ]
[ "${lines[0]}" = "Bisq RPC Client" ]
[ "${lines[1]}" = "Usage: bisq-cli [options] <method>" ]
[ "${lines[1]}" = "Usage: bisq-cli [options] <method> [params]" ]
# TODO add asserts after help text is modified for new endpoints
}
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 CoreWalletService {
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 CoreWalletService(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)) : "");
Copy link
Contributor

@dmos62 dmos62 Jun 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note: formatting logic in getAddressInfo and getFundingAddresses has a fair amount of duplication. Not making suggestions yet, will see what's in the next PRs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to take a look at this next, and any changes I make will be in a new PR associated with a 5-getpaymentaccts sub branch.

}

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();
}
}
Loading