From 3379376babab645af1f65cf595aca06768fd40c1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 25 Oct 2020 16:50:22 -0300 Subject: [PATCH] Refactor CLI output formatting code & add trade formatter - Move output column header specs to its own shared constants class. - Add new TradeFormat class for printing trade details in the console. - Print formatted trade in api trade tests -- to see output before using formatter in CLI (in next PR). --- .../method/trade/AbstractTradeTest.java | 1 - .../method/trade/TakeBuyBTCOfferTest.java | 6 + .../method/trade/TakeSellBTCOfferTest.java | 7 ++ .../java/bisq/cli/ColumnHeaderConstants.java | 38 ++++++ cli/src/main/java/bisq/cli/TableFormat.java | 26 +--- cli/src/main/java/bisq/cli/TradeFormat.java | 119 ++++++++++++++++++ 6 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/ColumnHeaderConstants.java create mode 100644 cli/src/main/java/bisq/cli/TradeFormat.java diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index 5364de7f7d..094007abd4 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -28,5 +28,4 @@ public class AbstractTradeTest extends AbstractOfferTest { assertEquals(expectedState.name(), trade.getState()); assertEquals(expectedPhase.name(), trade.getPhase()); } - } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 040cee5a18..4aba67fc71 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.cli.TradeFormat.format; import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; import static bisq.core.trade.Trade.Phase.FIAT_SENT; @@ -39,6 +40,7 @@ import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIAT import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; import static bisq.core.trade.Trade.State.SELLER_PUBLISHED_DEPOSIT_TX; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static java.lang.System.out; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -85,10 +87,12 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { trade = getTrade(bobdaemon, trade.getTradeId()); verifyExpectedTradeStateAndPhase(trade, SELLER_PUBLISHED_DEPOSIT_TX, DEPOSIT_PUBLISHED); + out.println(format(trade)); genBtcBlocksThenWait(1, 2250); trade = getTrade(bobdaemon, trade.getTradeId()); verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); + out.println(format(trade)); } catch (StatusRuntimeException e) { fail(e); } @@ -107,6 +111,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { trade = getTrade(alicedaemon, tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); + out.println(format(trade)); } catch (StatusRuntimeException e) { fail(e); } @@ -125,5 +130,6 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { // TODO is this a bug? Why is offer.state == available? assertEquals(AVAILABLE.name(), trade.getOffer().getState()); verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); + out.println(format(trade)); } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 5347159daa..658b408323 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.cli.TradeFormat.format; import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; import static bisq.core.trade.Trade.Phase.FIAT_SENT; @@ -39,6 +40,7 @@ import static bisq.core.trade.Trade.State.BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MS import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static java.lang.System.out; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -86,10 +88,12 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { trade = getTrade(bobdaemon, trade.getTradeId()); verifyExpectedTradeStateAndPhase(trade, BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG, DEPOSIT_PUBLISHED); + out.println(format(trade)); genBtcBlocksThenWait(1, 2250); trade = getTrade(bobdaemon, trade.getTradeId()); verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); + out.println(format(trade)); } catch (StatusRuntimeException e) { fail(e); @@ -110,6 +114,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { // TODO is this a bug? Why is offer.state == available? assertEquals(AVAILABLE.name(), trade.getOffer().getState()); verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); + + out.println(format(trade)); } catch (StatusRuntimeException e) { fail(e); } @@ -127,5 +133,6 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { trade = getTrade(alicedaemon, tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); + out.println(format(trade)); } } diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java new file mode 100644 index 0000000000..f5744f6cc7 --- /dev/null +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -0,0 +1,38 @@ +package bisq.cli; + +import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; + +class ColumnHeaderConstants { + + // For inserting 2 spaces between column headers. + 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. + static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' '); + static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); + static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); + static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; + static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); + static final String COL_HEADER_CURRENCY = "Currency"; + static final String COL_HEADER_DIRECTION = "Buy/Sell"; + static final String COL_HEADER_NAME = "Name"; + static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; + static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; + static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' '); + static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed"; + static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published"; + static final String COL_HEADER_TRADE_FIAT_SENT = "Fiat Sent"; + static final String COL_HEADER_TRADE_FIAT_RECEIVED = "Fiat Received"; + static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published"; + static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn"; + static final String COL_HEADER_TRADE_ROLE = "My Role"; + static final String COL_HEADER_TRADE_SHORT_ID = "ID"; + static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)"; + static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)"; + static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); + static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); +} diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 7d02af562f..e9868f336e 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -29,12 +29,12 @@ import java.util.List; import java.util.TimeZone; import java.util.stream.Collectors; +import static bisq.cli.ColumnHeaderConstants.*; 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.util.Collections.max; import static java.util.Comparator.comparing; @@ -42,28 +42,8 @@ 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 = " "; - - // 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 (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"; - 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 final TimeZone TZ_UTC = getTimeZone("UTC"); + static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); static String formatAddressBalanceTbl(List addressBalanceInfo) { String headerLine = (COL_HEADER_ADDRESS + COL_HEADER_DELIMITER diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java new file mode 100644 index 0000000000..4eadfa9008 --- /dev/null +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -0,0 +1,119 @@ +/* + * 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 bisq.proto.grpc.TradeInfo; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.function.Supplier; + +import static bisq.cli.ColumnHeaderConstants.*; +import static bisq.cli.CurrencyFormat.formatOfferPrice; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static com.google.common.base.Strings.padEnd; + +@VisibleForTesting +public class TradeFormat { + + @VisibleForTesting + public static String format(TradeInfo tradeInfo) { + // Some column values might be longer than header, so we need to calculated them. + int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length()); + int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length()); + + // We only show taker fee under its header when user is the taker. + boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker"); + Supplier takerFeeHeaderFormat = () -> isTaker ? + padEnd(COL_HEADER_TRADE_TAKER_FEE, 12, ' ') + COL_HEADER_DELIMITER + : ""; + Supplier takerFeeHeader = () -> isTaker ? + "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 1) + "s" + : ""; + + String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> currencyCode + + padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER + + takerFeeHeaderFormat.get() + + COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_FIAT_SENT + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_FIAT_RECEIVED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER + + COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER + + "%n"; + + String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode(); + String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode(); + String headerLine = isTaker + ? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode) + : String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode); + + String colDataFormat = "%-" + shortIdColWidth + "s" // left justify + + " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left justify + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // right justify + + "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // right justify + + "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // right justify + + takerFeeHeader.get() // right justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // left justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // left justify + + " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // left justify + + " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // left justify + + " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // left justify + + " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s" // left justify + ; + + return headerLine + + (isTaker + ? formatTradeForTaker(colDataFormat, tradeInfo) + : formatTradeForMaker(colDataFormat, tradeInfo)); + } + + private static String formatTradeForMaker(String format, TradeInfo tradeInfo) { + return String.format(format, + tradeInfo.getShortId(), + tradeInfo.getRole(), + formatOfferPrice(tradeInfo.getTradePrice()), + formatSatoshis(tradeInfo.getTradeAmountAsLong()), + formatSatoshis(tradeInfo.getTxFeeAsLong()), + tradeInfo.getIsDepositPublished() ? "YES" : "NO", + tradeInfo.getIsDepositConfirmed() ? "YES" : "NO", + tradeInfo.getIsFiatSent() ? "YES" : "NO", + tradeInfo.getIsFiatReceived() ? "YES" : "NO", + tradeInfo.getIsPayoutPublished() ? "YES" : "NO", + tradeInfo.getIsWithdrawn() ? "YES" : "NO"); + } + + private static String formatTradeForTaker(String format, TradeInfo tradeInfo) { + return String.format(format, + tradeInfo.getShortId(), + tradeInfo.getRole(), + formatOfferPrice(tradeInfo.getTradePrice()), + formatSatoshis(tradeInfo.getTradeAmountAsLong()), + formatSatoshis(tradeInfo.getTxFeeAsLong()), + formatSatoshis(tradeInfo.getTakerFeeAsLong()), + tradeInfo.getIsDepositPublished() ? "YES" : "NO", + tradeInfo.getIsDepositConfirmed() ? "YES" : "NO", + tradeInfo.getIsFiatSent() ? "YES" : "NO", + tradeInfo.getIsFiatReceived() ? "YES" : "NO", + tradeInfo.getIsPayoutPublished() ? "YES" : "NO", + tradeInfo.getIsWithdrawn() ? "YES" : "NO"); + } +}