From b1146fdd12ad89e94e38a8853e316f964824d67c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 12 Jun 2020 15:16:14 -0300 Subject: [PATCH 01/35] Rename CoreWalletService -> CoreWalletsService This change fixes the ambiguity in the original class name, which implied it was a btc wallet service, not a bsq and btc wallets service. --- ...alletService.java => CoreWalletsService.java} | 4 ++-- .../java/bisq/core/grpc/GrpcWalletService.java | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) rename core/src/main/java/bisq/core/grpc/{CoreWalletService.java => CoreWalletsService.java} (98%) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java similarity index 98% rename from core/src/main/java/bisq/core/grpc/CoreWalletService.java rename to core/src/main/java/bisq/core/grpc/CoreWalletsService.java index ff9383c55d4..65a6133488e 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -19,7 +19,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; @Slf4j -class CoreWalletService { +class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; @@ -31,7 +31,7 @@ class CoreWalletService { private KeyParameter tempAesKey; @Inject - public CoreWalletService(Balances balances, WalletsManager walletsManager) { + public CoreWalletsService(Balances balances, WalletsManager walletsManager) { this.balances = balances; this.walletsManager = walletsManager; } diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java index 92d4cc8b81f..0ce59bb4c09 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java @@ -20,17 +20,17 @@ class GrpcWalletService extends WalletGrpc.WalletImplBase { - private final CoreWalletService walletService; + private final CoreWalletsService walletsService; @Inject - public GrpcWalletService(CoreWalletService walletService) { - this.walletService = walletService; + public GrpcWalletService(CoreWalletsService walletsService) { + this.walletsService = walletsService; } @Override public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { try { - long result = walletService.getAvailableBalance(); + long result = walletsService.getAvailableBalance(); var reply = GetBalanceReply.newBuilder().setBalance(result).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -45,7 +45,7 @@ public void getBalance(GetBalanceRequest req, StreamObserver re public void setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { try { - walletService.setWalletPassword(req.getPassword(), req.getNewPassword()); + walletsService.setWalletPassword(req.getPassword(), req.getNewPassword()); var reply = SetWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -60,7 +60,7 @@ public void setWalletPassword(SetWalletPasswordRequest req, public void removeWalletPassword(RemoveWalletPasswordRequest req, StreamObserver responseObserver) { try { - walletService.removeWalletPassword(req.getPassword()); + walletsService.removeWalletPassword(req.getPassword()); var reply = RemoveWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -75,7 +75,7 @@ public void removeWalletPassword(RemoveWalletPasswordRequest req, public void lockWallet(LockWalletRequest req, StreamObserver responseObserver) { try { - walletService.lockWallet(); + walletsService.lockWallet(); var reply = LockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -90,7 +90,7 @@ public void lockWallet(LockWalletRequest req, public void unlockWallet(UnlockWalletRequest req, StreamObserver responseObserver) { try { - walletService.unlockWallet(req.getPassword(), req.getTimeout()); + walletsService.unlockWallet(req.getPassword(), req.getTimeout()); var reply = UnlockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); From ec66b14986bb0a9f3369009aafc541add4e65f03 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 12 Jun 2020 15:56:19 -0300 Subject: [PATCH 02/35] Add rpc wallet(s) protection tests This commit includes the following changes: * New tests for methods `lockwallet`, `unlockwallet`, `removewalletpassword`, and `setwalletpassword`. * New `getbalance` method error handing tests to verify error message correctness when wallet is locked. * Update to `getversion` method test -- now expects `1.3.4`. * Check for new `[params]` column header in help text. --- cli/test.sh | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/cli/test.sh b/cli/test.sh index 94aae7d25b6..ba81d598604 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -48,17 +48,99 @@ 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 @@ -69,7 +151,7 @@ run ./bisq-cli [ "$status" -eq 1 ] [ "${lines[0]}" = "Bisq RPC Client" ] - [ "${lines[1]}" = "Usage: bisq-cli [options] " ] + [ "${lines[1]}" = "Usage: bisq-cli [options] [params]" ] # TODO add asserts after help text is modified for new endpoints } @@ -77,6 +159,6 @@ run ./bisq-cli --help [ "$status" -eq 0 ] [ "${lines[0]}" = "Bisq RPC Client" ] - [ "${lines[1]}" = "Usage: bisq-cli [options] " ] + [ "${lines[1]}" = "Usage: bisq-cli [options] [params]" ] # TODO add asserts after help text is modified for new endpoints } From 85c96764fb3adaa25e8366d547be9d05e635f1bc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 13 Jun 2020 19:59:45 -0300 Subject: [PATCH 03/35] Add rpc method 'getfundingaddresses' This addresses task #1 in issue https://github.com/bisq-network/bisq/issues/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 #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. --- cli/src/main/java/bisq/cli/CliMain.java | 10 +- cli/test.sh | 5 + .../bisq/core/grpc/CoreWalletsService.java | 94 ++++++++++++++++++- .../bisq/core/grpc/GrpcWalletService.java | 17 ++++ proto/src/main/proto/grpc.proto | 9 ++ 5 files changed, 133 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index e5463e11cfb..ad59befae77 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -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; @@ -58,6 +59,7 @@ public class CliMain { private enum Method { getversion, getbalance, + getfundingaddresses, lockwallet, unlockwallet, removewalletpassword, @@ -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); @@ -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 diff --git a/cli/test.sh b/cli/test.sh index ba81d598604..be2e67bc46f 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -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 ] diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index 65a6133488e..b38fae120eb 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -1,21 +1,33 @@ 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 @@ -23,6 +35,7 @@ class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; + private final BtcWalletService btcWalletService; @Nullable private TimerTask lockTask; @@ -30,10 +43,19 @@ class CoreWalletsService { @Nullable private KeyParameter tempAesKey; + private final BigDecimal satoshiDivisor = new BigDecimal(100000000); + private final DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000"); + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + private final Function 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() { @@ -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> to avoid repeated calls to + // fundingAddress.getAddressString() and getAddressBalance(addressString) + List 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"); @@ -156,4 +236,16 @@ private KeyCrypterScrypt getKeyCrypterScrypt() { throw new IllegalStateException("wallet encrypter is not available"); return keyCrypterScrypt; } + + private AddressEntry getAddressEntry(String addressString) { + Optional 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(); + } } diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java index 0ce59bb4c09..62373fd1be9 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java @@ -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; @@ -40,6 +42,21 @@ public void getBalance(GetBalanceRequest req, StreamObserver re throw ex; } } + + @Override + public void getFundingAddresses(GetFundingAddressesRequest req, + StreamObserver 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, diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b8db4c6d24b..9e85dbe371e 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -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) { @@ -136,6 +138,13 @@ message GetBalanceReply { uint64 balance = 1; } +message GetFundingAddressesRequest { +} + +message GetFundingAddressesReply { + string fundingAddressesInfo = 1; +} + message SetWalletPasswordRequest { string password = 1; string newPassword = 2; From 2e415de4adaf54a9f9c926fa4557af95e6fdfbe4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 14 Jun 2020 13:05:37 -0300 Subject: [PATCH 04/35] Replace duplicate code in getFundingAddresses Cleaned up the method body and improved the returned string's formatting. Also added a line for this method in the CLI help text. --- cli/src/main/java/bisq/cli/CliMain.java | 1 + .../bisq/core/grpc/CoreWalletsService.java | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ad59befae77..36bb55add62 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -230,6 +230,7 @@ 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", "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"); diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index b38fae120eb..64d70e15c73 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -5,6 +5,8 @@ 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; @@ -22,6 +24,7 @@ import java.util.Timer; import java.util.TimerTask; import java.util.function.Function; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -84,39 +87,43 @@ public String getFundingAddresses() { if (walletsManager.areWalletsEncrypted() && tempAesKey == null) throw new IllegalStateException("wallet is locked"); - // TODO populate a List> to avoid repeated calls to - // fundingAddress.getAddressString() and getAddressBalance(addressString) - List fundingAddresses = btcWalletService.getAvailableAddressEntries(); - - // Create a new address with a zero balance if no addresses exist. - if (fundingAddresses.size() == 0) { + // Create a new funding address if none exists. + if (btcWalletService.getAvailableAddressEntries().size() == 0) btcWalletService.getFreshAddressEntry(); - fundingAddresses = btcWalletService.getAvailableAddressEntries(); - } - // Check to see if at least one of the existing addresses has a 0 balance. + // Populate a list of Tuple3 + List> 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 (AddressEntry fundingAddress : fundingAddresses) { - if (getAddressBalance(fundingAddress.getAddressString()) == 0) { + for (Tuple3 abc : addrBalanceConfirms) { + if (abc.second == 0) { hasZeroBalance = true; break; } } if (!hasZeroBalance) { - // None of the existing addresses have a zero balance, create a new one. - btcWalletService.getFreshAddressEntry(); - fundingAddresses = btcWalletService.getAvailableAddressEntries(); + // 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 objects + // and build the formatted info string. 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) : "") + 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); }); From b1228e5ea72211c9010943db1aa8bf46675417a6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 14 Jun 2020 14:23:47 -0300 Subject: [PATCH 05/35] Add rpc method 'getaddressbalance' This addresses task 2 in issue 4257 https://github.com/bisq-network/bisq/issues/4257 This new gRPC Wallet service method displays the balance and number of confimirmations of the most recent transaction for the given BTC wallet address. The new method required the usual boilerplate changes to grpc.proto, CliMain, and GrpcWalletService. Two unit tests to check error msgs was added to cli/test.sh. --- cli/src/main/java/bisq/cli/CliMain.java | 13 +++++++++++++ cli/test.sh | 14 ++++++++++++++ .../bisq/core/grpc/CoreWalletsService.java | 9 +++++++++ .../bisq/core/grpc/GrpcWalletService.java | 19 ++++++++++++++++++- proto/src/main/proto/grpc.proto | 10 ++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 36bb55add62..ab6f39fa41c 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetVersionGrpc; @@ -59,6 +60,7 @@ public class CliMain { private enum Method { getversion, getbalance, + getaddressbalance, getfundingaddresses, lockwallet, unlockwallet, @@ -154,6 +156,16 @@ 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); @@ -230,6 +242,7 @@ 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", diff --git a/cli/test.sh b/cli/test.sh index be2e67bc46f..875cb0a8a27 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -152,6 +152,20 @@ [ "$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 ] diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index 64d70e15c73..f9e3b7d1c60 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -80,6 +80,15 @@ public long getAddressBalance(String addressString) { 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"); diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java index 62373fd1be9..0343960f394 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletService.java @@ -1,5 +1,7 @@ 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; @@ -42,7 +44,22 @@ public void getBalance(GetBalanceRequest req, StreamObserver re throw ex; } } - + + @Override + public void getAddressBalance(GetAddressBalanceRequest req, + StreamObserver 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 responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 9e85dbe371e..41b490b9b53 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -119,6 +119,8 @@ message PlaceOfferReply { service Wallet { rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { } + rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { + } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { @@ -138,6 +140,14 @@ message GetBalanceReply { uint64 balance = 1; } +message GetAddressBalanceRequest { + string address = 1; +} + +message GetAddressBalanceReply { + string addressBalanceInfo = 1; +} + message GetFundingAddressesRequest { } From a7542e98bf62d86bc3486e1d4d925a9a0a64d8c6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 15 Jun 2020 14:11:51 -0300 Subject: [PATCH 06/35] Add rpc method 'createpaymentacct' This addresses task 4 in issue 4257. https://github.com/bisq-network/bisq/issues/4257 This PR should be reviewed/merged after PR 4304. https://github.com/bisq-network/bisq/pull/4304 This new gRPC PaymentAccounts service method creates a dummy PerfectMoney payment account for the given name, number and fiat currency code, as part of the required "simplest possible trading API" (for demo). An implementation supporting all payment methods is not in the scope. Changes specific to the new rpc method implementation follow: * New createpaymentacct method + help text was added to CliMain. Help text formatting was also changed to make room for larger method names and argument lists. * The PaymentAccount proto service def was renamed PaymentAccounts to avoid a name collision, and the new rpc CreatePaymentAccount was made part of the newly named PaymentAccounts service def. * New GrpcPaymentAccountsService (gRPC boilerplate) and CorePaymentAccountsService (method implementations) classes were added. * The gRPC GetPaymentAccountsService stub was moved from GrpcServer to the new GrpcPaymentAccountsService class, and GrpcPaymentAccountsService is injected into GrpcServer. * A new createpaymentacct unit test was added to the bats test suite (checks for successful return status code). Maybe bit out of scope, some small changes were made towards making sure the entire API is defined in CoreApi, which is used as a pass-through object to the new CorePaymentAccountsService. In the next PR, similar refactoring will be done to make CoreApi the pass-through object for all of the existing CoreWalletsService methods. (CoreWalletsService will be injected into CoreApi.) In the future, all Grpc*Service implementations will call core services through CoreApi, for the sake of consistency. --- cli/src/main/java/bisq/cli/CliMain.java | 47 ++++++++++++--- cli/test.sh | 5 ++ .../src/main/java/bisq/core/grpc/CoreApi.java | 11 +++- .../core/grpc/CorePaymentAccountsService.java | 57 +++++++++++++++++++ .../core/grpc/GrpcPaymentAccountsService.java | 46 +++++++++++++++ .../main/java/bisq/core/grpc/GrpcServer.java | 26 ++------- proto/src/main/proto/grpc.proto | 15 ++++- 7 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java create mode 100644 core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ab6f39fa41c..c7c16a4376f 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,12 +17,14 @@ package bisq.cli; +import bisq.proto.grpc.CreatePaymentAccountRequest; 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; +import bisq.proto.grpc.PaymentAccountsGrpc; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletRequest; @@ -58,6 +60,7 @@ public class CliMain { private enum Method { + createpaymentacct, getversion, getbalance, getaddressbalance, @@ -135,6 +138,7 @@ public static void run(String[] args) { })); var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); var walletService = WalletGrpc.newBlockingStub(channel).withCallCredentials(credentials); try { @@ -172,6 +176,30 @@ public static void run(String[] args) { out.println(reply.getFundingAddressesInfo()); return; } + case createpaymentacct: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("no account name specified"); + + var accountName = nonOptionArgs.get(1); + + if (nonOptionArgs.size() < 3) + throw new IllegalArgumentException("no account number specified"); + + var accountNumber = nonOptionArgs.get(2); + + if (nonOptionArgs.size() < 4) + throw new IllegalArgumentException("no fiat currency specified"); + + var fiatCurrencyCode = nonOptionArgs.get(3).toUpperCase(); + + var request = CreatePaymentAccountRequest.newBuilder() + .setAccountName(accountName) + .setAccountNumber(accountNumber) + .setFiatCurrencyCode(fiatCurrencyCode).build(); + paymentAccountsService.createPaymentAccount(request); + out.println(format("payment account %s saved", accountName)); + return; + } case lockwallet: { var request = LockWalletRequest.newBuilder().build(); walletService.lockWallet(request); @@ -238,16 +266,17 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.println(); parser.printHelpOn(stream); stream.println(); - stream.format("%-19s%-30s%s%n", "Method", "Params", "Description"); - 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", + stream.format("%-22s%-50s%s%n", "Method", "Params", "Description"); + stream.format("%-22s%-50s%s%n", "------", "------", "------------"); + stream.format("%-22s%-50s%s%n", "getversion", "", "Get server version"); + stream.format("%-22s%-50s%s%n", "getbalance", "", "Get server wallet balance"); + stream.format("%-22s%-50s%s%n", "getaddressbalance", "address", "Get server wallet address balance"); + stream.format("%-22s%-50s%s%n", "getfundingaddresses", "", "Get BTC funding addresses"); + stream.format("%-22s%-50s%s%n", "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); + stream.format("%-22s%-50s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet"); + stream.format("%-22s%-50s%s%n", "unlockwallet", "password timeout", "Store wallet password in memory for timeout seconds"); - stream.format("%-19s%-30s%s%n", "setwalletpassword", "password [newpassword]", + stream.format("%-22s%-50s%s%n", "setwalletpassword", "password [newpassword]", "Encrypt wallet with password, or set new password on encrypted wallet"); stream.println(); } catch (IOException ex) { diff --git a/cli/test.sh b/cli/test.sh index 875cb0a8a27..79754d188bb 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -166,6 +166,11 @@ [ "$output" = "Error: address bogus not found in wallet" ] } +@test "test createpaymentacct PerfectMoneyDummy 0123456789 USD" { + run ./bisq-cli --password=xyz createpaymentacct PerfectMoneyDummy 0123456789 USD + [ "$status" -eq 0 ] +} + @test "test help displayed on stderr if no options or arguments" { run ./bisq-cli [ "$status" -eq 1 ] diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index a0671f4d3b0..8d45f31d5d3 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -47,6 +47,7 @@ */ @Slf4j public class CoreApi { + private final CorePaymentAccountsService paymentAccountsService; private final OfferBookService offerBookService; private final TradeStatisticsManager tradeStatisticsManager; private final CreateOfferService createOfferService; @@ -54,11 +55,13 @@ public class CoreApi { private final User user; @Inject - public CoreApi(OfferBookService offerBookService, + public CoreApi(CorePaymentAccountsService paymentAccountsService, + OfferBookService offerBookService, TradeStatisticsManager tradeStatisticsManager, CreateOfferService createOfferService, OpenOfferManager openOfferManager, User user) { + this.paymentAccountsService = paymentAccountsService; this.offerBookService = offerBookService; this.tradeStatisticsManager = tradeStatisticsManager; this.createOfferService = createOfferService; @@ -78,8 +81,12 @@ public List getOffers() { return offerBookService.getOffers(); } + public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { + paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); + } + public Set getPaymentAccounts() { - return user.getPaymentAccounts(); + return paymentAccountsService.getPaymentAccounts(); } public void placeOffer(String currencyCode, diff --git a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java new file mode 100644 index 00000000000..db2d3be4a03 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java @@ -0,0 +1,57 @@ +package bisq.core.grpc; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.PerfectMoneyAccount; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.user.User; + +import bisq.common.config.Config; + +import javax.inject.Inject; + +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CorePaymentAccountsService { + + private final Config config; + private final AccountAgeWitnessService accountAgeWitnessService; + private final User user; + + @Inject + public CorePaymentAccountsService(Config config, + AccountAgeWitnessService accountAgeWitnessService, + User user) { + this.config = config; + this.accountAgeWitnessService = accountAgeWitnessService; + this.user = user; + } + + public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { + // Create and persist a PerfectMoney dummy payment account. There is no guard + // against creating accounts with duplicate names & numbers, only the uuid and + // creation date are unique. + PaymentMethod dummyPaymentMethod = PaymentMethod.getDummyPaymentMethod(PaymentMethod.PERFECT_MONEY_ID); + PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(dummyPaymentMethod); + paymentAccount.init(); + paymentAccount.setAccountName(accountName); + ((PerfectMoneyAccount) paymentAccount).setAccountNr(accountNumber); + paymentAccount.setSingleTradeCurrency(new FiatCurrency(fiatCurrencyCode)); + user.addPaymentAccount(paymentAccount); + + // Don't do this on mainnet until thoroughly tested. + if (config.baseCurrencyNetwork.isRegtest()) + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + + log.info("Payment account {} saved", paymentAccount.getId()); + } + + public Set getPaymentAccounts() { + return user.getPaymentAccounts(); + } +} diff --git a/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java new file mode 100644 index 00000000000..f2a9abf0bbb --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java @@ -0,0 +1,46 @@ +package bisq.core.grpc; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.CreatePaymentAccountReply; +import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetPaymentAccountsReply; +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; + +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.stream.Collectors; + + +public class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase { + + private final CoreApi coreApi; + + @Inject + public GrpcPaymentAccountsService(CoreApi coreApi) { + this.coreApi = coreApi; + } + + @Override + public void createPaymentAccount(CreatePaymentAccountRequest req, + StreamObserver responseObserver) { + coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); + var reply = CreatePaymentAccountReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void getPaymentAccounts(GetPaymentAccountsRequest req, + StreamObserver responseObserver) { + var tradeStatistics = coreApi.getPaymentAccounts().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } +} diff --git a/core/src/main/java/bisq/core/grpc/GrpcServer.java b/core/src/main/java/bisq/core/grpc/GrpcServer.java index 2b3543572b1..2c4f766b3c6 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/GrpcServer.java @@ -18,7 +18,6 @@ package bisq.core.grpc; import bisq.core.offer.Offer; -import bisq.core.payment.PaymentAccount; import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.statistics.TradeStatistics2; @@ -27,9 +26,6 @@ import bisq.proto.grpc.GetOffersGrpc; import bisq.proto.grpc.GetOffersReply; import bisq.proto.grpc.GetOffersRequest; -import bisq.proto.grpc.GetPaymentAccountsGrpc; -import bisq.proto.grpc.GetPaymentAccountsReply; -import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetTradeStatisticsGrpc; import bisq.proto.grpc.GetTradeStatisticsReply; import bisq.proto.grpc.GetTradeStatisticsRequest; @@ -60,14 +56,17 @@ public class GrpcServer { private final Server server; @Inject - public GrpcServer(Config config, CoreApi coreApi, GrpcWalletService walletService) { + public GrpcServer(Config config, + CoreApi coreApi, + GrpcPaymentAccountsService paymentAccountsService, + GrpcWalletService walletService) { this.coreApi = coreApi; this.server = ServerBuilder.forPort(config.apiPort) .addService(new GetVersionService()) .addService(new GetTradeStatisticsService()) .addService(new GetOffersService()) - .addService(new GetPaymentAccountsService()) .addService(new PlaceOfferService()) + .addService(paymentAccountsService) .addService(walletService) .intercept(new PasswordAuthInterceptor(config.apiPassword)) .build(); @@ -125,21 +124,6 @@ public void getOffers(GetOffersRequest req, StreamObserver respo } } - class GetPaymentAccountsService extends GetPaymentAccountsGrpc.GetPaymentAccountsImplBase { - @Override - public void getPaymentAccounts(GetPaymentAccountsRequest req, - StreamObserver responseObserver) { - - var tradeStatistics = coreApi.getPaymentAccounts().stream() - .map(PaymentAccount::toProtoMessage) - .collect(Collectors.toList()); - - var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } - } - class PlaceOfferService extends PlaceOfferGrpc.PlaceOfferImplBase { @Override public void placeOffer(PlaceOfferRequest req, StreamObserver responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 41b490b9b53..66c4eb2ada8 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -72,14 +72,25 @@ message GetOffersReply { } /////////////////////////////////////////////////////////////////////////////////////////// -// PaymentAccount +// PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// -service GetPaymentAccounts { +service PaymentAccounts { + rpc CreatePaymentAccount (CreatePaymentAccountRequest) returns (CreatePaymentAccountReply) { + } rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { } } +message CreatePaymentAccountRequest { + string accountName = 1; + string accountNumber = 2; + string fiatCurrencyCode = 3; +} + +message CreatePaymentAccountReply { +} + message GetPaymentAccountsRequest { } From bac3ed5697b4b854bb6c12128cee5aca1f64d09f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:43:26 -0300 Subject: [PATCH 07/35] Call core wallets service methods from CoreApi This change is a refactoring of the gRPC Wallets service for the purpose of making CoreApi the entry point to all core implementations. These changes should have been made in PR 4295. See https://github.com/bisq-network/bisq/pull/4295 The gRPC Wallet proto def name was changed to Wallets because this service manages BSQ and BTC wallets, and GrpcWalletService was changed to GrpcWalletsService for the same reason. This PR should be reviewed/merged after PR 4308. See https://github.com/bisq-network/bisq/pull/4308 This PR's branch was created from the PR 4308 branch. --- cli/src/main/java/bisq/cli/CliMain.java | 18 +++--- .../src/main/java/bisq/core/grpc/CoreApi.java | 62 ++++++++++++++++--- .../main/java/bisq/core/grpc/GrpcServer.java | 2 +- ...etService.java => GrpcWalletsService.java} | 24 +++---- proto/src/main/proto/grpc.proto | 4 +- 5 files changed, 78 insertions(+), 32 deletions(-) rename core/src/main/java/bisq/core/grpc/{GrpcWalletService.java => GrpcWalletsService.java} (86%) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index c7c16a4376f..dee11cc7dd7 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -28,7 +28,7 @@ import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletRequest; -import bisq.proto.grpc.WalletGrpc; +import bisq.proto.grpc.WalletsGrpc; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; @@ -139,7 +139,7 @@ public static void run(String[] args) { var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); var paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var walletService = WalletGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); try { switch (method) { @@ -151,7 +151,7 @@ public static void run(String[] args) { } case getbalance: { var request = GetBalanceRequest.newBuilder().build(); - var reply = walletService.getBalance(request); + var reply = walletsService.getBalance(request); var satoshiBalance = reply.getBalance(); var satoshiDivisor = new BigDecimal(100000000); var btcFormat = new DecimalFormat("###,##0.00000000"); @@ -166,13 +166,13 @@ public static void run(String[] args) { var request = GetAddressBalanceRequest.newBuilder() .setAddress(nonOptionArgs.get(1)).build(); - var reply = walletService.getAddressBalance(request); + var reply = walletsService.getAddressBalance(request); out.println(reply.getAddressBalanceInfo()); return; } case getfundingaddresses: { var request = GetFundingAddressesRequest.newBuilder().build(); - var reply = walletService.getFundingAddresses(request); + var reply = walletsService.getFundingAddresses(request); out.println(reply.getFundingAddressesInfo()); return; } @@ -202,7 +202,7 @@ public static void run(String[] args) { } case lockwallet: { var request = LockWalletRequest.newBuilder().build(); - walletService.lockWallet(request); + walletsService.lockWallet(request); out.println("wallet locked"); return; } @@ -222,7 +222,7 @@ public static void run(String[] args) { var request = UnlockWalletRequest.newBuilder() .setPassword(nonOptionArgs.get(1)) .setTimeout(timeout).build(); - walletService.unlockWallet(request); + walletsService.unlockWallet(request); out.println("wallet unlocked"); return; } @@ -231,7 +231,7 @@ public static void run(String[] args) { throw new IllegalArgumentException("no password specified"); var request = RemoveWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build(); - walletService.removeWalletPassword(request); + walletsService.removeWalletPassword(request); out.println("wallet decrypted"); return; } @@ -243,7 +243,7 @@ public static void run(String[] args) { var hasNewPassword = nonOptionArgs.size() == 3; if (hasNewPassword) requestBuilder.setNewPassword(nonOptionArgs.get(2)); - walletService.setWalletPassword(requestBuilder.build()); + walletsService.setWalletPassword(requestBuilder.build()); out.println("wallet encrypted" + (hasNewPassword ? " with new password" : "")); return; } diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 8d45f31d5d3..610f0d7d8dd 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -48,6 +48,7 @@ @Slf4j public class CoreApi { private final CorePaymentAccountsService paymentAccountsService; + private final CoreWalletsService walletsService; private final OfferBookService offerBookService; private final TradeStatisticsManager tradeStatisticsManager; private final CreateOfferService createOfferService; @@ -56,12 +57,14 @@ public class CoreApi { @Inject public CoreApi(CorePaymentAccountsService paymentAccountsService, + CoreWalletsService walletsService, OfferBookService offerBookService, TradeStatisticsManager tradeStatisticsManager, CreateOfferService createOfferService, OpenOfferManager openOfferManager, User user) { this.paymentAccountsService = paymentAccountsService; + this.walletsService = walletsService; this.offerBookService = offerBookService; this.tradeStatisticsManager = tradeStatisticsManager; this.createOfferService = createOfferService; @@ -73,20 +76,52 @@ public String getVersion() { return Version.VERSION; } - public List getTradeStatistics() { - return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); + /////////////////////////////////////////////////////////////////////////////////////////// + // Wallets + /////////////////////////////////////////////////////////////////////////////////////////// + + public long getAvailableBalance() { + return walletsService.getAvailableBalance(); } - public List getOffers() { - return offerBookService.getOffers(); + public long getAddressBalance(String addressString) { + return walletsService.getAddressBalance(addressString); } - public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { - paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); + public String getAddressBalanceInfo(String addressString) { + return walletsService.getAddressBalanceInfo(addressString); } - public Set getPaymentAccounts() { - return paymentAccountsService.getPaymentAccounts(); + public String getFundingAddresses() { + return walletsService.getFundingAddresses(); + } + + public void setWalletPassword(String password, String newPassword) { + walletsService.setWalletPassword(password, newPassword); + } + + public void lockWallet() { + walletsService.lockWallet(); + } + + public void unlockWallet(String password, long timeout) { + walletsService.unlockWallet(password, timeout); + } + + public void removeWalletPassword(String password) { + walletsService.removeWalletPassword(password); + } + + public List getTradeStatistics() { + return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); + } + + public int getNumConfirmationsForMostRecentTransaction(String addressString) { + return walletsService.getNumConfirmationsForMostRecentTransaction(addressString); + } + + public List getOffers() { + return offerBookService.getOffers(); } public void placeOffer(String currencyCode, @@ -152,4 +187,15 @@ public void placeOffer(String offerId, log::error); } + /////////////////////////////////////////////////////////////////////////////////////////// + // PaymentAccounts + /////////////////////////////////////////////////////////////////////////////////////////// + + public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { + paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); + } + + public Set getPaymentAccounts() { + return paymentAccountsService.getPaymentAccounts(); + } } diff --git a/core/src/main/java/bisq/core/grpc/GrpcServer.java b/core/src/main/java/bisq/core/grpc/GrpcServer.java index 2c4f766b3c6..6fa6dad9faf 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/GrpcServer.java @@ -59,7 +59,7 @@ public class GrpcServer { public GrpcServer(Config config, CoreApi coreApi, GrpcPaymentAccountsService paymentAccountsService, - GrpcWalletService walletService) { + GrpcWalletsService walletService) { this.coreApi = coreApi; this.server = ServerBuilder.forPort(config.apiPort) .addService(new GetVersionService()) diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java similarity index 86% rename from core/src/main/java/bisq/core/grpc/GrpcWalletService.java rename to core/src/main/java/bisq/core/grpc/GrpcWalletsService.java index 0343960f394..e7ec5629100 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java @@ -14,7 +14,7 @@ import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletReply; import bisq.proto.grpc.UnlockWalletRequest; -import bisq.proto.grpc.WalletGrpc; +import bisq.proto.grpc.WalletsGrpc; import io.grpc.Status; import io.grpc.StatusRuntimeException; @@ -22,19 +22,19 @@ import javax.inject.Inject; -class GrpcWalletService extends WalletGrpc.WalletImplBase { +class GrpcWalletsService extends WalletsGrpc.WalletsImplBase { - private final CoreWalletsService walletsService; + private final CoreApi coreApi; @Inject - public GrpcWalletService(CoreWalletsService walletsService) { - this.walletsService = walletsService; + public GrpcWalletsService(CoreApi coreApi) { + this.coreApi = coreApi; } @Override public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { try { - long result = walletsService.getAvailableBalance(); + long result = coreApi.getAvailableBalance(); var reply = GetBalanceReply.newBuilder().setBalance(result).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -49,7 +49,7 @@ public void getBalance(GetBalanceRequest req, StreamObserver re public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { try { - String result = walletsService.getAddressBalanceInfo(req.getAddress()); + String result = coreApi.getAddressBalanceInfo(req.getAddress()); var reply = GetAddressBalanceReply.newBuilder().setAddressBalanceInfo(result).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -64,7 +64,7 @@ public void getAddressBalance(GetAddressBalanceRequest req, public void getFundingAddresses(GetFundingAddressesRequest req, StreamObserver responseObserver) { try { - String result = walletsService.getFundingAddresses(); + String result = coreApi.getFundingAddresses(); var reply = GetFundingAddressesReply.newBuilder().setFundingAddressesInfo(result).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -79,7 +79,7 @@ public void getFundingAddresses(GetFundingAddressesRequest req, public void setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { try { - walletsService.setWalletPassword(req.getPassword(), req.getNewPassword()); + coreApi.setWalletPassword(req.getPassword(), req.getNewPassword()); var reply = SetWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -94,7 +94,7 @@ public void setWalletPassword(SetWalletPasswordRequest req, public void removeWalletPassword(RemoveWalletPasswordRequest req, StreamObserver responseObserver) { try { - walletsService.removeWalletPassword(req.getPassword()); + coreApi.removeWalletPassword(req.getPassword()); var reply = RemoveWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -109,7 +109,7 @@ public void removeWalletPassword(RemoveWalletPasswordRequest req, public void lockWallet(LockWalletRequest req, StreamObserver responseObserver) { try { - walletsService.lockWallet(); + coreApi.lockWallet(); var reply = LockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -124,7 +124,7 @@ public void lockWallet(LockWalletRequest req, public void unlockWallet(UnlockWalletRequest req, StreamObserver responseObserver) { try { - walletsService.unlockWallet(req.getPassword(), req.getTimeout()); + coreApi.unlockWallet(req.getPassword(), req.getTimeout()); var reply = UnlockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 66c4eb2ada8..1c7ca532b0a 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -124,10 +124,10 @@ message PlaceOfferReply { } /////////////////////////////////////////////////////////////////////////////////////////// -// Wallet +// Wallets /////////////////////////////////////////////////////////////////////////////////////////// -service Wallet { +service Wallets { rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { From 258d1801d2d5cecd06c60e60e31fa8b2ede744e0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 16 Jun 2020 12:48:41 -0300 Subject: [PATCH 08/35] Factor duplicate unlocked wallet checks into new method Response to comment in PR 4299: https://github.com/bisq-network/bisq/pull/4299#discussion_r440769032 This PR should be reviewed/merged after PR 4309. https://github.com/bisq-network/bisq/pull/4309 --- .../main/java/bisq/core/grpc/CoreWalletsService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index f9e3b7d1c60..be44122ab2e 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -65,8 +65,7 @@ public long getAvailableBalance() { if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); - if (walletsManager.areWalletsEncrypted() && tempAesKey == null) - throw new IllegalStateException("wallet is locked"); + verifyEncryptedWalletIsUnlocked(); var balance = balances.getAvailableBalance().get(); if (balance == null) @@ -93,8 +92,7 @@ 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"); + verifyEncryptedWalletIsUnlocked(); // Create a new funding address if none exists. if (btcWalletService.getAvailableAddressEntries().size() == 0) @@ -246,6 +244,12 @@ private void verifyWalletIsAvailableAndEncrypted() { throw new IllegalStateException("wallet is not encrypted with a password"); } + // Throws a RuntimeException if wallets are encrypted and locked. + private void verifyEncryptedWalletIsUnlocked() { + if (walletsManager.areWalletsEncrypted() && tempAesKey == null) + throw new IllegalStateException("wallet is locked"); + } + private KeyCrypterScrypt getKeyCrypterScrypt() { KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); if (keyCrypterScrypt == null) From c5134e14c28663e16b07a1eaf1ab63e4428f1f5a Mon Sep 17 00:00:00 2001 From: Dominykas Mostauskis Date: Thu, 18 Jun 2020 15:00:23 +0200 Subject: [PATCH 09/35] Replace Tuple3 with memoization --- .../bisq/core/grpc/CoreWalletsService.java | 89 +++++++++++-------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index be44122ab2e..26a64e50696 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -30,6 +30,10 @@ import javax.annotation.Nullable; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; + import static java.lang.String.format; import static java.util.concurrent.TimeUnit.SECONDS; @@ -88,6 +92,20 @@ public String getAddressBalanceInfo(String addressString) { + ((numConfirmations > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); } + /** + * Memoization stores the results of expensive function calls and returns + * the cached result when the same input occurs again. + * + * Resulting LoadingCache is used by calling `.get(input I)` or + * `.getUnchecked(input I)`, depending on whether or not `f` can return null. + * That's because CacheLoader throws an exception on null output from `f`. + */ + private static LoadingCache memoize(Function f) { + // f::apply is used, because Guava 20.0 Function doesn't yet extend + // Java Function. + return CacheBuilder.newBuilder().build(CacheLoader.from(f::apply)); + } + public String getFundingAddresses() { if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); @@ -98,44 +116,43 @@ public String getFundingAddresses() { if (btcWalletService.getAvailableAddressEntries().size() == 0) btcWalletService.getFreshAddressEntry(); - // Populate a list of Tuple3 - List> 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 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)); + List addressStrings = + btcWalletService + .getAvailableAddressEntries() + .stream() + .map(addressEntry -> addressEntry.getAddressString()) + .collect(Collectors.toList()); + + // getAddressBalance is memoized, because we'll map it over addresses twice. + // To get the balances, we'll be using .getUnchecked, because we know that + // this::getAddressBalance cannot return null. + var balances = memoize(this::getAddressBalance); + + boolean noAddressHasZeroBalance = + addressStrings.stream() + .allMatch(addressString -> balances.getUnchecked(addressString) != 0); + + if (noAddressHasZeroBalance) { + var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry(); + addressStrings.add(newZeroBalanceAddress.getAddressString()); } - // Iterate the list of Tuple3 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(); + String fundingAddressTable = + addressStrings.stream() + .map(addressString -> { + var balance = balances.getUnchecked(addressString); + var stringFormattedBalance = formatSatoshis.apply(balance); + var numConfirmations = + getNumConfirmationsForMostRecentTransaction(addressString); + String addressInfo = + "" + addressString + + " balance: " + format("%13s", stringFormattedBalance) + + ((balance > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); + return addressInfo; + }) + .collect(Collectors.joining("\n")); + + return fundingAddressTable; } public int getNumConfirmationsForMostRecentTransaction(String addressString) { From b0e278f32e97b788c22de8131f073ab0567407a5 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 18 Jun 2020 12:52:29 -0300 Subject: [PATCH 10/35] Refactor getFundingAddresses to use memoization Also reordered some import statements according to Bisq style rules. --- .../bisq/core/grpc/CoreWalletsService.java | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index 26a64e50696..93110315ceb 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -5,14 +5,16 @@ 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 com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + import org.spongycastle.crypto.params.KeyParameter; import java.text.DecimalFormat; @@ -30,10 +32,6 @@ import javax.annotation.Nullable; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.LoadingCache; - import static java.lang.String.format; import static java.util.concurrent.TimeUnit.SECONDS; @@ -56,6 +54,7 @@ class CoreWalletsService { private final Function formatSatoshis = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor)); + @Inject public CoreWalletsService(Balances balances, WalletsManager walletsManager, @@ -92,19 +91,6 @@ public String getAddressBalanceInfo(String addressString) { + ((numConfirmations > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); } - /** - * Memoization stores the results of expensive function calls and returns - * the cached result when the same input occurs again. - * - * Resulting LoadingCache is used by calling `.get(input I)` or - * `.getUnchecked(input I)`, depending on whether or not `f` can return null. - * That's because CacheLoader throws an exception on null output from `f`. - */ - private static LoadingCache memoize(Function f) { - // f::apply is used, because Guava 20.0 Function doesn't yet extend - // Java Function. - return CacheBuilder.newBuilder().build(CacheLoader.from(f::apply)); - } public String getFundingAddresses() { if (!walletsManager.areWalletsAvailable()) @@ -117,11 +103,11 @@ public String getFundingAddresses() { btcWalletService.getFreshAddressEntry(); List addressStrings = - btcWalletService - .getAvailableAddressEntries() - .stream() - .map(addressEntry -> addressEntry.getAddressString()) - .collect(Collectors.toList()); + btcWalletService + .getAvailableAddressEntries() + .stream() + .map(AddressEntry::getAddressString) + .collect(Collectors.toList()); // getAddressBalance is memoized, because we'll map it over addresses twice. // To get the balances, we'll be using .getUnchecked, because we know that @@ -129,30 +115,25 @@ public String getFundingAddresses() { var balances = memoize(this::getAddressBalance); boolean noAddressHasZeroBalance = - addressStrings.stream() - .allMatch(addressString -> balances.getUnchecked(addressString) != 0); + addressStrings.stream() + .allMatch(addressString -> balances.getUnchecked(addressString) != 0); if (noAddressHasZeroBalance) { var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry(); addressStrings.add(newZeroBalanceAddress.getAddressString()); } - String fundingAddressTable = - addressStrings.stream() - .map(addressString -> { - var balance = balances.getUnchecked(addressString); - var stringFormattedBalance = formatSatoshis.apply(balance); - var numConfirmations = - getNumConfirmationsForMostRecentTransaction(addressString); - String addressInfo = - "" + addressString - + " balance: " + format("%13s", stringFormattedBalance) - + ((balance > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); - return addressInfo; - }) - .collect(Collectors.joining("\n")); - - return fundingAddressTable; + return addressStrings.stream() + .map(addressString -> { + var balance = balances.getUnchecked(addressString); + var stringFormattedBalance = formatSatoshis.apply(balance); + var numConfirmations = + getNumConfirmationsForMostRecentTransaction(addressString); + return "" + addressString + + " balance: " + format("%13s", stringFormattedBalance) + + ((balance > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); + }) + .collect(Collectors.joining("\n")); } public int getNumConfirmationsForMostRecentTransaction(String addressString) { @@ -285,4 +266,18 @@ private AddressEntry getAddressEntry(String addressString) { return addressEntry.get(); } + + /** + * Memoization stores the results of expensive function calls and returns + * the cached result when the same input occurs again. + * + * Resulting LoadingCache is used by calling `.get(input I)` or + * `.getUnchecked(input I)`, depending on whether or not `f` can return null. + * That's because CacheLoader throws an exception on null output from `f`. + */ + private static LoadingCache memoize(Function f) { + // f::apply is used, because Guava 20.0 Function doesn't yet extend + // Java Function. + return CacheBuilder.newBuilder().build(CacheLoader.from(f::apply)); + } } From 1930411e61febf8e2b83ddd1b9c756626e93aaf8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 18 Jun 2020 12:55:31 -0300 Subject: [PATCH 11/35] Rmove blank line --- core/src/main/java/bisq/core/grpc/CoreWalletsService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index 93110315ceb..fcacd43dcbc 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -54,7 +54,6 @@ class CoreWalletsService { private final Function formatSatoshis = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor)); - @Inject public CoreWalletsService(Balances balances, WalletsManager walletsManager, From 435672a5ee71b4988b22d41f7762303b59bc3a22 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 19 Jun 2020 12:45:04 -0300 Subject: [PATCH 12/35] Add rpc method 'getpaymentaccts' This addresses task 5 in issue 4257 https://github.com/bisq-network/bisq/issues/4257 This new gRPC PaymentAccounts service method displays the user's saved payment accounts. A unit test to check a successful return status code was added to cli/test.sh. This PR should be reviewed/merged after PR 4322. https://github.com/bisq-network/bisq/pull/4322 --- cli/src/main/java/bisq/cli/CliMain.java | 18 ++++++++++++++++++ cli/test.sh | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index dee11cc7dd7..4f8b6f2d1be 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -21,6 +21,7 @@ import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.LockWalletRequest; @@ -45,6 +46,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -61,6 +63,7 @@ public class CliMain { private enum Method { createpaymentacct, + getpaymentaccts, getversion, getbalance, getaddressbalance, @@ -200,6 +203,20 @@ public static void run(String[] args) { out.println(format("payment account %s saved", accountName)); return; } + case getpaymentaccts: { + var request = GetPaymentAccountsRequest.newBuilder().build(); + var reply = paymentAccountsService.getPaymentAccounts(request); + var columnFormatSpec = "%-41s %-25s %-14s %s"; + out.println(format(columnFormatSpec, "ID", "Name", "Currency", "Payment Method")); + out.println(reply.getPaymentAccountsList().stream() + .map(a -> format(columnFormatSpec, + a.getId(), + a.getAccountName(), + a.getSelectedTradeCurrency().getCode(), + a.getPaymentMethod().getId())) + .collect(Collectors.joining("\n"))); + return; + } case lockwallet: { var request = LockWalletRequest.newBuilder().build(); walletsService.lockWallet(request); @@ -273,6 +290,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format("%-22s%-50s%s%n", "getaddressbalance", "address", "Get server wallet address balance"); stream.format("%-22s%-50s%s%n", "getfundingaddresses", "", "Get BTC funding addresses"); stream.format("%-22s%-50s%s%n", "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); + stream.format("%-22s%-50s%s%n", "getpaymentaccts", "", "Get user payment accounts"); stream.format("%-22s%-50s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet"); stream.format("%-22s%-50s%s%n", "unlockwallet", "password timeout", "Store wallet password in memory for timeout seconds"); diff --git a/cli/test.sh b/cli/test.sh index 79754d188bb..eaa64c9ebc8 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -171,6 +171,11 @@ [ "$status" -eq 0 ] } +@test "test getpaymentaccts" { + run ./bisq-cli --password=xyz getpaymentaccts + [ "$status" -eq 0 ] +} + @test "test help displayed on stderr if no options or arguments" { run ./bisq-cli [ "$status" -eq 1 ] From 331f488057ff0939d485d66da4394012514879e4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 19 Jun 2020 20:00:28 -0300 Subject: [PATCH 13/35] Return protos from funding address methods The 'getaddressbalance' and 'getfundingaddresses' methods now send new AddressBalanceInfo proto messages instead of a formatted String to the client. The AddressBalanceInfo message contains addressString, balance, and # of confirmations (transaction confidence) fields. Changes include: * A new AddressBalanceInfo proto message * A wrapper class for the new AddressBalanceInfo proto * New 'getaddressbalance' and 'getfundingaddresses' signatures in server * AddressBalanceInfo display logic in client * Removal of balance formatting logic in server * Refactoring of balance formatting logic in client --- cli/src/main/java/bisq/cli/CliMain.java | 32 +++++++++++--- .../src/main/java/bisq/core/grpc/CoreApi.java | 5 ++- .../bisq/core/grpc/CoreWalletsService.java | 37 ++++------------ .../bisq/core/grpc/GrpcWalletsService.java | 18 ++++++-- .../core/grpc/model/AddressBalanceInfo.java | 43 +++++++++++++++++++ proto/src/main/proto/grpc.proto | 10 ++++- 6 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 4f8b6f2d1be..3d1048c987c 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; @@ -46,6 +47,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -61,6 +63,12 @@ @Slf4j public class CliMain { + private static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); + private static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + private static final Function formatSatoshis = (sats) -> + BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); + private enum Method { createpaymentacct, getpaymentaccts, @@ -155,11 +163,7 @@ public static void run(String[] args) { case getbalance: { var request = GetBalanceRequest.newBuilder().build(); var reply = walletsService.getBalance(request); - var satoshiBalance = reply.getBalance(); - var satoshiDivisor = new BigDecimal(100000000); - var btcFormat = new DecimalFormat("###,##0.00000000"); - @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - var btcBalance = btcFormat.format(BigDecimal.valueOf(satoshiBalance).divide(satoshiDivisor)); + var btcBalance = formatSatoshis.apply(reply.getBalance()); out.println(btcBalance); return; } @@ -170,13 +174,16 @@ public static void run(String[] args) { var request = GetAddressBalanceRequest.newBuilder() .setAddress(nonOptionArgs.get(1)).build(); var reply = walletsService.getAddressBalance(request); - out.println(reply.getAddressBalanceInfo()); + out.println(addressBalanceInfoHeader()); + out.println(addressBalanceInfoDetail(reply.getAddressBalanceInfo())); return; } case getfundingaddresses: { var request = GetFundingAddressesRequest.newBuilder().build(); var reply = walletsService.getFundingAddresses(request); - out.println(reply.getFundingAddressesInfo()); + out.println(addressBalanceInfoHeader()); + reply.getAddressBalanceInfoList().forEach(balanceInfo -> + out.println(addressBalanceInfoDetail(balanceInfo))); return; } case createpaymentacct: { @@ -301,4 +308,15 @@ private static void printHelp(OptionParser parser, PrintStream stream) { ex.printStackTrace(stream); } } + + private static String addressBalanceInfoHeader() { + return format("%-35s %13s %s", "Address", "Balance", "Confirmations"); + } + + private static String addressBalanceInfoDetail(AddressBalanceInfo addressBalanceInfo) { + return format("%-35s %13s %14d", + addressBalanceInfo.getAddress(), + formatSatoshis.apply(addressBalanceInfo.getBalance()), + addressBalanceInfo.getNumConfirmations()); + } } diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 610f0d7d8dd..27d4d42573d 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -17,6 +17,7 @@ package bisq.core.grpc; +import bisq.core.grpc.model.AddressBalanceInfo; import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; @@ -88,11 +89,11 @@ public long getAddressBalance(String addressString) { return walletsService.getAddressBalance(addressString); } - public String getAddressBalanceInfo(String addressString) { + public AddressBalanceInfo getAddressBalanceInfo(String addressString) { return walletsService.getAddressBalanceInfo(addressString); } - public String getFundingAddresses() { + public List getFundingAddresses() { return walletsService.getFundingAddresses(); } diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index fcacd43dcbc..d7696dce1f3 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -4,6 +4,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; +import bisq.core.grpc.model.AddressBalanceInfo; import org.bitcoinj.core.Address; import org.bitcoinj.core.TransactionConfidence; @@ -17,10 +18,6 @@ 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; @@ -48,12 +45,6 @@ class CoreWalletsService { @Nullable private KeyParameter tempAesKey; - private final BigDecimal satoshiDivisor = new BigDecimal(100000000); - private final DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000"); - @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - private final Function formatSatoshis = (sats) -> - btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor)); - @Inject public CoreWalletsService(Balances balances, WalletsManager walletsManager, @@ -81,17 +72,13 @@ public long getAddressBalance(String addressString) { return btcWalletService.getBalanceForAddress(address).value; } - public String getAddressBalanceInfo(String addressString) { + public AddressBalanceInfo 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)) : ""); + return new AddressBalanceInfo(addressString, satoshiBalance, numConfirmations); } - - public String getFundingAddresses() { + public List getFundingAddresses() { if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); @@ -122,17 +109,11 @@ public String getFundingAddresses() { addressStrings.add(newZeroBalanceAddress.getAddressString()); } - return addressStrings.stream() - .map(addressString -> { - var balance = balances.getUnchecked(addressString); - var stringFormattedBalance = formatSatoshis.apply(balance); - var numConfirmations = - getNumConfirmationsForMostRecentTransaction(addressString); - return "" + addressString - + " balance: " + format("%13s", stringFormattedBalance) - + ((balance > 0) ? (" confirmations: " + format("%6d", numConfirmations)) : ""); - }) - .collect(Collectors.joining("\n")); + return addressStrings.stream().map(address -> + new AddressBalanceInfo(address, + balances.getUnchecked(address), + getNumConfirmationsForMostRecentTransaction(address))) + .collect(Collectors.toList()); } public int getNumConfirmationsForMostRecentTransaction(String addressString) { diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java index e7ec5629100..0e44e8b329b 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java @@ -1,5 +1,7 @@ package bisq.core.grpc; +import bisq.core.grpc.model.AddressBalanceInfo; + import bisq.proto.grpc.GetAddressBalanceReply; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceReply; @@ -22,6 +24,9 @@ import javax.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + class GrpcWalletsService extends WalletsGrpc.WalletsImplBase { private final CoreApi coreApi; @@ -49,8 +54,8 @@ public void getBalance(GetBalanceRequest req, StreamObserver re public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { try { - String result = coreApi.getAddressBalanceInfo(req.getAddress()); - var reply = GetAddressBalanceReply.newBuilder().setAddressBalanceInfo(result).build(); + AddressBalanceInfo result = coreApi.getAddressBalanceInfo(req.getAddress()); + var reply = GetAddressBalanceReply.newBuilder().setAddressBalanceInfo(result.toProtoMessage()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (IllegalStateException cause) { @@ -64,8 +69,13 @@ public void getAddressBalance(GetAddressBalanceRequest req, public void getFundingAddresses(GetFundingAddressesRequest req, StreamObserver responseObserver) { try { - String result = coreApi.getFundingAddresses(); - var reply = GetFundingAddressesReply.newBuilder().setFundingAddressesInfo(result).build(); + List result = coreApi.getFundingAddresses(); + var reply = GetFundingAddressesReply.newBuilder() + .addAllAddressBalanceInfo( + result.stream() + .map(AddressBalanceInfo::toProtoMessage) + .collect(Collectors.toList())) + .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (IllegalStateException cause) { diff --git a/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java b/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java new file mode 100644 index 00000000000..e452f999416 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java @@ -0,0 +1,43 @@ +package bisq.core.grpc.model; + +import bisq.common.Payload; + +public class AddressBalanceInfo implements Payload { + + private final String address; + private final long balance; // address' balance in satoshis + private final long numConfirmations; // # confirmations for address' most recent tx + + public AddressBalanceInfo(String address, long balance, long numConfirmations) { + this.address = address; + this.balance = balance; + this.numConfirmations = numConfirmations; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.AddressBalanceInfo toProtoMessage() { + return bisq.proto.grpc.AddressBalanceInfo.newBuilder() + .setAddress(address) + .setBalance(balance) + .setNumConfirmations(numConfirmations).build(); + } + + public static AddressBalanceInfo fromProto(bisq.proto.grpc.AddressBalanceInfo proto) { + return new AddressBalanceInfo(proto.getAddress(), + proto.getBalance(), + proto.getNumConfirmations()); + } + + @Override + public String toString() { + return "AddressBalanceInfo{" + + "address='" + address + '\'' + + ", balance=" + balance + + ", numConfirmations=" + numConfirmations + + '}'; + } +} diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 1c7ca532b0a..7168d04928c 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -156,14 +156,14 @@ message GetAddressBalanceRequest { } message GetAddressBalanceReply { - string addressBalanceInfo = 1; + AddressBalanceInfo addressBalanceInfo = 1; } message GetFundingAddressesRequest { } message GetFundingAddressesReply { - string fundingAddressesInfo = 1; + repeated AddressBalanceInfo addressBalanceInfo = 1; } message SetWalletPasswordRequest { @@ -194,3 +194,9 @@ message UnlockWalletRequest { message UnlockWalletReply { } + +message AddressBalanceInfo { + string address = 1; + int64 balance = 2; + int64 numConfirmations = 3; +} From 612bafe59a8bdb2da7b27d5f723174274302a617 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 08:42:58 -0300 Subject: [PATCH 14/35] Refactor AddressBalanceInfo display logic --- cli/src/main/java/bisq/cli/CliMain.java | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 3d1048c987c..a60d41bd2b0 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -56,10 +56,12 @@ import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; +import static java.util.Collections.singletonList; /** * A command-line client for the Bisq gRPC API. */ +@SuppressWarnings("ResultOfMethodCallIgnored") @Slf4j public class CliMain { @@ -174,16 +176,13 @@ public static void run(String[] args) { var request = GetAddressBalanceRequest.newBuilder() .setAddress(nonOptionArgs.get(1)).build(); var reply = walletsService.getAddressBalance(request); - out.println(addressBalanceInfoHeader()); - out.println(addressBalanceInfoDetail(reply.getAddressBalanceInfo())); + out.println(formatTable(singletonList(reply.getAddressBalanceInfo()))); return; } case getfundingaddresses: { var request = GetFundingAddressesRequest.newBuilder().build(); var reply = walletsService.getFundingAddresses(request); - out.println(addressBalanceInfoHeader()); - reply.getAddressBalanceInfoList().forEach(balanceInfo -> - out.println(addressBalanceInfoDetail(balanceInfo))); + out.println(formatTable(reply.getAddressBalanceInfoList())); return; } case createpaymentacct: { @@ -309,14 +308,13 @@ private static void printHelp(OptionParser parser, PrintStream stream) { } } - private static String addressBalanceInfoHeader() { - return format("%-35s %13s %s", "Address", "Balance", "Confirmations"); - } - - private static String addressBalanceInfoDetail(AddressBalanceInfo addressBalanceInfo) { - return format("%-35s %13s %14d", - addressBalanceInfo.getAddress(), - formatSatoshis.apply(addressBalanceInfo.getBalance()), - addressBalanceInfo.getNumConfirmations()); + private static String formatTable(List addressBalanceInfo) { + return format("%-35s %13s %s%n", "Address", "Balance", "Confirmations") + + addressBalanceInfo.stream() + .map(info -> format("%-35s %13s %14d", + info.getAddress(), + formatSatoshis.apply(info.getBalance()), + info.getNumConfirmations())) + .collect(Collectors.joining("\n")); } } From 37fb60672f56e7b7f5e19b061ce7d959058eb0bc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 09:59:06 -0300 Subject: [PATCH 15/35] Add license comment This is not in scope of current PR, but easy enough to review. --- .../java/bisq/cli/PasswordCallCredentials.java | 17 +++++++++++++++++ .../java/bisq/core/grpc/GrpcWalletsService.java | 17 +++++++++++++++++ .../bisq/core/grpc/PasswordAuthInterceptor.java | 17 +++++++++++++++++ .../core/grpc/model/AddressBalanceInfo.java | 17 +++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java index 14b451d28f8..a1de5be5564 100644 --- a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.cli; import io.grpc.CallCredentials; diff --git a/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java index 0e44e8b329b..f57cb69afb7 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcWalletsService.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import bisq.core.grpc.model.AddressBalanceInfo; diff --git a/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java index 2ab29bcdc95..291c09c5944 100644 --- a/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import io.grpc.Metadata; diff --git a/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java b/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java index e452f999416..dd9ed19f90e 100644 --- a/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java +++ b/core/src/main/java/bisq/core/grpc/model/AddressBalanceInfo.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc.model; import bisq.common.Payload; From 855ac0f250e5375dd89692fb7d0df8dc0b0c6a4e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:02:04 -0300 Subject: [PATCH 16/35] Add license comment --- .../java/bisq/core/grpc/CoreWalletsService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index d7696dce1f3..3eb4cac51ae 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import bisq.core.btc.Balances; From bfcc693f69d06ba1d2ebb33caa1a71a7f4d83542 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:03:10 -0300 Subject: [PATCH 17/35] Add license comment --- .../core/grpc/CorePaymentAccountsService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java index db2d3be4a03..c6c2613e1c7 100644 --- a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import bisq.core.account.witness.AccountAgeWitnessService; From d06807b0e53c2c3054c90e7268b92f4fe1740041 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:10:39 -0300 Subject: [PATCH 18/35] Wrap Exception from core in gRPC StatusRuntimeException --- .../core/grpc/GrpcPaymentAccountsService.java | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java index f2a9abf0bbb..f88e1adb8dd 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import bisq.core.payment.PaymentAccount; @@ -8,6 +25,8 @@ import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import javax.inject.Inject; @@ -27,20 +46,32 @@ public GrpcPaymentAccountsService(CoreApi coreApi) { @Override public void createPaymentAccount(CreatePaymentAccountRequest req, StreamObserver responseObserver) { - coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); - var reply = CreatePaymentAccountReply.newBuilder().build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); + try { + coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); + var reply = CreatePaymentAccountReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Exception cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } } @Override public void getPaymentAccounts(GetPaymentAccountsRequest req, StreamObserver responseObserver) { - var tradeStatistics = coreApi.getPaymentAccounts().stream() - .map(PaymentAccount::toProtoMessage) - .collect(Collectors.toList()); - var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); + try { + var tradeStatistics = coreApi.getPaymentAccounts().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Exception cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } } } From 7c073c65f5bf4849bcee1c2ab92acf9136827556 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:14:36 -0300 Subject: [PATCH 19/35] Revert "Add license comment" This reverts commit bfcc693f69d06ba1d2ebb33caa1a71a7f4d83542. This change was reverted because we want unexpected Exceptions to bubble up for now, until CorePaymentAccountsService.java throws specific IllegalStateExceptions with user friendly error messages. (See CoreWalletsService.java for example.) --- .../core/grpc/CorePaymentAccountsService.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java index c6c2613e1c7..db2d3be4a03 100644 --- a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java @@ -1,20 +1,3 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - package bisq.core.grpc; import bisq.core.account.witness.AccountAgeWitnessService; From d6ea0ea23692f37508acb56d2dc3d8f7333ebad0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:21:57 -0300 Subject: [PATCH 20/35] Re-add license comment The previous revert was a mistake. It applied to GrpcPaymentAccountsService, not CorePaymentAccountsService. --- .../core/grpc/CorePaymentAccountsService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java index db2d3be4a03..c6c2613e1c7 100644 --- a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + package bisq.core.grpc; import bisq.core.account.witness.AccountAgeWitnessService; From 41f1add76b4faf66c2baf3c2c6633809365b41b4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 10:24:14 -0300 Subject: [PATCH 21/35] Remove try catch block Remove the recently added gRPC StatusRuntimeException wrapping logic because we want unexpected Exceptions to bubble up for now, until CorePaymentAccountsService.java throws specific IllegalStateExceptions with user friendly error messages. (See CoreWalletsService.java for example.) --- .../core/grpc/GrpcPaymentAccountsService.java | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java index f88e1adb8dd..8388b85e3bc 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/GrpcPaymentAccountsService.java @@ -25,8 +25,6 @@ import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.PaymentAccountsGrpc; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import javax.inject.Inject; @@ -46,32 +44,20 @@ public GrpcPaymentAccountsService(CoreApi coreApi) { @Override public void createPaymentAccount(CreatePaymentAccountRequest req, StreamObserver responseObserver) { - try { - coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); - var reply = CreatePaymentAccountReply.newBuilder().build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (Exception cause) { - var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); - responseObserver.onError(ex); - throw ex; - } + coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); + var reply = CreatePaymentAccountReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); } @Override public void getPaymentAccounts(GetPaymentAccountsRequest req, StreamObserver responseObserver) { - try { - var tradeStatistics = coreApi.getPaymentAccounts().stream() - .map(PaymentAccount::toProtoMessage) - .collect(Collectors.toList()); - var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (Exception cause) { - var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); - responseObserver.onError(ex); - throw ex; - } + var tradeStatistics = coreApi.getPaymentAccounts().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); } } From 88cb90e2093e3563de1eb1e30cad46083245c172 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:56:28 -0300 Subject: [PATCH 22/35] Add rpc method 'getoffers' The new method returns current buy or sell offers for a fiat ccy. These changes need refactoring and polishing before merging, but they're committed in this state to be safe (don't lose work). Changes include: * New core.grpc classes CoreOffersService GrpcOffersService model.OfferInfo * CoreApi -- The new CoreOffersService is injected into CoreApi and the old getOffers() and placeOffer() impls were moved into the new CoreOffersService. The getOffers implementation was re-done. Other changes are just rearranging location of core method calls. * GrpcServer -- The new GrpcOffersService replaced the old GetOffersService and PlaceOfferService. * grpc.proto -- The old GetOffers and PlaceOffer services were combined into a single Offers service, and the PlaceOffer rpc was renamed as CreateOffer. These are the only substantive changes; the rest is just rearranging location of the service defs in the file. Also created a lighterweight OfferInfo proto message wrapper to be passed between server & client (client has no access to core's Offer and OfferPayload). * OfferInfo -- A new wrapper around the OfferInfo proto message. * CliMain -- The new GetOffers service stub was added. Some (maybe too much) number and ccy formatting logic was copied & modified from core. Some tedius string formatting was added too (needs to be tidied up). * License comments were also copied to several classes, and I made a mistake in reverting changes to the wrong file. TODO add unit tests --- cli/src/main/java/bisq/cli/CliMain.java | 91 ++++++++ .../src/main/java/bisq/core/grpc/CoreApi.java | 171 ++++++-------- .../bisq/core/grpc/CoreOffersService.java | 138 ++++++++++++ .../bisq/core/grpc/GrpcOffersService.java | 105 +++++++++ .../main/java/bisq/core/grpc/GrpcServer.java | 53 +---- .../java/bisq/core/grpc/model/OfferInfo.java | 213 ++++++++++++++++++ proto/src/main/proto/grpc.proto | 76 ++++--- 7 files changed, 670 insertions(+), 177 deletions(-) create mode 100644 core/src/main/java/bisq/core/grpc/CoreOffersService.java create mode 100644 core/src/main/java/bisq/core/grpc/GrpcOffersService.java create mode 100644 core/src/main/java/bisq/core/grpc/model/OfferInfo.java diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index a60d41bd2b0..79ca518bc6b 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -22,10 +22,12 @@ import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.OffersGrpc; import bisq.proto.grpc.PaymentAccountsGrpc; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; @@ -38,14 +40,20 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; +import java.text.DateFormat; import java.text.DecimalFormat; +import java.text.NumberFormat; import java.io.IOException; import java.io.PrintStream; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -57,6 +65,8 @@ import static java.lang.System.exit; import static java.lang.System.out; import static java.util.Collections.singletonList; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; /** * A command-line client for the Bisq gRPC API. @@ -65,6 +75,7 @@ @Slf4j public class CliMain { + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); private static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); private static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") @@ -72,6 +83,7 @@ public class CliMain { BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); private enum Method { + getoffers, createpaymentacct, getpaymentaccts, getversion, @@ -151,6 +163,7 @@ public static void run(String[] args) { })); var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); var paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); var walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); @@ -185,6 +198,45 @@ public static void run(String[] args) { out.println(formatTable(reply.getAddressBalanceInfoList())); return; } + case getoffers: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("no buy/sell direction specified"); + + var direction = nonOptionArgs.get(1).toUpperCase(); + if (!direction.equals("BUY") && !direction.equals("SELL")) + throw new IllegalArgumentException("no buy/sell direction specified"); + + if (nonOptionArgs.size() < 3) + throw new IllegalArgumentException("no fiat currency specified"); + + var fiatCurrency = nonOptionArgs.get(2).toUpperCase(); + + var request = GetOffersRequest.newBuilder() + .setDirection(direction) + .setFiatCurrencyCode(fiatCurrency) + .build(); + var reply = offersService.getOffers(request); + + // TODO Calculate these format specifiers on the fly? + out.println(format("%-8s Price in %s for 1 BTC %s %-23s %-14s %-24s %s", + "Buy/Sell", fiatCurrency, "BTC(min - max)", " " + fiatCurrency + "(min - max)", + "Payment Method", "Creation Date", "ID")); + out.println(reply.getOffersList().stream() + .map(o -> format("%-8s %22s %-25s %12s %-14s %-24s %s", + o.getDirection().equals(BUY.name()) ? SELL.name() : BUY.name(), + formatPrice(o.getPrice()), + o.getMinAmount() != o.getAmount() ? formatSatoshis.apply(o.getMinAmount()) + + " - " + formatSatoshis.apply(o.getAmount()) + : formatSatoshis.apply(o.getAmount()), + o.getMinVolume() != o.getVolume() ? formatVolume(o.getMinVolume()) + + " - " + formatVolume(o.getVolume()) + : formatVolume(o.getVolume()), + o.getPaymentMethodShortName(), + formatDateTime(o.getDate(), true), + o.getId())) + .collect(Collectors.joining("\n"))); + return; + } case createpaymentacct: { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no account name specified"); @@ -295,6 +347,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format("%-22s%-50s%s%n", "getbalance", "", "Get server wallet balance"); stream.format("%-22s%-50s%s%n", "getaddressbalance", "address", "Get server wallet address balance"); stream.format("%-22s%-50s%s%n", "getfundingaddresses", "", "Get BTC funding addresses"); + stream.format("%-22s%-50s%s%n", "getoffers", "buy | sell, fiat currency code", "Get current offers"); stream.format("%-22s%-50s%s%n", "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format("%-22s%-50s%s%n", "getpaymentaccts", "", "Get user payment accounts"); stream.format("%-22s%-50s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet"); @@ -317,4 +370,42 @@ private static String formatTable(List addressBalanceInfo) { info.getNumConfirmations())) .collect(Collectors.joining("\n")); } + + // TODO Find a proper home for these formatting methods, with minimum duplication + // of the :core and :desktop utils they were copied from. + + // Copied from bisq.core.util.FormattingUtils (pass formatted date as well to client) + private static String formatDateTime(long timestamp, boolean useLocaleAndLocalTimezone) { + Date date = new Date(timestamp); + Locale locale = useLocaleAndLocalTimezone ? Locale.getDefault() : Locale.US; + DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); + DateFormat timeInstance = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); + if (!useLocaleAndLocalTimezone) { + dateInstance.setTimeZone(TimeZone.getTimeZone("UTC")); + timeInstance.setTimeZone(TimeZone.getTimeZone("UTC")); + } + return formatDateTime(date, dateInstance, timeInstance); + } + + // Copied from bisq.core.util.FormattingUtils (pass formatted date as well to client) + private static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { + if (date != null) { + return dateFormatter.format(date) + " " + timeFormatter.format(date); + } else { + return ""; + } + } + + private static String formatPrice(long price) { + NUMBER_FORMAT.setMaximumFractionDigits(4); + NUMBER_FORMAT.setMinimumFractionDigits(4); + NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); + return NUMBER_FORMAT.format((double) price / 10000); + } + + private static String formatVolume(long volume) { + NUMBER_FORMAT.setMaximumFractionDigits(0); + NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); + return NUMBER_FORMAT.format((double) volume / 10000); + } } diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 27d4d42573d..88302a82d2a 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -19,16 +19,12 @@ import bisq.core.grpc.model.AddressBalanceInfo; import bisq.core.monetary.Price; -import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; -import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.statistics.TradeStatistics2; import bisq.core.trade.statistics.TradeStatisticsManager; -import bisq.core.user.User; import bisq.common.app.Version; @@ -48,35 +44,95 @@ */ @Slf4j public class CoreApi { + + private final CoreOffersService coreOffersService; private final CorePaymentAccountsService paymentAccountsService; private final CoreWalletsService walletsService; - private final OfferBookService offerBookService; private final TradeStatisticsManager tradeStatisticsManager; - private final CreateOfferService createOfferService; - private final OpenOfferManager openOfferManager; - private final User user; @Inject - public CoreApi(CorePaymentAccountsService paymentAccountsService, + public CoreApi(CoreOffersService coreOffersService, + CorePaymentAccountsService paymentAccountsService, CoreWalletsService walletsService, - OfferBookService offerBookService, - TradeStatisticsManager tradeStatisticsManager, - CreateOfferService createOfferService, - OpenOfferManager openOfferManager, - User user) { + TradeStatisticsManager tradeStatisticsManager) { + this.coreOffersService = coreOffersService; this.paymentAccountsService = paymentAccountsService; this.walletsService = walletsService; - this.offerBookService = offerBookService; this.tradeStatisticsManager = tradeStatisticsManager; - this.createOfferService = createOfferService; - this.openOfferManager = openOfferManager; - this.user = user; } public String getVersion() { return Version.VERSION; } + /////////////////////////////////////////////////////////////////////////////////////////// + // Offers + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getOffers(String direction, String fiatCurrencyCode) { + return coreOffersService.getOffers(direction, fiatCurrencyCode); + } + + public void createOffer(String currencyCode, + String directionAsString, + long priceAsLong, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + String paymentAccountId, + TransactionResultHandler resultHandler) { + coreOffersService.createOffer(currencyCode, + directionAsString, + priceAsLong, + useMarketBasedPrice, + marketPriceMargin, + amountAsLong, + minAmountAsLong, + buyerSecurityDeposit, + paymentAccountId, + resultHandler); + } + + public void createOffer(String offerId, + String currencyCode, + OfferPayload.Direction direction, + Price price, + boolean useMarketBasedPrice, + double marketPriceMargin, + Coin amount, + Coin minAmount, + double buyerSecurityDeposit, + PaymentAccount paymentAccount, + boolean useSavingsWallet, + TransactionResultHandler resultHandler) { + coreOffersService.createOffer(offerId, + currencyCode, + direction, + price, + useMarketBasedPrice, + marketPriceMargin, + amount, + minAmount, + buyerSecurityDeposit, + paymentAccount, + useSavingsWallet, + resultHandler); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PaymentAccounts + /////////////////////////////////////////////////////////////////////////////////////////// + + public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { + paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); + } + + public Set getPaymentAccounts() { + return paymentAccountsService.getPaymentAccounts(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// @@ -120,83 +176,4 @@ public List getTradeStatistics() { public int getNumConfirmationsForMostRecentTransaction(String addressString) { return walletsService.getNumConfirmationsForMostRecentTransaction(addressString); } - - public List getOffers() { - return offerBookService.getOffers(); - } - - public void placeOffer(String currencyCode, - String directionAsString, - long priceAsLong, - boolean useMarketBasedPrice, - double marketPriceMargin, - long amountAsLong, - long minAmountAsLong, - double buyerSecurityDeposit, - String paymentAccountId, - TransactionResultHandler resultHandler) { - String offerId = createOfferService.getRandomOfferId(); - OfferPayload.Direction direction = OfferPayload.Direction.valueOf(directionAsString); - Price price = Price.valueOf(currencyCode, priceAsLong); - Coin amount = Coin.valueOf(amountAsLong); - Coin minAmount = Coin.valueOf(minAmountAsLong); - PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); - // We don't support atm funding from external wallet to keep it simple - boolean useSavingsWallet = true; - - placeOffer(offerId, - currencyCode, - direction, - price, - useMarketBasedPrice, - marketPriceMargin, - amount, - minAmount, - buyerSecurityDeposit, - paymentAccount, - useSavingsWallet, - resultHandler); - } - - public void placeOffer(String offerId, - String currencyCode, - OfferPayload.Direction direction, - Price price, - boolean useMarketBasedPrice, - double marketPriceMargin, - Coin amount, - Coin minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount, - boolean useSavingsWallet, - TransactionResultHandler resultHandler) { - Offer offer = createOfferService.createAndGetOffer(offerId, - direction, - currencyCode, - amount, - minAmount, - price, - useMarketBasedPrice, - marketPriceMargin, - buyerSecurityDeposit, - paymentAccount); - - openOfferManager.placeOffer(offer, - buyerSecurityDeposit, - useSavingsWallet, - resultHandler, - log::error); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // PaymentAccounts - /////////////////////////////////////////////////////////////////////////////////////////// - - public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { - paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); - } - - public Set getPaymentAccounts() { - return paymentAccountsService.getPaymentAccounts(); - } } diff --git a/core/src/main/java/bisq/core/grpc/CoreOffersService.java b/core/src/main/java/bisq/core/grpc/CoreOffersService.java new file mode 100644 index 00000000000..95bf4d478e7 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/CoreOffersService.java @@ -0,0 +1,138 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.grpc; + +import bisq.core.monetary.Price; +import bisq.core.offer.CreateOfferService; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.PaymentAccount; +import bisq.core.trade.handlers.TransactionResultHandler; +import bisq.core.user.User; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.offer.OfferPayload.Direction.BUY; + +@Slf4j +public class CoreOffersService { + + private final CreateOfferService createOfferService; + private final OfferBookService offerBookService; + private final OpenOfferManager openOfferManager; + private final User user; + + @Inject + public CoreOffersService(CreateOfferService createOfferService, + OfferBookService offerBookService, + OpenOfferManager openOfferManager, + User user) { + this.createOfferService = createOfferService; + this.offerBookService = offerBookService; + this.openOfferManager = openOfferManager; + this.user = user; + } + + public List getOffers(String direction, String fiatCurrencyCode) { + List offers = offerBookService.getOffers().stream() + .filter(o -> !o.getDirection().name().equals(direction) + && o.getOfferPayload().getCounterCurrencyCode().equals(fiatCurrencyCode)) + .collect(Collectors.toList()); + + if (direction.equals(BUY.name())) + offers.sort(Comparator.comparing(Offer::getPrice)); + else + offers.sort(Comparator.comparing(Offer::getPrice).reversed()); + + return offers; + } + + public void createOffer(String currencyCode, + String directionAsString, + long priceAsLong, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double buyerSecurityDeposit, + String paymentAccountId, + TransactionResultHandler resultHandler) { + String offerId = createOfferService.getRandomOfferId(); + OfferPayload.Direction direction = OfferPayload.Direction.valueOf(directionAsString); + Price price = Price.valueOf(currencyCode, priceAsLong); + Coin amount = Coin.valueOf(amountAsLong); + Coin minAmount = Coin.valueOf(minAmountAsLong); + PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); + // We don't support atm funding from external wallet to keep it simple + boolean useSavingsWallet = true; + + //noinspection ConstantConditions + createOffer(offerId, + currencyCode, + direction, + price, + useMarketBasedPrice, + marketPriceMargin, + amount, + minAmount, + buyerSecurityDeposit, + paymentAccount, + useSavingsWallet, + resultHandler); + } + + public void createOffer(String offerId, + String currencyCode, + OfferPayload.Direction direction, + Price price, + boolean useMarketBasedPrice, + double marketPriceMargin, + Coin amount, + Coin minAmount, + double buyerSecurityDeposit, + PaymentAccount paymentAccount, + boolean useSavingsWallet, + TransactionResultHandler resultHandler) { + Offer offer = createOfferService.createAndGetOffer(offerId, + direction, + currencyCode, + amount, + minAmount, + price, + useMarketBasedPrice, + marketPriceMargin, + buyerSecurityDeposit, + paymentAccount); + + openOfferManager.placeOffer(offer, + buyerSecurityDeposit, + useSavingsWallet, + resultHandler, + log::error); + } +} diff --git a/core/src/main/java/bisq/core/grpc/GrpcOffersService.java b/core/src/main/java/bisq/core/grpc/GrpcOffersService.java new file mode 100644 index 00000000000..c0f90a8abb9 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/GrpcOffersService.java @@ -0,0 +1,105 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.grpc; + +import bisq.core.grpc.model.OfferInfo; +import bisq.core.trade.handlers.TransactionResultHandler; + +import bisq.proto.grpc.CreateOfferReply; +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.GetOffersReply; +import bisq.proto.grpc.GetOffersRequest; +import bisq.proto.grpc.OffersGrpc; + +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class GrpcOffersService extends OffersGrpc.OffersImplBase { + + private final CoreApi coreApi; + + @Inject + public GrpcOffersService(CoreApi coreApi) { + this.coreApi = coreApi; + } + + @Override + public void getOffers(GetOffersRequest req, + StreamObserver responseObserver) { + // The client cannot see bisq.core.Offer or its fromProto method. + // We use the lighter weight OfferInfo proto wrapper instead, containing just + // enough fields to view and create offers. + List result = coreApi.getOffers(req.getDirection(), req.getFiatCurrencyCode()) + .stream().map(offer -> new OfferInfo.OfferInfoBuilder() + .withId(offer.getId()) + .withDirection(offer.getDirection().name()) + .withPrice(offer.getPrice().getValue()) + .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) + .withMarketPriceMargin(offer.getMarketPriceMargin()) + .withAmount(offer.getAmount().value) + .withMinAmount(offer.getMinAmount().value) + .withVolume(offer.getVolume().getValue()) + .withMinVolume(offer.getMinVolume().getValue()) + .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) + .withPaymentAccountId("") // only used when creating offer (?) + .withPaymentMethodId(offer.getPaymentMethod().getId()) + .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) + .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) + .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) + .withDate(offer.getDate().getTime()) + .build()) + .collect(Collectors.toList()); + + var reply = GetOffersReply.newBuilder() + .addAllOffers( + result.stream() + .map(OfferInfo::toProtoMessage) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void createOffer(CreateOfferRequest req, + StreamObserver responseObserver) { + TransactionResultHandler resultHandler = transaction -> { + CreateOfferReply reply = CreateOfferReply.newBuilder().setResult(true).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }; + coreApi.createOffer( + req.getCurrencyCode(), + req.getDirection(), + req.getPrice(), + req.getUseMarketBasedPrice(), + req.getMarketPriceMargin(), + req.getAmount(), + req.getMinAmount(), + req.getBuyerSecurityDeposit(), + req.getPaymentAccountId(), + resultHandler); + } +} diff --git a/core/src/main/java/bisq/core/grpc/GrpcServer.java b/core/src/main/java/bisq/core/grpc/GrpcServer.java index 6fa6dad9faf..4b5d195a2e1 100644 --- a/core/src/main/java/bisq/core/grpc/GrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/GrpcServer.java @@ -17,24 +17,16 @@ package bisq.core.grpc; -import bisq.core.offer.Offer; -import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.statistics.TradeStatistics2; import bisq.common.config.Config; -import bisq.proto.grpc.GetOffersGrpc; -import bisq.proto.grpc.GetOffersReply; -import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetTradeStatisticsGrpc; import bisq.proto.grpc.GetTradeStatisticsReply; import bisq.proto.grpc.GetTradeStatisticsRequest; import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.GetVersionReply; import bisq.proto.grpc.GetVersionRequest; -import bisq.proto.grpc.PlaceOfferGrpc; -import bisq.proto.grpc.PlaceOfferReply; -import bisq.proto.grpc.PlaceOfferRequest; import io.grpc.Server; import io.grpc.ServerBuilder; @@ -58,16 +50,16 @@ public class GrpcServer { @Inject public GrpcServer(Config config, CoreApi coreApi, + GrpcOffersService offersService, GrpcPaymentAccountsService paymentAccountsService, - GrpcWalletsService walletService) { + GrpcWalletsService walletsService) { this.coreApi = coreApi; this.server = ServerBuilder.forPort(config.apiPort) .addService(new GetVersionService()) .addService(new GetTradeStatisticsService()) - .addService(new GetOffersService()) - .addService(new PlaceOfferService()) + .addService(offersService) .addService(paymentAccountsService) - .addService(walletService) + .addService(walletsService) .intercept(new PasswordAuthInterceptor(config.apiPassword)) .build(); } @@ -94,7 +86,6 @@ public void getVersion(GetVersionRequest req, StreamObserver re } } - class GetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase { @Override public void getTradeStatistics(GetTradeStatisticsRequest req, @@ -109,40 +100,4 @@ public void getTradeStatistics(GetTradeStatisticsRequest req, responseObserver.onCompleted(); } } - - class GetOffersService extends GetOffersGrpc.GetOffersImplBase { - @Override - public void getOffers(GetOffersRequest req, StreamObserver responseObserver) { - - var tradeStatistics = coreApi.getOffers().stream() - .map(Offer::toProtoMessage) - .collect(Collectors.toList()); - - var reply = GetOffersReply.newBuilder().addAllOffers(tradeStatistics).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } - } - - class PlaceOfferService extends PlaceOfferGrpc.PlaceOfferImplBase { - @Override - public void placeOffer(PlaceOfferRequest req, StreamObserver responseObserver) { - TransactionResultHandler resultHandler = transaction -> { - PlaceOfferReply reply = PlaceOfferReply.newBuilder().setResult(true).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - }; - coreApi.placeOffer( - req.getCurrencyCode(), - req.getDirection(), - req.getPrice(), - req.getUseMarketBasedPrice(), - req.getMarketPriceMargin(), - req.getAmount(), - req.getMinAmount(), - req.getBuyerSecurityDeposit(), - req.getPaymentAccountId(), - resultHandler); - } - } } diff --git a/core/src/main/java/bisq/core/grpc/model/OfferInfo.java b/core/src/main/java/bisq/core/grpc/model/OfferInfo.java new file mode 100644 index 00000000000..c753004e7b7 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/model/OfferInfo.java @@ -0,0 +1,213 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.grpc.model; + +import bisq.common.Payload; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode +@ToString +@Getter +public class OfferInfo implements Payload { + + private final String id; + private final String direction; + private final long price; + private final boolean useMarketBasedPrice; + private final double marketPriceMargin; + private final long amount; + private final long minAmount; + private final long volume; + private final long minVolume; + private final long buyerSecurityDeposit; + private final String paymentAccountId; // only used when creating offer + private final String paymentMethodId; + private final String paymentMethodShortName; + // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency + // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + private final String baseCurrencyCode; + private final String counterCurrencyCode; + private final long date; + + public OfferInfo(OfferInfoBuilder builder) { + this.id = builder.id; + this.direction = builder.direction; + this.price = builder.price; + this.useMarketBasedPrice = builder.useMarketBasedPrice; + this.marketPriceMargin = builder.marketPriceMargin; + this.amount = builder.amount; + this.minAmount = builder.minAmount; + this.volume = builder.volume; + this.minVolume = builder.minVolume; + this.buyerSecurityDeposit = builder.buyerSecurityDeposit; + this.paymentAccountId = builder.paymentAccountId; + this.paymentMethodId = builder.paymentMethodId; + this.paymentMethodShortName = builder.paymentMethodShortName; + this.baseCurrencyCode = builder.baseCurrencyCode; + this.counterCurrencyCode = builder.counterCurrencyCode; + this.date = builder.date; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.OfferInfo toProtoMessage() { + return bisq.proto.grpc.OfferInfo.newBuilder() + .setId(id) + .setDirection(direction) + .setPrice(price) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setAmount(amount) + .setMinAmount(minAmount) + .setVolume(volume) + .setMinVolume(minVolume) + .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setPaymentAccountId(paymentAccountId) + .setPaymentMethodId(paymentMethodId) + .setPaymentMethodShortName(paymentMethodShortName) + .setBaseCurrencyCode(baseCurrencyCode) + .setCounterCurrencyCode(counterCurrencyCode) + .setDate(date) + .build(); + } + + public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { + /* + TODO (needed by createoffer) + return new OfferInfo(proto.getOfferPayload().getId(), + proto.getOfferPayload().getDate()); + */ + return null; + } + + /** + * OfferInfoBuilder helps avoid bungling use of the OfferInfo constructor + * argument list, which is large, of all one type, and not wise to overload. + */ + public static class OfferInfoBuilder { + private String id; + private String direction; + private long price; + private boolean useMarketBasedPrice; + private double marketPriceMargin; + private long amount; + private long minAmount; + private long volume; + private long minVolume; + private long buyerSecurityDeposit; + private String paymentAccountId; + private String paymentMethodId; + private String paymentMethodShortName; + private String baseCurrencyCode; + private String counterCurrencyCode; + private long date; + + public OfferInfoBuilder() { + } + + public OfferInfoBuilder withId(String id) { + this.id = id; + return this; + } + + public OfferInfoBuilder withDirection(String direction) { + this.direction = direction; + return this; + } + + public OfferInfoBuilder withPrice(long price) { + this.price = price; + return this; + } + + public OfferInfoBuilder withUseMarketBasedPrice(boolean useMarketBasedPrice) { + this.useMarketBasedPrice = useMarketBasedPrice; + return this; + } + + public OfferInfoBuilder withMarketPriceMargin(double useMarketBasedPrice) { + this.marketPriceMargin = useMarketBasedPrice; + return this; + } + + public OfferInfoBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public OfferInfoBuilder withMinAmount(long minAmount) { + this.minAmount = minAmount; + return this; + } + + public OfferInfoBuilder withVolume(long volume) { + this.volume = volume; + return this; + } + + public OfferInfoBuilder withMinVolume(long minVolume) { + this.minVolume = minVolume; + return this; + } + + public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { + this.buyerSecurityDeposit = buyerSecurityDeposit; + return this; + } + + public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { + this.paymentAccountId = paymentAccountId; + return this; + } + + public OfferInfoBuilder withPaymentMethodId(String paymentMethodId) { + this.paymentMethodId = paymentMethodId; + return this; + } + + public OfferInfoBuilder withPaymentMethodShortName(String paymentMethodShortName) { + this.paymentMethodShortName = paymentMethodShortName; + return this; + } + + public OfferInfoBuilder withBaseCurrencyCode(String baseCurrencyCode) { + this.baseCurrencyCode = baseCurrencyCode; + return this; + } + + public OfferInfoBuilder withCounterCurrencyCode(String counterCurrencyCode) { + this.counterCurrencyCode = counterCurrencyCode; + return this; + } + + public OfferInfoBuilder withDate(long date) { + this.date = date; + return this; + } + + public OfferInfo build() { + return new OfferInfo(this); + } + } +} diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 7168d04928c..730920b0622 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -40,35 +40,58 @@ message GetVersionReply { } /////////////////////////////////////////////////////////////////////////////////////////// -// TradeStatistics +// Offers /////////////////////////////////////////////////////////////////////////////////////////// -service GetTradeStatistics { - rpc GetTradeStatistics (GetTradeStatisticsRequest) returns (GetTradeStatisticsReply) { +service Offers { + rpc GetOffers (GetOffersRequest) returns (GetOffersReply) { + } + rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) { } } -message GetTradeStatisticsRequest { +message GetOffersRequest { + string direction = 1; + string fiatCurrencyCode = 2; } -message GetTradeStatisticsReply { - repeated TradeStatistics2 TradeStatistics = 1; +message GetOffersReply { + repeated OfferInfo offers = 1; } -/////////////////////////////////////////////////////////////////////////////////////////// -// Offer -/////////////////////////////////////////////////////////////////////////////////////////// - -service GetOffers { - rpc GetOffers (GetOffersRequest) returns (GetOffersReply) { - } +message CreateOfferRequest { + string currencyCode = 1; // TODO switch order w/ direction field in next PR + string direction = 2; + uint64 price = 3; + bool useMarketBasedPrice = 4; + double marketPriceMargin = 5; + uint64 amount = 6; + uint64 minAmount = 7; + double buyerSecurityDeposit = 8; + string paymentAccountId = 9; } -message GetOffersRequest { +message CreateOfferReply { + bool result = 1; } -message GetOffersReply { - repeated Offer offers = 1; +message OfferInfo { + string id = 1; + string direction = 2; + uint64 price = 3; + bool useMarketBasedPrice = 4; + double marketPriceMargin = 5; + uint64 amount = 6; + uint64 minAmount = 7; + uint64 volume = 8; + uint64 minVolume = 9; + uint64 buyerSecurityDeposit = 10; + string paymentAccountId = 11; // only used when creating offer + string paymentMethodId = 12; + string paymentMethodShortName = 13; + string baseCurrencyCode = 14; + string counterCurrencyCode = 15; + uint64 date = 16; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -99,28 +122,19 @@ message GetPaymentAccountsReply { } /////////////////////////////////////////////////////////////////////////////////////////// -// PlaceOffer +// TradeStatistics /////////////////////////////////////////////////////////////////////////////////////////// -service PlaceOffer { - rpc PlaceOffer (PlaceOfferRequest) returns (PlaceOfferReply) { +service GetTradeStatistics { + rpc GetTradeStatistics (GetTradeStatisticsRequest) returns (GetTradeStatisticsReply) { } } -message PlaceOfferRequest { - string currencyCode = 1; - string direction = 2; - uint64 price = 3; - bool useMarketBasedPrice = 4; - double marketPriceMargin = 5; - uint64 amount = 6; - uint64 minAmount = 7; - double buyerSecurityDeposit = 8; - string paymentAccountId = 9; +message GetTradeStatisticsRequest { } -message PlaceOfferReply { - bool result = 1; +message GetTradeStatisticsReply { + repeated TradeStatistics2 TradeStatistics = 1; } /////////////////////////////////////////////////////////////////////////////////////////// From 1756258e818ec819dffc511ec71c554ab404fc0b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:09:18 -0300 Subject: [PATCH 23/35] Do not use protobuf.OfferPayload.Direction in client --- cli/src/main/java/bisq/cli/CliMain.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 79ca518bc6b..9d0f348e560 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -65,8 +65,6 @@ import static java.lang.System.exit; import static java.lang.System.out; import static java.util.Collections.singletonList; -import static protobuf.OfferPayload.Direction.BUY; -import static protobuf.OfferPayload.Direction.SELL; /** * A command-line client for the Bisq gRPC API. @@ -223,7 +221,7 @@ public static void run(String[] args) { "Payment Method", "Creation Date", "ID")); out.println(reply.getOffersList().stream() .map(o -> format("%-8s %22s %-25s %12s %-14s %-24s %s", - o.getDirection().equals(BUY.name()) ? SELL.name() : BUY.name(), + o.getDirection().equals("BUY") ? "SELL" : "BUY", formatPrice(o.getPrice()), o.getMinAmount() != o.getAmount() ? formatSatoshis.apply(o.getMinAmount()) + " - " + formatSatoshis.apply(o.getAmount()) From 4778976b6bb78b73537571b2ce8089fe6f427dbf Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:24:49 -0300 Subject: [PATCH 24/35] Fix comments --- core/src/main/java/bisq/core/grpc/model/OfferInfo.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/model/OfferInfo.java b/core/src/main/java/bisq/core/grpc/model/OfferInfo.java index c753004e7b7..0b2dc5fe0cd 100644 --- a/core/src/main/java/bisq/core/grpc/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/grpc/model/OfferInfo.java @@ -94,16 +94,18 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { /* - TODO (needed by createoffer) + TODO (will be needed by the createoffer method) return new OfferInfo(proto.getOfferPayload().getId(), proto.getOfferPayload().getDate()); */ return null; } - /** - * OfferInfoBuilder helps avoid bungling use of the OfferInfo constructor - * argument list, which is large, of all one type, and not wise to overload. + /* + * OfferInfoBuilder helps avoid bungling use of a large OfferInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. */ public static class OfferInfoBuilder { private String id; From b25abf1b6bf14aa3332fccb8e7b312ba43425673 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 21 Jun 2020 19:46:37 -0300 Subject: [PATCH 25/35] Refactor CLI output table formatting This change moves logic for formatting BTC balances, dates and tables out of CliMain. Two new output formatting classes were added: CurrencyFormat and TableFormat. --- cli/src/main/java/bisq/cli/CliMain.java | 96 ++------------- .../main/java/bisq/cli/CurrencyFormat.java | 47 +++++++ cli/src/main/java/bisq/cli/TableFormat.java | 116 ++++++++++++++++++ 3 files changed, 170 insertions(+), 89 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/CurrencyFormat.java create mode 100644 cli/src/main/java/bisq/cli/TableFormat.java diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 9d0f348e560..f768430b624 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,7 +17,6 @@ package bisq.cli; -import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; @@ -40,26 +39,18 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; -import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.NumberFormat; - import java.io.IOException; import java.io.PrintStream; -import java.math.BigDecimal; -import java.math.RoundingMode; - -import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.TimeZone; import java.util.concurrent.TimeUnit; -import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatOfferTable; import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.exit; @@ -73,13 +64,6 @@ @Slf4j public class CliMain { - private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); - private static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); - private static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); - @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - private static final Function formatSatoshis = (sats) -> - BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); - private enum Method { getoffers, createpaymentacct, @@ -176,7 +160,7 @@ public static void run(String[] args) { case getbalance: { var request = GetBalanceRequest.newBuilder().build(); var reply = walletsService.getBalance(request); - var btcBalance = formatSatoshis.apply(reply.getBalance()); + var btcBalance = formatSatoshis(reply.getBalance()); out.println(btcBalance); return; } @@ -187,13 +171,13 @@ public static void run(String[] args) { var request = GetAddressBalanceRequest.newBuilder() .setAddress(nonOptionArgs.get(1)).build(); var reply = walletsService.getAddressBalance(request); - out.println(formatTable(singletonList(reply.getAddressBalanceInfo()))); + out.println(formatAddressBalanceTbl(singletonList(reply.getAddressBalanceInfo()))); return; } case getfundingaddresses: { var request = GetFundingAddressesRequest.newBuilder().build(); var reply = walletsService.getFundingAddresses(request); - out.println(formatTable(reply.getAddressBalanceInfoList())); + out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList())); return; } case getoffers: { @@ -214,25 +198,7 @@ public static void run(String[] args) { .setFiatCurrencyCode(fiatCurrency) .build(); var reply = offersService.getOffers(request); - - // TODO Calculate these format specifiers on the fly? - out.println(format("%-8s Price in %s for 1 BTC %s %-23s %-14s %-24s %s", - "Buy/Sell", fiatCurrency, "BTC(min - max)", " " + fiatCurrency + "(min - max)", - "Payment Method", "Creation Date", "ID")); - out.println(reply.getOffersList().stream() - .map(o -> format("%-8s %22s %-25s %12s %-14s %-24s %s", - o.getDirection().equals("BUY") ? "SELL" : "BUY", - formatPrice(o.getPrice()), - o.getMinAmount() != o.getAmount() ? formatSatoshis.apply(o.getMinAmount()) - + " - " + formatSatoshis.apply(o.getAmount()) - : formatSatoshis.apply(o.getAmount()), - o.getMinVolume() != o.getVolume() ? formatVolume(o.getMinVolume()) - + " - " + formatVolume(o.getVolume()) - : formatVolume(o.getVolume()), - o.getPaymentMethodShortName(), - formatDateTime(o.getDate(), true), - o.getId())) - .collect(Collectors.joining("\n"))); + out.println(formatOfferTable(reply.getOffersList(), fiatCurrency)); return; } case createpaymentacct: { @@ -358,52 +324,4 @@ private static void printHelp(OptionParser parser, PrintStream stream) { ex.printStackTrace(stream); } } - - private static String formatTable(List addressBalanceInfo) { - return format("%-35s %13s %s%n", "Address", "Balance", "Confirmations") - + addressBalanceInfo.stream() - .map(info -> format("%-35s %13s %14d", - info.getAddress(), - formatSatoshis.apply(info.getBalance()), - info.getNumConfirmations())) - .collect(Collectors.joining("\n")); - } - - // TODO Find a proper home for these formatting methods, with minimum duplication - // of the :core and :desktop utils they were copied from. - - // Copied from bisq.core.util.FormattingUtils (pass formatted date as well to client) - private static String formatDateTime(long timestamp, boolean useLocaleAndLocalTimezone) { - Date date = new Date(timestamp); - Locale locale = useLocaleAndLocalTimezone ? Locale.getDefault() : Locale.US; - DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); - DateFormat timeInstance = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); - if (!useLocaleAndLocalTimezone) { - dateInstance.setTimeZone(TimeZone.getTimeZone("UTC")); - timeInstance.setTimeZone(TimeZone.getTimeZone("UTC")); - } - return formatDateTime(date, dateInstance, timeInstance); - } - - // Copied from bisq.core.util.FormattingUtils (pass formatted date as well to client) - private static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { - if (date != null) { - return dateFormatter.format(date) + " " + timeFormatter.format(date); - } else { - return ""; - } - } - - private static String formatPrice(long price) { - NUMBER_FORMAT.setMaximumFractionDigits(4); - NUMBER_FORMAT.setMinimumFractionDigits(4); - NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); - return NUMBER_FORMAT.format((double) price / 10000); - } - - private static String formatVolume(long volume) { - NUMBER_FORMAT.setMaximumFractionDigits(0); - NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); - return NUMBER_FORMAT.format((double) volume / 10000); - } } diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java new file mode 100644 index 00000000000..859dc4e8e8f --- /dev/null +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -0,0 +1,47 @@ +package bisq.cli; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import java.util.Locale; + +class CurrencyFormat { + + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); + + static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); + static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + static final String formatSatoshis(long sats) { + return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); + } + + static String formatAmountRange(long minAmount, long amount) { + return minAmount != amount + ? formatSatoshis(minAmount) + " - " + formatSatoshis(amount) + : formatSatoshis(amount); + } + + static String formatVolumeRange(long minVolume, long volume) { + return minVolume != volume + ? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume) + : formatOfferVolume(volume); + } + + static String formatOfferPrice(long price) { + NUMBER_FORMAT.setMaximumFractionDigits(4); + NUMBER_FORMAT.setMinimumFractionDigits(4); + NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); + return NUMBER_FORMAT.format((double) price / 10000); + } + + static String formatOfferVolume(long volume) { + NUMBER_FORMAT.setMaximumFractionDigits(0); + NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); + return NUMBER_FORMAT.format((double) volume / 10000); + } +} diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java new file mode 100644 index 00000000000..bee9e0c73a2 --- /dev/null +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -0,0 +1,116 @@ +package bisq.cli; + +import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.OfferInfo; + +import java.text.DateFormat; + +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import static bisq.cli.CurrencyFormat.formatAmountRange; +import static bisq.cli.CurrencyFormat.formatOfferPrice; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.CurrencyFormat.formatVolumeRange; +import static com.google.common.base.Strings.padEnd; +import static java.lang.String.format; +import static java.text.DateFormat.DEFAULT; +import static java.text.DateFormat.getDateInstance; +import static java.text.DateFormat.getTimeInstance; +import static java.util.Collections.max; +import static java.util.Comparator.comparing; +import static java.util.TimeZone.getTimeZone; + +class TableFormat { + + // For inserting 2 spaces between column headers. + private static final String COL_HEADER_DELIMITER = " "; + + // Table column header format specs, right padded with two spaces. In some cases + // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the + // expected max data string length is accounted for. In others, the column header length + // are expected to be greater than any column value length. + private static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); + private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date", 24, ' '); + private static final String COL_HEADER_DIRECTION = "Buy/Sell"; + private static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; + private static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; + private static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); + private static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); + + static String formatAddressBalanceTbl(List addressBalanceInfo) { + return format("%-35s %13s %s%n", "Address", "Balance", "Confirmations") + + addressBalanceInfo.stream() + .map(info -> format("%-35s %13s %14d", + info.getAddress(), + formatSatoshis(info.getBalance()), + info.getNumConfirmations())) + .collect(Collectors.joining("\n")); + } + + static String formatOfferTable(List offerInfo, String fiatCurrency) { + + // Some column values might be longer than header, so we need to calculated them. + int paymentMethodColWidth = getLengthOfLongestColumn( + COL_HEADER_PAYMENT_METHOD.length(), + offerInfo.stream() + .map(o -> o.getPaymentMethodShortName()) + .collect(Collectors.toList())); + + String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER + + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrency + + COL_HEADER_AMOUNT + COL_HEADER_DELIMITER + + COL_HEADER_VOLUME + COL_HEADER_DELIMITER // includes %s -> fiatCurrency + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + + COL_HEADER_UUID.trim() + "%n"; + String headerLine = format(headersFormat, fiatCurrency, fiatCurrency); + + String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" // left + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify to end of hdr + + " %-" + (COL_HEADER_AMOUNT.length() - 1) + "s" // left justify + + " %" + COL_HEADER_VOLUME.length() + "s" // right justify + + " %-" + paymentMethodColWidth + "s" // left justify + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" // left justify + + " %-" + COL_HEADER_UUID.length() + "s"; + return headerLine + + offerInfo.stream() + .map(o -> format(colDataFormat, + o.getDirection().equals("BUY") ? "SELL" : "BUY", + formatOfferPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatDateTime(o.getDate(), true), + o.getId())) + .collect(Collectors.joining("\n")); + } + + // Return length of the longest string value, or the header.len, whichever is greater. + private static int getLengthOfLongestColumn(int headerLength, List strings) { + int longest = max(strings, comparing(s -> s.length())).length(); + return longest > headerLength ? longest : headerLength; + } + + private static String formatDateTime(long timestamp, boolean useLocaleAndLocalTimezone) { + Date date = new Date(timestamp); + Locale locale = useLocaleAndLocalTimezone ? Locale.getDefault() : Locale.US; + DateFormat dateInstance = getDateInstance(DEFAULT, locale); + DateFormat timeInstance = getTimeInstance(DEFAULT, locale); + if (!useLocaleAndLocalTimezone) { + dateInstance.setTimeZone(getTimeZone("UTC")); + timeInstance.setTimeZone(getTimeZone("UTC")); + } + return formatDateTime(date, dateInstance, timeInstance); + } + + private static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { + if (date != null) { + return dateFormatter.format(date) + " " + timeFormatter.format(date); + } else { + return ""; + } + } +} From a48af7c05276f8a219994a776d9c4f4a30b97fb9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 22 Jun 2020 14:43:59 -0300 Subject: [PATCH 26/35] Add 'getoffers' unit tests --- cli/test.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cli/test.sh b/cli/test.sh index eaa64c9ebc8..c44c85abf44 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -176,6 +176,35 @@ [ "$status" -eq 0 ] } +@test "test getoffers missing direction argument" { + run ./bisq-cli --password=xyz getoffers + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no buy/sell direction specified" ] +} + +@test "test getoffers missing ccy argument" { + run ./bisq-cli --password=xyz getoffers buy + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: no fiat currency specified" ] +} + +@test "test getoffers buy eur check return status" { + run ./bisq-cli --password=xyz getoffers buy eur + [ "$status" -eq 0 ] +} + +@test "test getoffers buy eur check return status" { + run ./bisq-cli --password=xyz getoffers buy eur + [ "$status" -eq 0 ] +} + +@test "test getoffers sell gbp check return status" { + run ./bisq-cli --password=xyz getoffers sell gbp + [ "$status" -eq 0 ] +} + @test "test help displayed on stderr if no options or arguments" { run ./bisq-cli [ "$status" -eq 1 ] From 61285a760280eb42d00ef703f019a97ce806b0f9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 22 Jun 2020 15:08:46 -0300 Subject: [PATCH 27/35] Do not change case of input params in client This commit is for a change requested in PR 4308: https://github.com/bisq-network/bisq/pull/4308#pullrequestreview-435055483 ".toUpperCase() seems misplaced here. It would soon get repetive. Whether the underlying logic differentiates between capitalizations is a low-level implementation detail and would do better at the lowest practical level." --- cli/src/main/java/bisq/cli/CliMain.java | 8 ++++---- core/src/main/java/bisq/core/grpc/CoreOffersService.java | 4 ++-- .../java/bisq/core/grpc/CorePaymentAccountsService.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index f768430b624..8e5d275c761 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -184,14 +184,14 @@ public static void run(String[] args) { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no buy/sell direction specified"); - var direction = nonOptionArgs.get(1).toUpperCase(); - if (!direction.equals("BUY") && !direction.equals("SELL")) + var direction = nonOptionArgs.get(1); + if (!direction.equalsIgnoreCase("BUY") && !direction.equalsIgnoreCase("SELL")) throw new IllegalArgumentException("no buy/sell direction specified"); if (nonOptionArgs.size() < 3) throw new IllegalArgumentException("no fiat currency specified"); - var fiatCurrency = nonOptionArgs.get(2).toUpperCase(); + var fiatCurrency = nonOptionArgs.get(2); var request = GetOffersRequest.newBuilder() .setDirection(direction) @@ -215,7 +215,7 @@ public static void run(String[] args) { if (nonOptionArgs.size() < 4) throw new IllegalArgumentException("no fiat currency specified"); - var fiatCurrencyCode = nonOptionArgs.get(3).toUpperCase(); + var fiatCurrencyCode = nonOptionArgs.get(3); var request = CreatePaymentAccountRequest.newBuilder() .setAccountName(accountName) diff --git a/core/src/main/java/bisq/core/grpc/CoreOffersService.java b/core/src/main/java/bisq/core/grpc/CoreOffersService.java index 95bf4d478e7..3ac365bd888 100644 --- a/core/src/main/java/bisq/core/grpc/CoreOffersService.java +++ b/core/src/main/java/bisq/core/grpc/CoreOffersService.java @@ -60,8 +60,8 @@ public CoreOffersService(CreateOfferService createOfferService, public List getOffers(String direction, String fiatCurrencyCode) { List offers = offerBookService.getOffers().stream() - .filter(o -> !o.getDirection().name().equals(direction) - && o.getOfferPayload().getCounterCurrencyCode().equals(fiatCurrencyCode)) + .filter(o -> !o.getDirection().name().equalsIgnoreCase(direction) + && o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(fiatCurrencyCode)) .collect(Collectors.toList()); if (direction.equals(BUY.name())) diff --git a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java index c6c2613e1c7..9a7183d21ab 100644 --- a/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/grpc/CorePaymentAccountsService.java @@ -58,7 +58,7 @@ public void createPaymentAccount(String accountName, String accountNumber, Strin paymentAccount.init(); paymentAccount.setAccountName(accountName); ((PerfectMoneyAccount) paymentAccount).setAccountNr(accountNumber); - paymentAccount.setSingleTradeCurrency(new FiatCurrency(fiatCurrencyCode)); + paymentAccount.setSingleTradeCurrency(new FiatCurrency(fiatCurrencyCode.toUpperCase())); user.addPaymentAccount(paymentAccount); // Don't do this on mainnet until thoroughly tested. From 0d9bdefa006843fb1f8b555672377ad0d85f493c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 22 Jun 2020 15:23:27 -0300 Subject: [PATCH 28/35] Add 'getoffers' smoke test --- .../java/bisq/cli/GetOffersSmokeTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 cli/src/test/java/bisq/cli/GetOffersSmokeTest.java diff --git a/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java new file mode 100644 index 00000000000..49f849e0cc7 --- /dev/null +++ b/cli/src/test/java/bisq/cli/GetOffersSmokeTest.java @@ -0,0 +1,39 @@ +package bisq.cli; + +import static java.lang.System.out; + +/** + Smoke tests for getoffers method. Useful for examining the format of the console output. + + Prerequisites: + + - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` + + This can be run on mainnet. + */ +public class GetOffersSmokeTest { + + public static void main(String[] args) { + + out.println(">>> getoffers buy usd"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "usd"}); + out.println(">>> getoffers sell usd"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "usd"}); + + out.println(">>> getoffers buy eur"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "eur"}); + out.println(">>> getoffers sell eur"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "eur"}); + + out.println(">>> getoffers buy gbp"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "gbp"}); + out.println(">>> getoffers sell gbp"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "gbp"}); + + out.println(">>> getoffers buy brl"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "buy", "brl"}); + out.println(">>> getoffers sell brl"); + CliMain.main(new String[]{"--password=xyz", "getoffers", "sell", "brl"}); + } + +} From 8dcfa50bde222b01b78bcc88e685109f1657d63d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 22 Jun 2020 17:49:46 -0300 Subject: [PATCH 29/35] Define reusable headers from balance-info tbl --- cli/src/main/java/bisq/cli/TableFormat.java | 36 +++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index bee9e0c73a2..4803a06a361 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -15,6 +15,7 @@ import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.cli.CurrencyFormat.formatVolumeRange; import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; import static java.lang.String.format; import static java.text.DateFormat.DEFAULT; import static java.text.DateFormat.getDateInstance; @@ -32,18 +33,27 @@ class TableFormat { // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the // expected max data string length is accounted for. In others, the column header length // are expected to be greater than any column value length. + private static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' '); private static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); + private static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); + private static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date", 24, ' '); - private static final String COL_HEADER_DIRECTION = "Buy/Sell"; + private static final String COL_HEADER_DIRECTION = "Buy/Sell"; // TODO "Take Offer to private static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; private static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; private static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); private static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); static String formatAddressBalanceTbl(List addressBalanceInfo) { - return format("%-35s %13s %s%n", "Address", "Balance", "Confirmations") + String headerLine = (COL_HEADER_ADDRESS + COL_HEADER_DELIMITER + + COL_HEADER_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n"); + String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // left justify + + " %" + COL_HEADER_BALANCE.length() + "s" // right justify + + " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // right justify + return headerLine + addressBalanceInfo.stream() - .map(info -> format("%-35s %13s %14d", + .map(info -> format(colDataFormat, info.getAddress(), formatSatoshis(info.getBalance()), info.getNumConfirmations())) @@ -88,6 +98,26 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { .collect(Collectors.joining("\n")); } + static String formatPaymentAcctTbl() { + /* + case getpaymentaccts: { + var request = GetPaymentAccountsRequest.newBuilder().build(); + var reply = paymentAccountsService.getPaymentAccounts(request); + var columnFormatSpec = "%-41s %-25s %-14s %s"; + out.println(format(columnFormatSpec, "ID", "Name", "Currency", "Payment Method")); + out.println(reply.getPaymentAccountsList().stream() + .map(a -> format(columnFormatSpec, + a.getId(), + a.getAccountName(), + a.getSelectedTradeCurrency().getCode(), + a.getPaymentMethod().getId())) + .collect(Collectors.joining("\n"))); + return; + } + */ + return ""; + } + // Return length of the longest string value, or the header.len, whichever is greater. private static int getLengthOfLongestColumn(int headerLength, List strings) { int longest = max(strings, comparing(s -> s.length())).length(); From 52529a912eeae18c78dae9ed00e8c73d6357917f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 22 Jun 2020 18:20:09 -0300 Subject: [PATCH 30/35] Move getpaymentaccts tbl formatting to TableFormat Created a new TableFormat.formatPaymentAcctTbl method. Also: * Defined new "Currency" and "Name" column headers in TableFormat. * Changed syntax of stream().map() calls in some TableFormat methods. * Fixed verbose return statement in TableFormat.getLengthOfLongestColumn. This commit is out of scope for the getoffers PR (4329), but is included as part of the migration of all console tbl formatting from the client into TableFormat. --- cli/src/main/java/bisq/cli/CliMain.java | 12 +---- cli/src/main/java/bisq/cli/TableFormat.java | 55 +++++++++++++-------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 8e5d275c761..cb99de79e3b 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -44,13 +44,13 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.cli.TableFormat.formatAddressBalanceTbl; import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.cli.TableFormat.formatPaymentAcctTbl; import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.exit; @@ -228,15 +228,7 @@ public static void run(String[] args) { case getpaymentaccts: { var request = GetPaymentAccountsRequest.newBuilder().build(); var reply = paymentAccountsService.getPaymentAccounts(request); - var columnFormatSpec = "%-41s %-25s %-14s %s"; - out.println(format(columnFormatSpec, "ID", "Name", "Currency", "Payment Method")); - out.println(reply.getPaymentAccountsList().stream() - .map(a -> format(columnFormatSpec, - a.getId(), - a.getAccountName(), - a.getSelectedTradeCurrency().getCode(), - a.getPaymentMethod().getId())) - .collect(Collectors.joining("\n"))); + out.println(formatPaymentAcctTbl(reply.getPaymentAccountsList())); return; } case lockwallet: { diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 4803a06a361..bb1ef4e3b2a 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -3,6 +3,8 @@ import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.OfferInfo; +import protobuf.PaymentAccount; + import java.text.DateFormat; import java.util.Date; @@ -38,7 +40,9 @@ class TableFormat { private static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); private static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date", 24, ' '); + private static final String COL_HEADER_CURRENCY = "Currency"; private static final String COL_HEADER_DIRECTION = "Buy/Sell"; // TODO "Take Offer to + private static final String COL_HEADER_NAME = "Name"; private static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; private static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; private static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); @@ -66,7 +70,7 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { int paymentMethodColWidth = getLengthOfLongestColumn( COL_HEADER_PAYMENT_METHOD.length(), offerInfo.stream() - .map(o -> o.getPaymentMethodShortName()) + .map(OfferInfo::getPaymentMethodShortName) .collect(Collectors.toList())); String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER @@ -98,30 +102,39 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { .collect(Collectors.joining("\n")); } - static String formatPaymentAcctTbl() { - /* - case getpaymentaccts: { - var request = GetPaymentAccountsRequest.newBuilder().build(); - var reply = paymentAccountsService.getPaymentAccounts(request); - var columnFormatSpec = "%-41s %-25s %-14s %s"; - out.println(format(columnFormatSpec, "ID", "Name", "Currency", "Payment Method")); - out.println(reply.getPaymentAccountsList().stream() - .map(a -> format(columnFormatSpec, - a.getId(), - a.getAccountName(), - a.getSelectedTradeCurrency().getCode(), - a.getPaymentMethod().getId())) - .collect(Collectors.joining("\n"))); - return; - } - */ - return ""; + static String formatPaymentAcctTbl(List paymentAccounts) { + // Some column values might be longer than header, so we need to calculated them. + int nameColWidth = getLengthOfLongestColumn( + COL_HEADER_NAME.length(), + paymentAccounts.stream().map(PaymentAccount::getAccountName) + .collect(Collectors.toList())); + int paymentMethodColWidth = getLengthOfLongestColumn( + COL_HEADER_PAYMENT_METHOD.length(), + paymentAccounts.stream().map(a -> a.getPaymentMethod().getId()) + .collect(Collectors.toList())); + + String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CURRENCY + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%-" + nameColWidth + "s" // left justify + + " %" + COL_HEADER_CURRENCY.length() + "s" // right justify + + " %-" + paymentMethodColWidth + "s" // left justify + + " %-" + COL_HEADER_UUID.length() + "s"; // left justify + return headerLine + + paymentAccounts.stream() + .map(a -> format(colDataFormat, + a.getAccountName(), + a.getSelectedTradeCurrency().getCode(), + a.getPaymentMethod().getId(), + a.getId())) + .collect(Collectors.joining("\n")); } // Return length of the longest string value, or the header.len, whichever is greater. private static int getLengthOfLongestColumn(int headerLength, List strings) { - int longest = max(strings, comparing(s -> s.length())).length(); - return longest > headerLength ? longest : headerLength; + int longest = max(strings, comparing(String::length)).length(); + return Math.max(longest, headerLength); } private static String formatDateTime(long timestamp, boolean useLocaleAndLocalTimezone) { From 9691f350825cbe33e2a4ab17b51d1032f4e9efc1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 23 Jun 2020 12:42:56 -0300 Subject: [PATCH 31/35] Check param count only, not param order correctness This change simplifies client 'createpaymentacct' method parameter validation. It no longer assumes parameter ordering is correct, and only verifies the string parameter count is correct. A unit test was also added to cli/test.sh This commit is in response to the requested change in PR 4308. https://github.com/bisq-network/bisq/pull/4308#pullrequestreview-435052357 --- cli/src/main/java/bisq/cli/CliMain.java | 13 +++---------- cli/test.sh | 7 +++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index cb99de79e3b..074af31d19b 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -202,19 +202,12 @@ public static void run(String[] args) { return; } case createpaymentacct: { - if (nonOptionArgs.size() < 2) - throw new IllegalArgumentException("no account name specified"); + if (nonOptionArgs.size() < 4) + throw new IllegalArgumentException( + "incorrect parameter count, expecting account name, account number, currency code"); var accountName = nonOptionArgs.get(1); - - if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("no account number specified"); - var accountNumber = nonOptionArgs.get(2); - - if (nonOptionArgs.size() < 4) - throw new IllegalArgumentException("no fiat currency specified"); - var fiatCurrencyCode = nonOptionArgs.get(3); var request = CreatePaymentAccountRequest.newBuilder() diff --git a/cli/test.sh b/cli/test.sh index c44c85abf44..d21ab501d55 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -166,6 +166,13 @@ [ "$output" = "Error: address bogus not found in wallet" ] } +@test "test createpaymentacct PerfectMoneyDummy (missing nbr, ccy params)" { + run ./bisq-cli --password=xyz createpaymentacct PerfectMoneyDummy + [ "$status" -eq 1 ] + echo "actual output: $output" >&2 + [ "$output" = "Error: incorrect parameter count, expecting account name, account number, currency code" ] +} + @test "test createpaymentacct PerfectMoneyDummy 0123456789 USD" { run ./bisq-cli --password=xyz createpaymentacct PerfectMoneyDummy 0123456789 USD [ "$status" -eq 0 ] From e1fddfacf8867ed139f0e594f2fb4adafe27e8c4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 23 Jun 2020 13:11:36 -0300 Subject: [PATCH 32/35] Remove duplication in wallets availability checks This change adds a new 'verifyWalletsAreAvailable' method to the client, which eliminates this duplicated statement: throw new IllegalStateException("wallet is not yet available"); The commit is in response to a requested change in PR 4312: https://github.com/bisq-network/bisq/pull/4312#pullrequestreview-435659314 --- .../java/bisq/core/grpc/CoreWalletsService.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java index 3eb4cac51ae..1d3719558b7 100644 --- a/core/src/main/java/bisq/core/grpc/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/grpc/CoreWalletsService.java @@ -72,9 +72,7 @@ public CoreWalletsService(Balances balances, } public long getAvailableBalance() { - if (!walletsManager.areWalletsAvailable()) - throw new IllegalStateException("wallet is not yet available"); - + verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); var balance = balances.getAvailableBalance().get(); @@ -96,9 +94,7 @@ public AddressBalanceInfo getAddressBalanceInfo(String addressString) { } public List getFundingAddresses() { - if (!walletsManager.areWalletsAvailable()) - throw new IllegalStateException("wallet is not yet available"); - + verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); // Create a new funding address if none exists. @@ -140,8 +136,7 @@ public int getNumConfirmationsForMostRecentTransaction(String addressString) { } public void setWalletPassword(String password, String newPassword) { - if (!walletsManager.areWalletsAvailable()) - throw new IllegalStateException("wallet is not yet available"); + verifyWalletsAreAvailable(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); @@ -230,6 +225,12 @@ public void removeWalletPassword(String password) { walletsManager.backupWallets(); } + // Throws a RuntimeException if wallets are not available (encrypted or not). + private void verifyWalletsAreAvailable() { + if (!walletsManager.areWalletsAvailable()) + throw new IllegalStateException("wallet is not yet available"); + } + // Throws a RuntimeException if wallets are not available or not encrypted. private void verifyWalletIsAvailableAndEncrypted() { if (!walletsManager.areWalletsAvailable()) From 69792098c63093bf80926dbcf2355fecfdfa4b5a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 23 Jun 2020 15:29:32 -0300 Subject: [PATCH 33/35] Check param count only, not param order correctness This change simplifies client 'getoffers' method parameter validation. It no longer assumes parameter ordering is correct, nor validates the direction parameter value. The client only verifies the correct number of string parameters are present. --- cli/src/main/java/bisq/cli/CliMain.java | 10 ++-------- cli/test.sh | 9 +-------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 074af31d19b..37dbe3f52d6 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -181,16 +181,10 @@ public static void run(String[] args) { return; } case getoffers: { - if (nonOptionArgs.size() < 2) - throw new IllegalArgumentException("no buy/sell direction specified"); - - var direction = nonOptionArgs.get(1); - if (!direction.equalsIgnoreCase("BUY") && !direction.equalsIgnoreCase("SELL")) - throw new IllegalArgumentException("no buy/sell direction specified"); - if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("no fiat currency specified"); + throw new IllegalArgumentException("incorrect parameter count, expecting direction (buy|sell), currency code"); + var direction = nonOptionArgs.get(1); var fiatCurrency = nonOptionArgs.get(2); var request = GetOffersRequest.newBuilder() diff --git a/cli/test.sh b/cli/test.sh index d21ab501d55..54af77c717b 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -187,14 +187,7 @@ run ./bisq-cli --password=xyz getoffers [ "$status" -eq 1 ] echo "actual output: $output" >&2 - [ "$output" = "Error: no buy/sell direction specified" ] -} - -@test "test getoffers missing ccy argument" { - run ./bisq-cli --password=xyz getoffers buy - [ "$status" -eq 1 ] - echo "actual output: $output" >&2 - [ "$output" = "Error: no fiat currency specified" ] + [ "$output" = "Error: incorrect parameter count, expecting direction (buy|sell), currency code" ] } @test "test getoffers buy eur check return status" { From 51d82b1dff4764bf136a6a9049ebdd054ad63095 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 23 Jun 2020 17:15:39 -0300 Subject: [PATCH 34/35] Fix offer list filter bug due to direction flip Respect the direction parmeter; do not give it meaning it does not have. If the user passes a 'buy' parameter, return buy offers. Do not misinterpret the param's intent. The direction parameter's value does not imply "buy=I'm a buyer, show me sell offers" or "sell=I'm a seller, show me buy offers". I got mixed up by looking at the UI. If I want to sell BTC, I click the SELL tab to view buy offers (maker as buyer). If I want to buy BTC, I click the BUY tab to view sell offers (maker as seller). This change also fixes an offer list sorting bug. The commit is in response to a requested changes in PR 4329: https://github.com/bisq-network/bisq/pull/4329#pullrequestreview-436033502 --- cli/src/main/java/bisq/cli/TableFormat.java | 2 +- .../java/bisq/core/grpc/CoreOffersService.java | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index bb1ef4e3b2a..ca74f057236 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -92,7 +92,7 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { return headerLine + offerInfo.stream() .map(o -> format(colDataFormat, - o.getDirection().equals("BUY") ? "SELL" : "BUY", + o.getDirection(), formatOfferPrice(o.getPrice()), formatAmountRange(o.getMinAmount(), o.getAmount()), formatVolumeRange(o.getMinVolume(), o.getVolume()), diff --git a/core/src/main/java/bisq/core/grpc/CoreOffersService.java b/core/src/main/java/bisq/core/grpc/CoreOffersService.java index 3ac365bd888..0325bcf4ff3 100644 --- a/core/src/main/java/bisq/core/grpc/CoreOffersService.java +++ b/core/src/main/java/bisq/core/grpc/CoreOffersService.java @@ -60,14 +60,19 @@ public CoreOffersService(CreateOfferService createOfferService, public List getOffers(String direction, String fiatCurrencyCode) { List offers = offerBookService.getOffers().stream() - .filter(o -> !o.getDirection().name().equalsIgnoreCase(direction) - && o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(fiatCurrencyCode)) + .filter(o -> { + var offerOfWantedDirection = o.getDirection().name().equalsIgnoreCase(direction); + var offerInWantedCurrency = o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(fiatCurrencyCode); + return offerOfWantedDirection && offerInWantedCurrency; + }) .collect(Collectors.toList()); - if (direction.equals(BUY.name())) - offers.sort(Comparator.comparing(Offer::getPrice)); - else + // A buyer probably wants to see sell orders in price ascending order. + // A seller probably wants to see buy orders in price descending order. + if (direction.equalsIgnoreCase(BUY.name())) offers.sort(Comparator.comparing(Offer::getPrice).reversed()); + else + offers.sort(Comparator.comparing(Offer::getPrice)); return offers; } From f820897e5bc45dbb8bb824f84f024d3f4fcb5ad0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 24 Jun 2020 13:27:43 -0300 Subject: [PATCH 35/35] Format dates ISO 8601 in UTC We display all dates in UTC, using the "yyyy-MM-dd'T'HH:mm:ss'Z'" format. --- cli/src/main/java/bisq/cli/TableFormat.java | 35 ++++++--------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index ca74f057236..84d0f5820de 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -5,11 +5,11 @@ import protobuf.PaymentAccount; -import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.Locale; +import java.util.TimeZone; import java.util.stream.Collectors; import static bisq.cli.CurrencyFormat.formatAmountRange; @@ -19,15 +19,15 @@ import static com.google.common.base.Strings.padEnd; import static com.google.common.base.Strings.padStart; import static java.lang.String.format; -import static java.text.DateFormat.DEFAULT; -import static java.text.DateFormat.getDateInstance; -import static java.text.DateFormat.getTimeInstance; import static java.util.Collections.max; import static java.util.Comparator.comparing; import static java.util.TimeZone.getTimeZone; class TableFormat { + private static final TimeZone TZ_UTC = getTimeZone("UTC"); + private static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + // For inserting 2 spaces between column headers. private static final String COL_HEADER_DELIMITER = " "; @@ -39,7 +39,7 @@ class TableFormat { private static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); private static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); private static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; - private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date", 24, ' '); + private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); private static final String COL_HEADER_CURRENCY = "Currency"; private static final String COL_HEADER_DIRECTION = "Buy/Sell"; // TODO "Take Offer to private static final String COL_HEADER_NAME = "Name"; @@ -97,7 +97,7 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { formatAmountRange(o.getMinAmount(), o.getAmount()), formatVolumeRange(o.getMinVolume(), o.getVolume()), o.getPaymentMethodShortName(), - formatDateTime(o.getDate(), true), + formatTimestamp(o.getDate()), o.getId())) .collect(Collectors.joining("\n")); } @@ -137,23 +137,8 @@ private static int getLengthOfLongestColumn(int headerLength, List strin return Math.max(longest, headerLength); } - private static String formatDateTime(long timestamp, boolean useLocaleAndLocalTimezone) { - Date date = new Date(timestamp); - Locale locale = useLocaleAndLocalTimezone ? Locale.getDefault() : Locale.US; - DateFormat dateInstance = getDateInstance(DEFAULT, locale); - DateFormat timeInstance = getTimeInstance(DEFAULT, locale); - if (!useLocaleAndLocalTimezone) { - dateInstance.setTimeZone(getTimeZone("UTC")); - timeInstance.setTimeZone(getTimeZone("UTC")); - } - return formatDateTime(date, dateInstance, timeInstance); - } - - private static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { - if (date != null) { - return dateFormatter.format(date) + " " + timeFormatter.format(date); - } else { - return ""; - } + private static String formatTimestamp(long timestamp) { + DATE_FORMAT_ISO_8601.setTimeZone(TZ_UTC); + return DATE_FORMAT_ISO_8601.format(new Date(timestamp)); } }