Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add rpc method 'getoffers' #4329

Merged
merged 36 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b1146fd
Rename CoreWalletService -> CoreWalletsService
ghubstan Jun 12, 2020
ec66b14
Add rpc wallet(s) protection tests
ghubstan Jun 12, 2020
85c9676
Add rpc method 'getfundingaddresses'
ghubstan Jun 13, 2020
2e415de
Replace duplicate code in getFundingAddresses
ghubstan Jun 14, 2020
b1228e5
Add rpc method 'getaddressbalance'
ghubstan Jun 14, 2020
a7542e9
Add rpc method 'createpaymentacct'
ghubstan Jun 15, 2020
bac3ed5
Call core wallets service methods from CoreApi
ghubstan Jun 15, 2020
258d180
Factor duplicate unlocked wallet checks into new method
ghubstan Jun 16, 2020
c5134e1
Replace Tuple3 with memoization
dmos62 Jun 18, 2020
9db9ee2
Merge pull request #2 from dmos62/Z-getfundingaddresses-patch
ghubstan Jun 18, 2020
b0e278f
Refactor getFundingAddresses to use memoization
ghubstan Jun 18, 2020
1930411
Rmove blank line
ghubstan Jun 18, 2020
435672a
Add rpc method 'getpaymentaccts'
ghubstan Jun 19, 2020
331f488
Return protos from funding address methods
ghubstan Jun 19, 2020
612bafe
Refactor AddressBalanceInfo display logic
ghubstan Jun 20, 2020
37fb606
Add license comment
ghubstan Jun 20, 2020
855ac0f
Add license comment
ghubstan Jun 20, 2020
bfcc693
Add license comment
ghubstan Jun 20, 2020
d06807b
Wrap Exception from core in gRPC StatusRuntimeException
ghubstan Jun 20, 2020
7c073c6
Revert "Add license comment"
ghubstan Jun 20, 2020
d6ea0ea
Re-add license comment
ghubstan Jun 20, 2020
41f1add
Remove try catch block
ghubstan Jun 20, 2020
88cb90e
Add rpc method 'getoffers'
ghubstan Jun 20, 2020
1756258
Do not use protobuf.OfferPayload.Direction in client
ghubstan Jun 20, 2020
4778976
Fix comments
ghubstan Jun 20, 2020
b25abf1
Refactor CLI output table formatting
ghubstan Jun 21, 2020
a48af7c
Add 'getoffers' unit tests
ghubstan Jun 22, 2020
61285a7
Do not change case of input params in client
ghubstan Jun 22, 2020
0d9bdef
Add 'getoffers' smoke test
ghubstan Jun 22, 2020
8dcfa50
Define reusable headers from balance-info tbl
ghubstan Jun 22, 2020
52529a9
Move getpaymentaccts tbl formatting to TableFormat
ghubstan Jun 22, 2020
9691f35
Check param count only, not param order correctness
ghubstan Jun 23, 2020
e1fddfa
Remove duplication in wallets availability checks
ghubstan Jun 23, 2020
6979209
Check param count only, not param order correctness
ghubstan Jun 23, 2020
51d82b1
Fix offer list filter bug due to direction flip
ghubstan Jun 23, 2020
f820897
Format dates ISO 8601 in UTC
ghubstan Jun 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 34 additions & 42 deletions cli/src/main/java/bisq/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@

package bisq.cli;

import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.CreatePaymentAccountRequest;
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;
Expand All @@ -38,20 +39,18 @@
import joptsimple.OptionParser;
import joptsimple.OptionSet;

import java.text.DecimalFormat;

import java.io.IOException;
import java.io.PrintStream;

import java.math.BigDecimal;

import java.util.List;
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 bisq.cli.TableFormat.formatPaymentAcctTbl;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
Expand All @@ -65,13 +64,8 @@
@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<Long, String> formatSatoshis = (sats) ->
BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));

private enum Method {
getoffers,
createpaymentacct,
getpaymentaccts,
getversion,
Expand Down Expand Up @@ -151,6 +145,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);

Expand All @@ -165,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;
}
Expand All @@ -176,30 +171,44 @@ 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 createpaymentacct: {
case getoffers: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no account name specified");
throw new IllegalArgumentException("no buy/sell direction specified");

var accountName = nonOptionArgs.get(1);
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 account number specified");
throw new IllegalArgumentException("no fiat currency specified");

var accountNumber = nonOptionArgs.get(2);
var fiatCurrency = nonOptionArgs.get(2);

var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setFiatCurrencyCode(fiatCurrency)
.build();
var reply = offersService.getOffers(request);
out.println(formatOfferTable(reply.getOffersList(), fiatCurrency));
return;
}
case createpaymentacct: {
if (nonOptionArgs.size() < 4)
throw new IllegalArgumentException("no fiat currency specified");
throw new IllegalArgumentException(
"incorrect parameter count, expecting account name, account number, currency code");

var fiatCurrencyCode = nonOptionArgs.get(3).toUpperCase();
var accountName = nonOptionArgs.get(1);
var accountNumber = nonOptionArgs.get(2);
var fiatCurrencyCode = nonOptionArgs.get(3);

var request = CreatePaymentAccountRequest.newBuilder()
.setAccountName(accountName)
Expand All @@ -212,15 +221,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: {
Expand Down Expand Up @@ -295,6 +296,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");
Expand All @@ -307,14 +309,4 @@ private static void printHelp(OptionParser parser, PrintStream stream) {
ex.printStackTrace(stream);
}
}

private static String formatTable(List<AddressBalanceInfo> 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"));
}
}
47 changes: 47 additions & 0 deletions cli/src/main/java/bisq/cli/CurrencyFormat.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 17 additions & 0 deletions cli/src/main/java/bisq/cli/PasswordCallCredentials.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

package bisq.cli;

import io.grpc.CallCredentials;
Expand Down
159 changes: 159 additions & 0 deletions cli/src/main/java/bisq/cli/TableFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package bisq.cli;

import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.OfferInfo;

import protobuf.PaymentAccount;

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 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 {

// 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_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_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, ' ');
private static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');

static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
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(colDataFormat,
info.getAddress(),
formatSatoshis(info.getBalance()),
info.getNumConfirmations()))
.collect(Collectors.joining("\n"));
}

static String formatOfferTable(List<OfferInfo> 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(OfferInfo::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"));
}

static String formatPaymentAcctTbl(List<PaymentAccount> 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<String> strings) {
int longest = max(strings, comparing(String::length)).length();
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 "";
}
}
}
Loading