From 6422ac0cab6d8a6e948e25b1ac42ddfca397d9ef Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 24 Jun 2022 14:00:41 -0300
Subject: [PATCH] Copy bisq.cli's console output fmt api, grpc utils
Use the existing Bisq API CLI utils for making calls to the daemon,
and formatting responses.
When/if the CLI is released on jitpack, this code can be removed
from this repo, and loaded from a gradle dependency.
---
.../main/java/bisq/bots/CurrencyFormat.java | 120 +++++++
.../src/main/java/bisq/bots/GrpcStubs.java | 67 ++++
.../bisq/bots/PasswordCallCredentials.java | 62 ++++
.../src/main/java/bisq/bots/table/Table.java | 153 +++++++++
.../table/builder/AbstractTableBuilder.java | 44 +++
.../builder/AbstractTradeListBuilder.java | 301 ++++++++++++++++++
.../builder/AddressBalanceTableBuilder.java | 71 +++++
.../table/builder/BsqBalanceTableBuilder.java | 75 +++++
.../table/builder/BtcBalanceTableBuilder.java | 67 ++++
.../builder/ClosedTradeTableBuilder.java | 80 +++++
.../builder/FailedTradeTableBuilder.java | 64 ++++
.../bots/table/builder/OfferTableBuilder.java | 259 +++++++++++++++
.../table/builder/OpenTradeTableBuilder.java | 62 ++++
.../builder/PaymentAccountTableBuilder.java | 68 ++++
.../bisq/bots/table/builder/TableBuilder.java | 68 ++++
.../table/builder/TableBuilderConstants.java | 82 +++++
.../bisq/bots/table/builder/TableType.java | 35 ++
.../builder/TradeDetailTableBuilder.java | 159 +++++++++
.../builder/TradeTableColumnSupplier.java | 285 +++++++++++++++++
.../builder/TransactionTableBuilder.java | 95 ++++++
.../bots/table/column/AbstractColumn.java | 86 +++++
.../table/column/AltcoinVolumeColumn.java | 88 +++++
.../bisq/bots/table/column/BooleanColumn.java | 131 ++++++++
.../bisq/bots/table/column/BtcColumn.java | 48 +++
.../java/bisq/bots/table/column/Column.java | 122 +++++++
.../bisq/bots/table/column/DoubleColumn.java | 93 ++++++
.../bisq/bots/table/column/FiatColumn.java | 83 +++++
.../bisq/bots/table/column/IntegerColumn.java | 93 ++++++
.../table/column/Iso8601DateTimeColumn.java | 63 ++++
.../bisq/bots/table/column/LongColumn.java | 93 ++++++
.../table/column/MixedTradeFeeColumn.java | 59 ++++
.../bisq/bots/table/column/NumberColumn.java | 32 ++
.../bisq/bots/table/column/SatoshiColumn.java | 72 +++++
.../bisq/bots/table/column/StringColumn.java | 102 ++++++
.../table/column/ZippedStringColumns.java | 124 ++++++++
35 files changed, 3506 insertions(+)
create mode 100644 java-examples/src/main/java/bisq/bots/CurrencyFormat.java
create mode 100644 java-examples/src/main/java/bisq/bots/GrpcStubs.java
create mode 100644 java-examples/src/main/java/bisq/bots/PasswordCallCredentials.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/Table.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/AbstractTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/AbstractTradeListBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/AddressBalanceTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/BsqBalanceTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/BtcBalanceTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/ClosedTradeTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/FailedTradeTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/OfferTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/OpenTradeTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/PaymentAccountTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TableBuilderConstants.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TableType.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TradeDetailTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TradeTableColumnSupplier.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/builder/TransactionTableBuilder.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/AbstractColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/AltcoinVolumeColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/BooleanColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/BtcColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/Column.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/DoubleColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/FiatColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/IntegerColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/Iso8601DateTimeColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/LongColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/MixedTradeFeeColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/NumberColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/SatoshiColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/StringColumn.java
create mode 100644 java-examples/src/main/java/bisq/bots/table/column/ZippedStringColumns.java
diff --git a/java-examples/src/main/java/bisq/bots/CurrencyFormat.java b/java-examples/src/main/java/bisq/bots/CurrencyFormat.java
new file mode 100644
index 0000000..fa4b64c
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/CurrencyFormat.java
@@ -0,0 +1,120 @@
+/*
+ * 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.bots;
+
+import bisq.proto.grpc.TxFeeRateInfo;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import static java.lang.String.format;
+import static java.math.RoundingMode.HALF_UP;
+import static java.math.RoundingMode.UNNECESSARY;
+
+/**
+ * Utility for formatting amounts, volumes and fees; there is no i18n support in the CLI.
+ */
+@VisibleForTesting
+public class CurrencyFormat {
+
+ // Use the US locale as a base for all DecimalFormats, but commas should be omitted from number strings.
+ private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
+
+ // Use the US locale as a base for all NumberFormats, but commas should be omitted from number strings.
+ private static final NumberFormat US_LOCALE_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
+
+ // Formats numbers for internal use, i.e., grpc request parameters.
+ private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000");
+
+ static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000);
+ static final DecimalFormat SATOSHI_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS);
+ static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.########", DECIMAL_FORMAT_SYMBOLS);
+ static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS);
+
+ static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
+ static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS);
+
+ public static String formatSatoshis(String sats) {
+ //noinspection BigDecimalMethodWithoutRoundingCalled
+ return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
+ }
+
+ @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
+ public static String formatSatoshis(long sats) {
+ return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
+ }
+
+ @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
+ public static String formatBtc(long sats) {
+ return BTC_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
+ }
+
+ @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
+ public static String formatBsq(long sats) {
+ return BSQ_FORMAT.format(new BigDecimal(sats).divide(BSQ_SATOSHI_DIVISOR));
+ }
+
+ public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) {
+ if (txFeeRateInfo.getUseCustomTxFeeRate())
+ return format("custom tx fee rate: %s sats/byte, network rate: %s sats/byte, min network rate: %s sats/byte",
+ formatFeeSatoshis(txFeeRateInfo.getCustomTxFeeRate()),
+ formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()),
+ formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
+ else
+ return format("tx fee rate: %s sats/byte, min tx fee rate: %s sats/byte",
+ formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()),
+ formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
+ }
+
+ public static String formatPrice(long price) {
+ US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
+ US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
+ US_LOCALE_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
+ return US_LOCALE_NUMBER_FORMAT.format((double) price / 10_000);
+ }
+
+ public static String formatFiatVolume(long volume) {
+ US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(0);
+ US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(0);
+ US_LOCALE_NUMBER_FORMAT.setRoundingMode(HALF_UP);
+ return US_LOCALE_NUMBER_FORMAT.format((double) volume / 10_000);
+ }
+
+ public static long toSatoshis(BigDecimal btc) {
+ return btc.multiply(SATOSHI_DIVISOR).longValue();
+ }
+
+ public static long toSatoshis(String btc) {
+ if (btc.startsWith("-"))
+ throw new IllegalArgumentException(format("'%s' is not a positive number", btc));
+
+ try {
+ return new BigDecimal(btc).multiply(SATOSHI_DIVISOR).longValue();
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(format("'%s' is not a number", btc));
+ }
+ }
+
+ public static String formatFeeSatoshis(long sats) {
+ return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats));
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/GrpcStubs.java b/java-examples/src/main/java/bisq/bots/GrpcStubs.java
new file mode 100644
index 0000000..c6ed9fb
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/GrpcStubs.java
@@ -0,0 +1,67 @@
+/*
+ * 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.bots;
+
+import bisq.proto.grpc.*;
+import io.grpc.CallCredentials;
+import io.grpc.ManagedChannelBuilder;
+import lombok.extern.slf4j.Slf4j;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * gRPC Service Stubs -- all blocking.
+ */
+@Slf4j
+final class GrpcStubs {
+
+ public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService;
+ public final HelpGrpc.HelpBlockingStub helpService;
+ public final GetVersionGrpc.GetVersionBlockingStub versionService;
+ public final OffersGrpc.OffersBlockingStub offersService;
+ public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
+ public final PriceGrpc.PriceBlockingStub priceService;
+ public final ShutdownServerGrpc.ShutdownServerBlockingStub shutdownService;
+ public final TradesGrpc.TradesBlockingStub tradesService;
+ public final WalletsGrpc.WalletsBlockingStub walletsService;
+
+ public GrpcStubs(String apiHost, int apiPort, String apiPassword) {
+ CallCredentials credentials = new PasswordCallCredentials(apiPassword);
+
+ var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build();
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ log.info("Shutting down bot's grpc channel.");
+ channel.shutdown().awaitTermination(1, SECONDS);
+ log.info("Bot channel shutdown complete.");
+ } catch (InterruptedException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }));
+
+ this.disputeAgentsService = DisputeAgentsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.helpService = HelpGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.shutdownService = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/PasswordCallCredentials.java b/java-examples/src/main/java/bisq/bots/PasswordCallCredentials.java
new file mode 100644
index 0000000..a119c4b
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/PasswordCallCredentials.java
@@ -0,0 +1,62 @@
+/*
+ * 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.bots;
+
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.Metadata.Key;
+
+import java.util.concurrent.Executor;
+
+import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
+import static io.grpc.Status.UNAUTHENTICATED;
+import static java.lang.String.format;
+
+/**
+ * Sets the {@value PASSWORD_KEY} rpc call header to a given value.
+ */
+class PasswordCallCredentials extends CallCredentials {
+
+ public static final String PASSWORD_KEY = "password";
+
+ private final String passwordValue;
+
+ public PasswordCallCredentials(String passwordValue) {
+ if (passwordValue == null)
+ throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY));
+ this.passwordValue = passwordValue;
+ }
+
+ @Override
+ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) {
+ appExecutor.execute(() -> {
+ try {
+ var headers = new Metadata();
+ var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER);
+ headers.put(passwordKey, passwordValue);
+ metadataApplier.apply(headers);
+ } catch (Throwable ex) {
+ metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
+ }
+ });
+ }
+
+ @Override
+ public void thisUsesUnstableApi() {
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/Table.java b/java-examples/src/main/java/bisq/bots/table/Table.java
new file mode 100644
index 0000000..67bed91
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/Table.java
@@ -0,0 +1,153 @@
+/*
+ * 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.bots.table;
+
+import bisq.bots.table.column.Column;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+import static com.google.common.base.Strings.padStart;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * A simple table of formatted data for the CLI's output console. A table must be
+ * created with at least one populated column, and each column passed to the constructor
+ * must contain the same number of rows. Null checking is omitted because tables are
+ * populated by protobuf message fields which cannot be null.
+ *
+ * All data in a column has the same type: long, string, etc., but a table
+ * may contain an arbitrary number of columns of any type. For output formatting
+ * purposes, numeric and date columns should be transformed to a StringColumn type with
+ * formatted and justified string values before being passed to the constructor.
+ *
+ * This is not a relational, rdbms table.
+ */
+public class Table {
+
+ public final Column>[] columns;
+ public final int rowCount;
+
+ // Each printed column is delimited by two spaces.
+ private final int columnDelimiterLength = 2;
+
+ /**
+ * Default constructor. Takes populated Columns.
+ *
+ * @param columns containing the same number of rows
+ */
+ public Table(Column>... columns) {
+ this.columns = columns;
+ this.rowCount = columns.length > 0 ? columns[0].rowCount() : 0;
+ validateStructure();
+ }
+
+ /**
+ * Print table data to a PrintStream.
+ *
+ * @param printStream the target output stream
+ */
+ public void print(PrintStream printStream) {
+ printColumnNames(printStream);
+ for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ printRow(printStream, rowIndex);
+ }
+ }
+
+ /**
+ * Print table column names to a PrintStream.
+ *
+ * @param printStream the target output stream
+ */
+ private void printColumnNames(PrintStream printStream) {
+ IntStream.range(0, columns.length).forEachOrdered(colIndex -> {
+ var c = columns[colIndex];
+ var justifiedName = c.getJustification().equals(RIGHT)
+ ? padStart(c.getName(), c.getWidth(), ' ')
+ : c.getName();
+ var paddedWidth = colIndex == columns.length - 1
+ ? c.getName().length()
+ : c.getWidth() + columnDelimiterLength;
+ printStream.printf("%-" + paddedWidth + "s", justifiedName);
+ });
+ printStream.println();
+ }
+
+ /**
+ * Print a table row to a PrintStream.
+ *
+ * @param printStream the target output stream
+ */
+ private void printRow(PrintStream printStream, int rowIndex) {
+ IntStream.range(0, columns.length).forEachOrdered(colIndex -> {
+ var c = columns[colIndex];
+ var paddedWidth = colIndex == columns.length - 1
+ ? c.getWidth()
+ : c.getWidth() + columnDelimiterLength;
+ printStream.printf("%-" + paddedWidth + "s", c.getRow(rowIndex));
+ if (colIndex == columns.length - 1)
+ printStream.println();
+ });
+ }
+
+ /**
+ * Returns the table's formatted output as a String.
+ *
+ * @return String
+ */
+ @Override
+ public String toString() {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (PrintStream ps = new PrintStream(baos, true, UTF_8)) {
+ print(ps);
+ }
+ return baos.toString();
+ }
+
+ /**
+ * Verifies the table has columns, and each column has the same number of rows.
+ */
+ private void validateStructure() {
+ if (columns.length == 0)
+ throw new IllegalArgumentException("Table has no columns.");
+
+ if (columns[0].isEmpty())
+ throw new IllegalArgumentException(
+ format("Table's 1st column (%s) has no data.",
+ columns[0].getName()));
+
+ IntStream.range(1, columns.length).forEachOrdered(colIndex -> {
+ var c = columns[colIndex];
+
+ if (c.isEmpty())
+ throw new IllegalStateException(
+ format("Table column # %d (%s) does not have any data.",
+ colIndex + 1,
+ c.getName()));
+
+ if (this.rowCount != c.rowCount())
+ throw new IllegalStateException(
+ format("Table column # %d (%s) does not have same number of rows as 1st column.",
+ colIndex + 1,
+ c.getName()));
+ });
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/AbstractTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/AbstractTableBuilder.java
new file mode 100644
index 0000000..994e1a5
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/AbstractTableBuilder.java
@@ -0,0 +1,44 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.proto.grpc.OfferInfo;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Abstract superclass for TableBuilder implementations.
+ */
+abstract class AbstractTableBuilder {
+
+ protected final Predicate isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC");
+
+ protected final TableType tableType;
+ protected final List> protos;
+
+ AbstractTableBuilder(TableType tableType, List> protos) {
+ this.tableType = tableType;
+ this.protos = protos;
+ if (protos.isEmpty())
+ throw new IllegalArgumentException("cannot build a table without rows");
+ }
+
+ public abstract Table build();
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/AbstractTradeListBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/AbstractTradeListBuilder.java
new file mode 100644
index 0000000..16d1a06
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/AbstractTradeListBuilder.java
@@ -0,0 +1,301 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.column.Column;
+import bisq.bots.table.column.MixedTradeFeeColumn;
+import bisq.proto.grpc.ContractInfo;
+import bisq.proto.grpc.TradeInfo;
+
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import static bisq.bots.CurrencyFormat.formatSatoshis;
+import static bisq.bots.table.builder.TableBuilderConstants.COL_HEADER_BUYER_DEPOSIT;
+import static bisq.bots.table.builder.TableBuilderConstants.COL_HEADER_SELLER_DEPOSIT;
+import static bisq.bots.table.builder.TableType.TRADE_DETAIL_TBL;
+import static java.lang.String.format;
+import static protobuf.OfferDirection.SELL;
+
+abstract class AbstractTradeListBuilder extends AbstractTableBuilder {
+
+ protected final List trades;
+
+ protected final TradeTableColumnSupplier colSupplier;
+
+ protected final Column colTradeId;
+ @Nullable
+ protected final Column colCreateDate;
+ @Nullable
+ protected final Column colMarket;
+ protected final Column colPrice;
+ @Nullable
+ protected final Column colPriceDeviation;
+ @Nullable
+ protected final Column colCurrency;
+ @Nullable
+ protected final Column colAmount;
+ @Nullable
+ protected final Column colMixedAmount;
+ @Nullable
+ protected final Column colMinerTxFee;
+ @Nullable
+ protected final MixedTradeFeeColumn colMixedTradeFee;
+ @Nullable
+ protected final Column colBuyerDeposit;
+ @Nullable
+ protected final Column colSellerDeposit;
+ @Nullable
+ protected final Column colPaymentMethod;
+ @Nullable
+ protected final Column colRole;
+ @Nullable
+ protected final Column colOfferType;
+ @Nullable
+ protected final Column colClosingStatus;
+
+ // Trade detail tbl specific columns
+
+ @Nullable
+ protected final Column colIsDepositPublished;
+ @Nullable
+ protected final Column colIsDepositConfirmed;
+ @Nullable
+ protected final Column colIsPayoutPublished;
+ @Nullable
+ protected final Column colIsCompleted;
+ @Nullable
+ protected final Column colBisqTradeFee;
+ @Nullable
+ protected final Column colTradeCost;
+ @Nullable
+ protected final Column colIsPaymentStartedMessageSent;
+ @Nullable
+ protected final Column colIsPaymentReceivedMessageSent;
+ @Nullable
+ protected final Column colAltcoinReceiveAddressColumn;
+
+ // BSQ swap trade detail specific columns
+
+ @Nullable
+ protected final Column status;
+ @Nullable
+ protected final Column colTxId;
+ @Nullable
+ protected final Column colNumConfirmations;
+
+ AbstractTradeListBuilder(TableType tableType, List> protos) {
+ super(tableType, protos);
+ validate();
+
+ this.trades = protos.stream().map(p -> (TradeInfo) p).collect(Collectors.toList());
+ this.colSupplier = new TradeTableColumnSupplier(tableType, trades);
+
+ this.colTradeId = colSupplier.tradeIdColumn.get();
+ this.colCreateDate = colSupplier.createDateColumn.get();
+ this.colMarket = colSupplier.marketColumn.get();
+ this.colPrice = colSupplier.priceColumn.get();
+ this.colPriceDeviation = colSupplier.priceDeviationColumn.get();
+ this.colCurrency = colSupplier.currencyColumn.get();
+ this.colAmount = colSupplier.amountColumn.get();
+ this.colMixedAmount = colSupplier.mixedAmountColumn.get();
+ this.colMinerTxFee = colSupplier.minerTxFeeColumn.get();
+ this.colMixedTradeFee = colSupplier.mixedTradeFeeColumn.get();
+ this.colBuyerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_BUYER_DEPOSIT);
+ this.colSellerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_SELLER_DEPOSIT);
+ this.colPaymentMethod = colSupplier.paymentMethodColumn.get();
+ this.colRole = colSupplier.roleColumn.get();
+ this.colOfferType = colSupplier.offerTypeColumn.get();
+ this.colClosingStatus = colSupplier.statusDescriptionColumn.get();
+
+ // Trade detail specific columns, some in common with BSQ swap trades detail.
+
+ this.colIsDepositPublished = colSupplier.depositPublishedColumn.get();
+ this.colIsDepositConfirmed = colSupplier.depositConfirmedColumn.get();
+ this.colIsPayoutPublished = colSupplier.payoutPublishedColumn.get();
+ this.colIsCompleted = colSupplier.fundsWithdrawnColumn.get();
+ this.colBisqTradeFee = colSupplier.bisqTradeDetailFeeColumn.get();
+ this.colTradeCost = colSupplier.tradeCostColumn.get();
+ this.colIsPaymentStartedMessageSent = colSupplier.paymentStartedMessageSentColumn.get();
+ this.colIsPaymentReceivedMessageSent = colSupplier.paymentReceivedMessageSentColumn.get();
+ //noinspection ConstantConditions
+ this.colAltcoinReceiveAddressColumn = colSupplier.altcoinReceiveAddressColumn.get();
+
+ // BSQ swap trade detail specific columns
+
+ this.status = colSupplier.bsqSwapStatusColumn.get();
+ this.colTxId = colSupplier.bsqSwapTxIdColumn.get();
+ this.colNumConfirmations = colSupplier.numConfirmationsColumn.get();
+ }
+
+ protected void validate() {
+ if (isTradeDetailTblBuilder.get()) {
+ if (protos.size() != 1)
+ throw new IllegalArgumentException("trade detail tbl can have only one row");
+ } else if (protos.isEmpty()) {
+ throw new IllegalArgumentException("trade tbl has no rows");
+ }
+ }
+
+ // Helper Functions
+
+ private final Supplier isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL);
+ protected final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
+ protected final Predicate isBsqTrade = (t) -> !isFiatOffer.test(t.getOffer()) && t.getOffer().getBaseCurrencyCode().equals("BSQ");
+ protected final Predicate isBsqSwapTrade = (t) -> t.getOffer().getIsBsqSwapOffer();
+ protected final Predicate isMyOffer = (t) -> t.getOffer().getIsMyOffer();
+ protected final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
+ protected final Predicate isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name());
+ protected final Predicate isBtcSeller = (t) -> (isMyOffer.test(t) && isSellOffer.test(t))
+ || (!isMyOffer.test(t) && !isSellOffer.test(t));
+ protected final Predicate isTradeFeeBtc = (t) -> isMyOffer.test(t)
+ ? t.getOffer().getIsCurrencyForMakerFeeBtc()
+ : t.getIsCurrencyForTakerFeeBtc();
+
+
+ // Column Value Functions
+
+ // Altcoin volumes from server are string representations of decimals.
+ // Converting them to longs ("sats") requires shifting the decimal points
+ // to left: 2 for BSQ, 8 for other altcoins.
+ protected final Function toAltcoinTradeVolumeAsLong = (t) ->
+ isBsqTrade.test(t)
+ ? new BigDecimal(t.getTradeVolume()).movePointRight(2).longValue()
+ : new BigDecimal(t.getTradeVolume()).movePointRight(8).longValue();
+
+ protected final Function toTradeVolumeAsString = (t) ->
+ isFiatTrade.test(t)
+ ? t.getTradeVolume()
+ : formatSatoshis(t.getTradeAmountAsLong());
+
+ protected final Function toTradeVolumeAsLong = (t) ->
+ isFiatTrade.test(t)
+ ? Long.parseLong(t.getTradeVolume())
+ : toAltcoinTradeVolumeAsLong.apply(t);
+
+ protected final Function toTradeAmount = (t) ->
+ isFiatTrade.test(t)
+ ? t.getTradeAmountAsLong()
+ : toTradeVolumeAsLong.apply(t);
+
+ protected final Function toMarket = (t) ->
+ t.getOffer().getBaseCurrencyCode() + "/"
+ + t.getOffer().getCounterCurrencyCode();
+
+ protected final Function toPaymentCurrencyCode = (t) ->
+ isFiatTrade.test(t)
+ ? t.getOffer().getCounterCurrencyCode()
+ : t.getOffer().getBaseCurrencyCode();
+
+ protected final Function toPriceDeviation = (t) ->
+ t.getOffer().getUseMarketBasedPrice()
+ ? format("%.2f%s", t.getOffer().getMarketPriceMarginPct(), "%")
+ : "N/A";
+
+ protected final Function toMyMinerTxFee = (t) -> {
+ if (isBsqSwapTrade.test(t)) {
+ // The BTC seller pays the miner fee for both sides.
+ return isBtcSeller.test(t) ? t.getTxFeeAsLong() : 0L;
+ } else {
+ return isTaker.test(t)
+ ? t.getTxFeeAsLong()
+ : t.getOffer().getTxFee();
+ }
+ };
+
+ protected final Function toTradeFeeBsq = (t) -> {
+ var isMyOffer = t.getOffer().getIsMyOffer();
+ if (isMyOffer) {
+ return t.getOffer().getIsCurrencyForMakerFeeBtc()
+ ? 0L // Maker paid BTC fee, return 0.
+ : t.getOffer().getMakerFee();
+ } else {
+ return t.getIsCurrencyForTakerFeeBtc()
+ ? 0L // Taker paid BTC fee, return 0.
+ : t.getTakerFeeAsLong();
+ }
+ };
+
+ protected final Function toTradeFeeBtc = (t) -> {
+ var isMyOffer = t.getOffer().getIsMyOffer();
+ if (isMyOffer) {
+ return t.getOffer().getIsCurrencyForMakerFeeBtc()
+ ? t.getOffer().getMakerFee()
+ : 0L; // Maker paid BSQ fee, return 0.
+ } else {
+ return t.getIsCurrencyForTakerFeeBtc()
+ ? t.getTakerFeeAsLong()
+ : 0L; // Taker paid BSQ fee, return 0.
+ }
+ };
+
+ protected final Function toMyMakerOrTakerFee = (t) -> {
+ if (isBsqSwapTrade.test(t)) {
+ return isTaker.test(t)
+ ? t.getBsqSwapTradeInfo().getBsqTakerTradeFee()
+ : t.getBsqSwapTradeInfo().getBsqMakerTradeFee();
+ } else {
+ return isTaker.test(t)
+ ? t.getTakerFeeAsLong()
+ : t.getOffer().getMakerFee();
+ }
+ };
+
+ protected final Function toOfferType = (t) -> {
+ if (isFiatTrade.test(t)) {
+ return t.getOffer().getDirection() + " " + t.getOffer().getBaseCurrencyCode();
+ } else {
+ if (t.getOffer().getDirection().equals("BUY")) {
+ return "SELL " + t.getOffer().getBaseCurrencyCode();
+ } else {
+ return "BUY " + t.getOffer().getBaseCurrencyCode();
+ }
+ }
+ };
+
+ protected final Predicate showAltCoinBuyerAddress = (t) -> {
+ if (isFiatTrade.test(t)) {
+ return false;
+ } else {
+ ContractInfo contract = t.getContract();
+ boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
+ if (isTaker.test(t)) {
+ return !isBuyerMakerAndSellerTaker;
+ } else {
+ return isBuyerMakerAndSellerTaker;
+ }
+ }
+ };
+
+ protected final Function toAltcoinReceiveAddress = (t) -> {
+ if (showAltCoinBuyerAddress.test(t)) {
+ ContractInfo contract = t.getContract();
+ boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
+ return isBuyerMakerAndSellerTaker // (is BTC buyer / maker)
+ ? contract.getTakerPaymentAccountPayload().getAddress()
+ : contract.getMakerPaymentAccountPayload().getAddress();
+ } else {
+ return "";
+ }
+ };
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/AddressBalanceTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/AddressBalanceTableBuilder.java
new file mode 100644
index 0000000..a003585
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/AddressBalanceTableBuilder.java
@@ -0,0 +1,71 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.*;
+import bisq.proto.grpc.AddressBalanceInfo;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.ADDRESS_BALANCE_TBL;
+import static java.lang.String.format;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a List of
+ * {@code bisq.proto.grpc.AddressBalanceInfo} objects.
+ */
+class AddressBalanceTableBuilder extends AbstractTableBuilder {
+
+ // Default columns not dynamically generated with address info.
+ private final Column colAddress;
+ private final Column colAvailableBalance;
+ private final Column colConfirmations;
+ private final Column colIsUsed;
+
+ AddressBalanceTableBuilder(List> protos) {
+ super(ADDRESS_BALANCE_TBL, protos);
+ colAddress = new StringColumn(format(COL_HEADER_ADDRESS, "BTC"));
+ this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE);
+ this.colConfirmations = new LongColumn(COL_HEADER_CONFIRMATIONS);
+ this.colIsUsed = new BooleanColumn(COL_HEADER_IS_USED_ADDRESS);
+ }
+
+ public Table build() {
+ List addresses = protos.stream()
+ .map(a -> (AddressBalanceInfo) a)
+ .collect(Collectors.toList());
+
+ // Populate columns with address info.
+ //noinspection SimplifyStreamApiCallChains
+ addresses.stream().forEachOrdered(a -> {
+ colAddress.addRow(a.getAddress());
+ colAvailableBalance.addRow(a.getBalance());
+ colConfirmations.addRow(a.getNumConfirmations());
+ colIsUsed.addRow(!a.getIsAddressUnused());
+ });
+
+ // Define and return the table instance with populated columns.
+ return new Table(colAddress,
+ colAvailableBalance.asStringColumn(),
+ colConfirmations.asStringColumn(),
+ colIsUsed.asStringColumn());
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/BsqBalanceTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/BsqBalanceTableBuilder.java
new file mode 100644
index 0000000..19c40d3
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/BsqBalanceTableBuilder.java
@@ -0,0 +1,75 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.Column;
+import bisq.bots.table.column.SatoshiColumn;
+import bisq.proto.grpc.BsqBalanceInfo;
+
+import java.util.List;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.BSQ_BALANCE_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a
+ * {@code bisq.proto.grpc.BsqBalanceInfo} object.
+ */
+class BsqBalanceTableBuilder extends AbstractTableBuilder {
+
+ // Default columns not dynamically generated with bsq balance info.
+ private final Column colAvailableConfirmedBalance;
+ private final Column colUnverifiedBalance;
+ private final Column colUnconfirmedChangeBalance;
+ private final Column colLockedForVotingBalance;
+ private final Column colLockupBondsBalance;
+ private final Column colUnlockingBondsBalance;
+
+ BsqBalanceTableBuilder(List> protos) {
+ super(BSQ_BALANCE_TBL, protos);
+ this.colAvailableConfirmedBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_CONFIRMED_BALANCE, true);
+ this.colUnverifiedBalance = new SatoshiColumn(COL_HEADER_UNVERIFIED_BALANCE, true);
+ this.colUnconfirmedChangeBalance = new SatoshiColumn(COL_HEADER_UNCONFIRMED_CHANGE_BALANCE, true);
+ this.colLockedForVotingBalance = new SatoshiColumn(COL_HEADER_LOCKED_FOR_VOTING_BALANCE, true);
+ this.colLockupBondsBalance = new SatoshiColumn(COL_HEADER_LOCKUP_BONDS_BALANCE, true);
+ this.colUnlockingBondsBalance = new SatoshiColumn(COL_HEADER_UNLOCKING_BONDS_BALANCE, true);
+ }
+
+ public Table build() {
+ BsqBalanceInfo balance = (BsqBalanceInfo) protos.get(0);
+
+ // Populate columns with bsq balance info.
+
+ colAvailableConfirmedBalance.addRow(balance.getAvailableConfirmedBalance());
+ colUnverifiedBalance.addRow(balance.getUnverifiedBalance());
+ colUnconfirmedChangeBalance.addRow(balance.getUnconfirmedChangeBalance());
+ colLockedForVotingBalance.addRow(balance.getLockedForVotingBalance());
+ colLockupBondsBalance.addRow(balance.getLockupBondsBalance());
+ colUnlockingBondsBalance.addRow(balance.getUnlockingBondsBalance());
+
+ // Define and return the table instance with populated columns.
+
+ return new Table(colAvailableConfirmedBalance.asStringColumn(),
+ colUnverifiedBalance.asStringColumn(),
+ colUnconfirmedChangeBalance.asStringColumn(),
+ colLockedForVotingBalance.asStringColumn(),
+ colLockupBondsBalance.asStringColumn(),
+ colUnlockingBondsBalance.asStringColumn());
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/BtcBalanceTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/BtcBalanceTableBuilder.java
new file mode 100644
index 0000000..0360026
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/BtcBalanceTableBuilder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.Column;
+import bisq.bots.table.column.SatoshiColumn;
+import bisq.proto.grpc.BtcBalanceInfo;
+
+import java.util.List;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.BTC_BALANCE_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a
+ * {@code bisq.proto.grpc.BtcBalanceInfo} object.
+ */
+class BtcBalanceTableBuilder extends AbstractTableBuilder {
+
+ // Default columns not dynamically generated with btc balance info.
+ private final Column colAvailableBalance;
+ private final Column colReservedBalance;
+ private final Column colTotalAvailableBalance;
+ private final Column colLockedBalance;
+
+ BtcBalanceTableBuilder(List> protos) {
+ super(BTC_BALANCE_TBL, protos);
+ this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE);
+ this.colReservedBalance = new SatoshiColumn(COL_HEADER_RESERVED_BALANCE);
+ this.colTotalAvailableBalance = new SatoshiColumn(COL_HEADER_TOTAL_AVAILABLE_BALANCE);
+ this.colLockedBalance = new SatoshiColumn(COL_HEADER_LOCKED_BALANCE);
+ }
+
+ public Table build() {
+ BtcBalanceInfo balance = (BtcBalanceInfo) protos.get(0);
+
+ // Populate columns with btc balance info.
+
+ colAvailableBalance.addRow(balance.getAvailableBalance());
+ colReservedBalance.addRow(balance.getReservedBalance());
+ colTotalAvailableBalance.addRow(balance.getTotalAvailableBalance());
+ colLockedBalance.addRow(balance.getLockedBalance());
+
+ // Define and return the table instance with populated columns.
+
+ return new Table(colAvailableBalance.asStringColumn(),
+ colReservedBalance.asStringColumn(),
+ colTotalAvailableBalance.asStringColumn(),
+ colLockedBalance.asStringColumn());
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/ClosedTradeTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/ClosedTradeTableBuilder.java
new file mode 100644
index 0000000..dc76d51
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/ClosedTradeTableBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+
+import java.util.List;
+
+import static bisq.bots.table.builder.TableType.CLOSED_TRADES_TBL;
+
+@SuppressWarnings("ConstantConditions")
+class ClosedTradeTableBuilder extends AbstractTradeListBuilder {
+
+ ClosedTradeTableBuilder(List> protos) {
+ super(CLOSED_TRADES_TBL, protos);
+ }
+
+ public Table build() {
+ populateColumns();
+ return new Table(colTradeId,
+ colCreateDate.asStringColumn(),
+ colMarket,
+ colPrice.justify(),
+ colPriceDeviation.justify(),
+ colAmount.asStringColumn(),
+ colMixedAmount.justify(),
+ colCurrency,
+ colMinerTxFee.asStringColumn(),
+ colMixedTradeFee.asStringColumn(),
+ colBuyerDeposit.asStringColumn(),
+ colSellerDeposit.asStringColumn(),
+ colOfferType,
+ colClosingStatus);
+ }
+
+ private void populateColumns() {
+ trades.forEach(t -> {
+ colTradeId.addRow(t.getTradeId());
+ colCreateDate.addRow(t.getDate());
+ colMarket.addRow(toMarket.apply(t));
+ colPrice.addRow(t.getTradePrice());
+ colPriceDeviation.addRow(toPriceDeviation.apply(t));
+ colAmount.addRow(t.getTradeAmountAsLong());
+ colMixedAmount.addRow(t.getTradeVolume());
+ colCurrency.addRow(toPaymentCurrencyCode.apply(t));
+ colMinerTxFee.addRow(toMyMinerTxFee.apply(t));
+
+ if (t.getOffer().getIsBsqSwapOffer()) {
+ // For BSQ Swaps, BTC buyer pays the BSQ trade fee for both sides (BTC seller pays no fee).
+ var optionalTradeFeeBsq = isBtcSeller.test(t) ? 0L : toTradeFeeBsq.apply(t);
+ colMixedTradeFee.addRow(optionalTradeFeeBsq, true);
+ } else if (isTradeFeeBtc.test(t)) {
+ colMixedTradeFee.addRow(toTradeFeeBtc.apply(t), false);
+ } else {
+ // V1 trade fee paid in BSQ.
+ colMixedTradeFee.addRow(toTradeFeeBsq.apply(t), true);
+ }
+
+ colBuyerDeposit.addRow(t.getOffer().getBuyerSecurityDeposit());
+ colSellerDeposit.addRow(t.getOffer().getSellerSecurityDeposit());
+ colOfferType.addRow(toOfferType.apply(t));
+ colClosingStatus.addRow(t.getClosingStatus());
+ });
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/FailedTradeTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/FailedTradeTableBuilder.java
new file mode 100644
index 0000000..7a78d2c
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/FailedTradeTableBuilder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+
+import java.util.List;
+
+import static bisq.bots.table.builder.TableType.FAILED_TRADES_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a list of {@code bisq.proto.grpc.TradeInfo} objects.
+ */
+@SuppressWarnings("ConstantConditions")
+class FailedTradeTableBuilder extends AbstractTradeListBuilder {
+
+ FailedTradeTableBuilder(List> protos) {
+ super(FAILED_TRADES_TBL, protos);
+ }
+
+ public Table build() {
+ populateColumns();
+ return new Table(colTradeId,
+ colCreateDate.asStringColumn(),
+ colMarket,
+ colPrice.justify(),
+ colAmount.asStringColumn(),
+ colMixedAmount.justify(),
+ colCurrency,
+ colOfferType,
+ colRole,
+ colClosingStatus);
+ }
+
+ private void populateColumns() {
+ trades.forEach(t -> {
+ colTradeId.addRow(t.getTradeId());
+ colCreateDate.addRow(t.getDate());
+ colMarket.addRow(toMarket.apply(t));
+ colPrice.addRow(t.getTradePrice());
+ colAmount.addRow(t.getTradeAmountAsLong());
+ colMixedAmount.addRow(t.getTradeVolume());
+ colCurrency.addRow(toPaymentCurrencyCode.apply(t));
+ colOfferType.addRow(toOfferType.apply(t));
+ colRole.addRow(t.getRole());
+ colClosingStatus.addRow("Failed");
+ });
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/OfferTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/OfferTableBuilder.java
new file mode 100644
index 0000000..3136ec6
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/OfferTableBuilder.java
@@ -0,0 +1,259 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.*;
+import bisq.proto.grpc.OfferInfo;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.OFFER_TBL;
+import static bisq.bots.table.column.Column.JUSTIFICATION.*;
+import static bisq.bots.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES;
+import static java.lang.String.format;
+import static protobuf.OfferDirection.BUY;
+import static protobuf.OfferDirection.SELL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a List of
+ * {@code bisq.proto.grpc.OfferInfo} objects.
+ */
+class OfferTableBuilder extends AbstractTableBuilder {
+
+ // Columns common to both fiat and cryptocurrency offers.
+ private final Column colOfferId = new StringColumn(COL_HEADER_UUID, LEFT);
+ private final Column colDirection = new StringColumn(COL_HEADER_DIRECTION, LEFT);
+ private final Column colAmount = new SatoshiColumn("Temp Amount", NONE);
+ private final Column colMinAmount = new SatoshiColumn("Temp Min Amount", NONE);
+ private final Column colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT);
+ private final Column colCreateDate = new Iso8601DateTimeColumn(COL_HEADER_CREATION_DATE);
+
+ OfferTableBuilder(List> protos) {
+ super(OFFER_TBL, protos);
+ }
+
+ public Table build() {
+ List offers = protos.stream().map(p -> (OfferInfo) p).collect(Collectors.toList());
+ return isShowingFiatOffers.get()
+ ? buildFiatOfferTable(offers)
+ : buildCryptoCurrencyOfferTable(offers);
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public Table buildFiatOfferTable(List offers) {
+ @Nullable
+ Column colEnabled = enabledColumn.get(); // Not boolean: "YES", "NO", or "PENDING"
+ Column colFiatPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE, fiatTradeCurrency.get()), RIGHT);
+ Column colVolume = new StringColumn(format("Temp Volume (%s)", fiatTradeCurrency.get()), NONE);
+ Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", fiatTradeCurrency.get()), NONE);
+ @Nullable
+ Column colTriggerPrice = fiatTriggerPriceColumn.get();
+
+ // Populate columns with offer info.
+
+ offers.forEach(o -> {
+ if (colEnabled != null)
+ colEnabled.addRow(toEnabled.apply(o));
+
+ colDirection.addRow(o.getDirection());
+ colFiatPrice.addRow(o.getPrice());
+ colMinAmount.addRow(o.getMinAmount());
+ colAmount.addRow(o.getAmount());
+ colVolume.addRow(o.getVolume());
+ colMinVolume.addRow(o.getMinVolume());
+
+ if (colTriggerPrice != null)
+ colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice()));
+
+ colPaymentMethod.addRow(o.getPaymentMethodShortName());
+ colCreateDate.addRow(o.getDate());
+ colOfferId.addRow(o.getId());
+ });
+
+ ZippedStringColumns amountRange = zippedAmountRangeColumns.get();
+ ZippedStringColumns volumeRange =
+ new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, fiatTradeCurrency.get()),
+ RIGHT,
+ " - ",
+ colMinVolume.asStringColumn(),
+ colVolume.asStringColumn());
+
+ // Define and return the table instance with populated columns.
+
+ if (isShowingMyOffers.get()) {
+ return new Table(colEnabled.asStringColumn(),
+ colDirection,
+ colFiatPrice.justify(),
+ amountRange.asStringColumn(EXCLUDE_DUPLICATES),
+ volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
+ colTriggerPrice.justify(),
+ colPaymentMethod,
+ colCreateDate.asStringColumn(),
+ colOfferId);
+ } else {
+ return new Table(colDirection,
+ colFiatPrice.justify(),
+ amountRange.asStringColumn(EXCLUDE_DUPLICATES),
+ volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
+ colPaymentMethod,
+ colCreateDate.asStringColumn(),
+ colOfferId);
+ }
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public Table buildCryptoCurrencyOfferTable(List offers) {
+ @Nullable
+ Column colEnabled = enabledColumn.get(); // Not boolean: YES, NO, or PENDING
+ Column colBtcPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE_OF_ALTCOIN, altcoinTradeCurrency.get()), RIGHT);
+ Column colVolume = new StringColumn(format("Temp Volume (%s)", altcoinTradeCurrency.get()), NONE);
+ Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", altcoinTradeCurrency.get()), NONE);
+ @Nullable
+ Column colTriggerPrice = altcoinTriggerPriceColumn.get();
+
+ // Populate columns with offer info.
+
+ offers.forEach(o -> {
+ if (colEnabled != null)
+ colEnabled.addRow(toEnabled.apply(o));
+
+ colDirection.addRow(directionFormat.apply(o));
+ colBtcPrice.addRow(o.getPrice());
+ colAmount.addRow(o.getAmount());
+ colMinAmount.addRow(o.getMinAmount());
+ colVolume.addRow(o.getVolume());
+ colMinVolume.addRow(o.getMinVolume());
+
+ if (colTriggerPrice != null)
+ colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice()));
+
+ colPaymentMethod.addRow(o.getPaymentMethodShortName());
+ colCreateDate.addRow(o.getDate());
+ colOfferId.addRow(o.getId());
+ });
+
+ ZippedStringColumns amountRange = zippedAmountRangeColumns.get();
+ ZippedStringColumns volumeRange =
+ new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, altcoinTradeCurrency.get()),
+ RIGHT,
+ " - ",
+ colMinVolume.asStringColumn(),
+ colVolume.asStringColumn());
+
+ // Define and return the table instance with populated columns.
+
+ if (isShowingMyOffers.get()) {
+ if (isShowingBsqOffers.get()) {
+ return new Table(colEnabled.asStringColumn(),
+ colDirection,
+ colBtcPrice.justify(),
+ amountRange.asStringColumn(EXCLUDE_DUPLICATES),
+ volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
+ colPaymentMethod,
+ colCreateDate.asStringColumn(),
+ colOfferId);
+ } else {
+ return new Table(colEnabled.asStringColumn(),
+ colDirection,
+ colBtcPrice.justify(),
+ amountRange.asStringColumn(EXCLUDE_DUPLICATES),
+ volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
+ colTriggerPrice.justify(),
+ colPaymentMethod,
+ colCreateDate.asStringColumn(),
+ colOfferId);
+ }
+ } else {
+ return new Table(colDirection,
+ colBtcPrice.justify(),
+ amountRange.asStringColumn(EXCLUDE_DUPLICATES),
+ volumeRange.asStringColumn(EXCLUDE_DUPLICATES),
+ colPaymentMethod,
+ colCreateDate.asStringColumn(),
+ colOfferId);
+ }
+ }
+
+ private final Function toBlankOrNonZeroValue = (s) -> s.trim().equals("0") ? "" : s;
+ private final Supplier firstOfferInList = () -> (OfferInfo) protos.get(0);
+ private final Supplier isShowingMyOffers = () -> firstOfferInList.get().getIsMyOffer();
+ private final Supplier isShowingFiatOffers = () -> isFiatOffer.test(firstOfferInList.get());
+ private final Supplier fiatTradeCurrency = () -> firstOfferInList.get().getCounterCurrencyCode();
+ private final Supplier altcoinTradeCurrency = () -> firstOfferInList.get().getBaseCurrencyCode();
+ private final Supplier isShowingBsqOffers = () ->
+ !isFiatOffer.test(firstOfferInList.get()) && altcoinTradeCurrency.get().equals("BSQ");
+
+ @Nullable // Not a boolean column: YES, NO, or PENDING.
+ private final Supplier enabledColumn = () ->
+ isShowingMyOffers.get()
+ ? new StringColumn(COL_HEADER_ENABLED, LEFT)
+ : null;
+ @Nullable
+ private final Supplier fiatTriggerPriceColumn = () ->
+ isShowingMyOffers.get()
+ ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, fiatTradeCurrency.get()), RIGHT)
+ : null;
+ @Nullable
+ private final Supplier altcoinTriggerPriceColumn = () ->
+ isShowingMyOffers.get() && !isShowingBsqOffers.get()
+ ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, altcoinTradeCurrency.get()), RIGHT)
+ : null;
+
+ private final Function toEnabled = (o) -> {
+ if (o.getIsMyOffer() && o.getIsMyPendingOffer())
+ return "PENDING";
+ else
+ return o.getIsActivated() ? "YES" : "NO";
+ };
+
+ private final Function toMirroredDirection = (d) ->
+ d.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name();
+
+ private final Function directionFormat = (o) -> {
+ if (isFiatOffer.test(o)) {
+ return o.getBaseCurrencyCode();
+ } else {
+ // Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)".
+ String direction = o.getDirection();
+ String mirroredDirection = toMirroredDirection.apply(direction);
+ Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase();
+ return format("%s %s (%s %s)",
+ mixedCase.apply(mirroredDirection),
+ o.getBaseCurrencyCode(),
+ mixedCase.apply(direction),
+ o.getCounterCurrencyCode());
+ }
+ };
+
+ private final Supplier zippedAmountRangeColumns = () -> {
+ if (colMinAmount.isEmpty() || colAmount.isEmpty())
+ throw new IllegalStateException("amount columns must have data");
+
+ return new ZippedStringColumns(COL_HEADER_AMOUNT_RANGE,
+ RIGHT,
+ " - ",
+ colMinAmount.asStringColumn(),
+ colAmount.asStringColumn());
+ };
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/OpenTradeTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/OpenTradeTableBuilder.java
new file mode 100644
index 0000000..f962102
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/OpenTradeTableBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+
+import java.util.List;
+
+import static bisq.bots.table.builder.TableType.OPEN_TRADES_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a list of {@code bisq.proto.grpc.TradeInfo} objects.
+ */
+@SuppressWarnings("ConstantConditions")
+class OpenTradeTableBuilder extends AbstractTradeListBuilder {
+
+ OpenTradeTableBuilder(List> protos) {
+ super(OPEN_TRADES_TBL, protos);
+ }
+
+ public Table build() {
+ populateColumns();
+ return new Table(colTradeId,
+ colCreateDate.asStringColumn(),
+ colMarket,
+ colPrice.justify(),
+ colAmount.asStringColumn(),
+ colMixedAmount.justify(),
+ colCurrency,
+ colPaymentMethod,
+ colRole);
+ }
+
+ private void populateColumns() {
+ trades.forEach(t -> {
+ colTradeId.addRow(t.getTradeId());
+ colCreateDate.addRow(t.getDate());
+ colMarket.addRow(toMarket.apply(t));
+ colPrice.addRow(t.getTradePrice());
+ colAmount.addRow(t.getTradeAmountAsLong());
+ colMixedAmount.addRow(t.getTradeVolume());
+ colCurrency.addRow(toPaymentCurrencyCode.apply(t));
+ colPaymentMethod.addRow(t.getOffer().getPaymentMethodShortName());
+ colRole.addRow(t.getRole());
+ });
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/PaymentAccountTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/PaymentAccountTableBuilder.java
new file mode 100644
index 0000000..eff8428
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/PaymentAccountTableBuilder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.Column;
+import bisq.bots.table.column.StringColumn;
+import protobuf.PaymentAccount;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a List of
+ * {@code protobuf.PaymentAccount} objects.
+ */
+class PaymentAccountTableBuilder extends AbstractTableBuilder {
+
+ // Default columns not dynamically generated with payment account info.
+ private final Column colName;
+ private final Column colCurrency;
+ private final Column colPaymentMethod;
+ private final Column colId;
+
+ PaymentAccountTableBuilder(List> protos) {
+ super(PAYMENT_ACCOUNT_TBL, protos);
+ this.colName = new StringColumn(COL_HEADER_NAME);
+ this.colCurrency = new StringColumn(COL_HEADER_CURRENCY);
+ this.colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD);
+ this.colId = new StringColumn(COL_HEADER_UUID);
+ }
+
+ public Table build() {
+ List paymentAccounts = protos.stream()
+ .map(a -> (PaymentAccount) a)
+ .collect(Collectors.toList());
+
+ // Populate columns with payment account info.
+ //noinspection SimplifyStreamApiCallChains
+ paymentAccounts.stream().forEachOrdered(a -> {
+ colName.addRow(a.getAccountName());
+ colCurrency.addRow(a.getSelectedTradeCurrency().getCode());
+ colPaymentMethod.addRow(a.getPaymentMethod().getId());
+ colId.addRow(a.getId());
+ });
+
+ // Define and return the table instance with populated columns.
+ return new Table(colName, colCurrency, colPaymentMethod, colId);
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/TableBuilder.java
new file mode 100644
index 0000000..366f5cb
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TableBuilder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * Table builder factory. It is not conventionally named TableBuilderFactory because
+ * it has no static factory methods. The number of static fields and methods in the
+ * {@code bisq.bots.table} are kept to a minimum in an effort o reduce class load time
+ * in the session-less CLI.
+ */
+public class TableBuilder extends AbstractTableBuilder {
+
+ public TableBuilder(TableType tableType, Object proto) {
+ this(tableType, singletonList(proto));
+ }
+
+ public TableBuilder(TableType tableType, List> protos) {
+ super(tableType, protos);
+ }
+
+ public Table build() {
+ switch (tableType) {
+ case ADDRESS_BALANCE_TBL:
+ return new AddressBalanceTableBuilder(protos).build();
+ case BSQ_BALANCE_TBL:
+ return new BsqBalanceTableBuilder(protos).build();
+ case BTC_BALANCE_TBL:
+ return new BtcBalanceTableBuilder(protos).build();
+ case CLOSED_TRADES_TBL:
+ return new ClosedTradeTableBuilder(protos).build();
+ case FAILED_TRADES_TBL:
+ return new FailedTradeTableBuilder(protos).build();
+ case OFFER_TBL:
+ return new OfferTableBuilder(protos).build();
+ case OPEN_TRADES_TBL:
+ return new OpenTradeTableBuilder(protos).build();
+ case PAYMENT_ACCOUNT_TBL:
+ return new PaymentAccountTableBuilder(protos).build();
+ case TRADE_DETAIL_TBL:
+ return new TradeDetailTableBuilder(protos).build();
+ case TRANSACTION_TBL:
+ return new TransactionTableBuilder(protos).build();
+ default:
+ throw new IllegalArgumentException("invalid cli table type " + tableType.name());
+ }
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TableBuilderConstants.java b/java-examples/src/main/java/bisq/bots/table/builder/TableBuilderConstants.java
new file mode 100644
index 0000000..118a281
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TableBuilderConstants.java
@@ -0,0 +1,82 @@
+/*
+ * 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.bots.table.builder;
+
+/**
+ * Table column name constants.
+ */
+class TableBuilderConstants {
+ static final String COL_HEADER_ADDRESS = "%-3s Address";
+ static final String COL_HEADER_AMOUNT = "Amount";
+ static final String COL_HEADER_AMOUNT_IN_BTC = "Amount in BTC";
+ static final String COL_HEADER_AMOUNT_RANGE = "BTC(min - max)";
+ static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance";
+ static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance";
+ static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance";
+ static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance";
+ static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance";
+ static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance";
+ static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance";
+ static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance";
+ static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance";
+ static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance";
+ static final String COL_HEADER_BSQ_SWAP_TRADE_ROLE = "My BSQ Swap Role";
+ static final String COL_HEADER_BUYER_DEPOSIT = "Buyer Deposit (BTC)";
+ static final String COL_HEADER_SELLER_DEPOSIT = "Seller Deposit (BTC)";
+ static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
+ static final String COL_HEADER_DEVIATION = "Deviation";
+ static final String COL_HEADER_IS_USED_ADDRESS = "Is Used";
+ static final String COL_HEADER_CREATION_DATE = "Creation Date (UTC)";
+ static final String COL_HEADER_CURRENCY = "Currency";
+ static final String COL_HEADER_DATE_TIME = "Date/Time (UTC)";
+ static final String COL_HEADER_DETAILED_AMOUNT = "Amount(%-3s)";
+ static final String COL_HEADER_DETAILED_PRICE = "Price in %-3s for 1 BTC";
+ static final String COL_HEADER_DETAILED_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s";
+ static final String COL_HEADER_DIRECTION = "Buy/Sell";
+ static final String COL_HEADER_ENABLED = "Enabled";
+ static final String COL_HEADER_MARKET = "Market";
+ static final String COL_HEADER_NAME = "Name";
+ static final String COL_HEADER_OFFER_TYPE = "Offer Type";
+ static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
+ static final String COL_HEADER_PRICE = "Price";
+ static final String COL_HEADER_STATUS = "Status";
+ static final String COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS = "%-3s Buyer Address";
+ static final String COL_HEADER_TRADE_BUYER_COST = "Buyer Cost(%-3s)";
+ 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_PAYMENT_SENT = "%-3s Sent";
+ static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = "%-3s 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_ID = "Trade ID";
+ static final String COL_HEADER_TRADE_ROLE = "My Role";
+ static final String COL_HEADER_TRADE_SHORT_ID = "ID";
+ static final String COL_HEADER_TRADE_MAKER_FEE = "Maker Fee(%-3s)";
+ static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)";
+ static final String COL_HEADER_TRADE_FEE = "Trade Fee";
+ static final String COL_HEADER_TRIGGER_PRICE = "Trigger Price(%-3s)";
+ static final String COL_HEADER_TX_ID = "Tx ID";
+ static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";
+ static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)";
+ static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)";
+ static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)";
+ static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed";
+ static final String COL_HEADER_TX_MEMO = "Memo";
+ static final String COL_HEADER_VOLUME_RANGE = "%-3s(min - max)";
+ static final String COL_HEADER_UUID = "ID";
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TableType.java b/java-examples/src/main/java/bisq/bots/table/builder/TableType.java
new file mode 100644
index 0000000..9c1b3b3
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TableType.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bots.table.builder;
+
+/**
+ * Used as param in TableBuilder constructor instead of inspecting
+ * protos to find out what kind of CLI output table should be built.
+ */
+public enum TableType {
+ ADDRESS_BALANCE_TBL,
+ BSQ_BALANCE_TBL,
+ BTC_BALANCE_TBL,
+ CLOSED_TRADES_TBL,
+ FAILED_TRADES_TBL,
+ OFFER_TBL,
+ OPEN_TRADES_TBL,
+ PAYMENT_ACCOUNT_TBL,
+ TRADE_DETAIL_TBL,
+ TRANSACTION_TBL
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TradeDetailTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/TradeDetailTableBuilder.java
new file mode 100644
index 0000000..6fdbdaa
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TradeDetailTableBuilder.java
@@ -0,0 +1,159 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.Column;
+import bisq.proto.grpc.TradeInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+import static bisq.bots.table.builder.TableType.TRADE_DETAIL_TBL;
+import static java.lang.String.format;
+import static protobuf.BsqSwapTrade.State.COMPLETED;
+import static protobuf.BsqSwapTrade.State.PREPARATION;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a {@code bisq.proto.grpc.TradeInfo} object.
+ */
+@SuppressWarnings("ConstantConditions")
+class TradeDetailTableBuilder extends AbstractTradeListBuilder {
+
+ private final Predicate isPendingBsqSwap = (t) -> t.getState().equals(PREPARATION.name());
+ private final Predicate isCompletedBsqSwap = (t) -> t.getState().equals(COMPLETED.name());
+
+ TradeDetailTableBuilder(List> protos) {
+ super(TRADE_DETAIL_TBL, protos);
+ }
+
+ /**
+ * Build a single row trade detail table.
+ *
+ * @return Table containing one row
+ */
+ public Table build() {
+ // A trade detail table only has one row.
+ var trade = trades.get(0);
+ populateColumns(trade);
+ List> columns = defineColumnList(trade);
+ return new Table(columns.toArray(new Column>[0]));
+ }
+
+ private void populateColumns(TradeInfo trade) {
+ if (isBsqSwapTrade.test(trade)) {
+ var isPending = isPendingBsqSwap.test(trade);
+ var isCompleted = isCompletedBsqSwap.test(trade);
+ if (isPending == isCompleted)
+ throw new IllegalStateException(
+ format("programmer error: trade must be either pending or completed, is pending=%s and completed=%s",
+ isPending,
+ isCompleted));
+ populateBsqSwapTradeColumns(trade);
+ } else {
+ populateBisqV1TradeColumns(trade);
+ }
+ }
+
+ private void populateBisqV1TradeColumns(TradeInfo trade) {
+ colTradeId.addRow(trade.getShortId());
+ colRole.addRow(trade.getRole());
+ colPrice.addRow(trade.getTradePrice());
+ colAmount.addRow(toTradeAmount.apply(trade));
+ colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
+ colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
+ colIsDepositPublished.addRow(trade.getIsDepositPublished());
+ colIsDepositConfirmed.addRow(trade.getIsDepositConfirmed());
+ colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
+ colIsPaymentStartedMessageSent.addRow(trade.getIsPaymentStartedMessageSent());
+ colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceivedMessageSent());
+ colIsPayoutPublished.addRow(trade.getIsPayoutPublished());
+ colIsCompleted.addRow(trade.getIsCompleted());
+ if (colAltcoinReceiveAddressColumn != null)
+ colAltcoinReceiveAddressColumn.addRow(toAltcoinReceiveAddress.apply(trade));
+ }
+
+ private void populateBsqSwapTradeColumns(TradeInfo trade) {
+ colTradeId.addRow(trade.getShortId());
+ colRole.addRow(trade.getRole());
+ colPrice.addRow(trade.getTradePrice());
+ colAmount.addRow(toTradeAmount.apply(trade));
+ colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
+ colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
+
+ colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
+
+ var isCompleted = isCompletedBsqSwap.test(trade);
+ status.addRow(isCompleted ? "COMPLETED" : "PENDING");
+ if (isCompleted) {
+ colTxId.addRow(trade.getBsqSwapTradeInfo().getTxId());
+ colNumConfirmations.addRow(trade.getBsqSwapTradeInfo().getNumConfirmations());
+ }
+ }
+
+ private List> defineColumnList(TradeInfo trade) {
+ return isBsqSwapTrade.test(trade)
+ ? getBsqSwapTradeColumnList(isCompletedBsqSwap.test(trade))
+ : getBisqV1TradeColumnList();
+ }
+
+ private List> getBisqV1TradeColumnList() {
+ List> columns = new ArrayList<>() {{
+ add(colTradeId);
+ add(colRole);
+ add(colPrice.justify());
+ add(colAmount.asStringColumn());
+ add(colMinerTxFee.asStringColumn());
+ add(colBisqTradeFee.asStringColumn());
+ add(colIsDepositPublished.asStringColumn());
+ add(colIsDepositConfirmed.asStringColumn());
+ add(colTradeCost.justify());
+ add(colIsPaymentStartedMessageSent.asStringColumn());
+ add(colIsPaymentReceivedMessageSent.asStringColumn());
+ add(colIsPayoutPublished.asStringColumn());
+ add(colIsCompleted.asStringColumn());
+ }};
+
+ if (colAltcoinReceiveAddressColumn != null)
+ columns.add(colAltcoinReceiveAddressColumn);
+
+ return columns;
+ }
+
+ private List> getBsqSwapTradeColumnList(boolean isCompleted) {
+ List> columns = new ArrayList<>() {{
+ add(colTradeId);
+ add(colRole);
+ add(colPrice.justify());
+ add(colAmount.asStringColumn());
+ add(colMinerTxFee.asStringColumn());
+ add(colBisqTradeFee.asStringColumn());
+ add(colTradeCost.justify());
+ add(status);
+ }};
+
+ if (isCompleted)
+ columns.add(colTxId);
+
+ if (!colNumConfirmations.isEmpty())
+ columns.add(colNumConfirmations.asStringColumn());
+
+ return columns;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TradeTableColumnSupplier.java b/java-examples/src/main/java/bisq/bots/table/builder/TradeTableColumnSupplier.java
new file mode 100644
index 0000000..ad2eeed
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TradeTableColumnSupplier.java
@@ -0,0 +1,285 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.column.*;
+import bisq.proto.grpc.ContractInfo;
+import bisq.proto.grpc.OfferInfo;
+import bisq.proto.grpc.TradeInfo;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.*;
+import static bisq.bots.table.column.AltcoinVolumeColumn.DISPLAY_MODE.ALTCOIN_VOLUME;
+import static bisq.bots.table.column.AltcoinVolumeColumn.DISPLAY_MODE.BSQ_VOLUME;
+import static bisq.bots.table.column.Column.JUSTIFICATION.LEFT;
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+import static java.lang.String.format;
+
+/**
+ * Convenience for supplying column definitions to
+ * open/closed/failed/detail trade table builders.
+ */
+@Slf4j
+class TradeTableColumnSupplier {
+
+ @Getter
+ private final TableType tableType;
+ @Getter
+ private final List trades;
+
+ public TradeTableColumnSupplier(TableType tableType, List trades) {
+ this.tableType = tableType;
+ this.trades = trades;
+ }
+
+ private final Supplier isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL);
+ private final Supplier isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL);
+ private final Supplier isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL);
+ private final Supplier isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL);
+ private final Supplier firstRow = () -> getTrades().get(0);
+ private final Predicate isFiatOffer = (o) -> o.getBaseCurrencyCode().equals("BTC");
+ private final Predicate isFiatTrade = (t) -> isFiatOffer.test(t.getOffer());
+ private final Predicate isBsqSwapTrade = (t) -> t.getOffer().getIsBsqSwapOffer();
+ private final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker");
+ private final Supplier isSwapTradeDetail = () ->
+ isTradeDetailTblBuilder.get() && isBsqSwapTrade.test(firstRow.get());
+
+ final Supplier tradeIdColumn = () -> isTradeDetailTblBuilder.get()
+ ? new StringColumn(COL_HEADER_TRADE_SHORT_ID)
+ : new StringColumn(COL_HEADER_TRADE_ID);
+
+ final Supplier createDateColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new Iso8601DateTimeColumn(COL_HEADER_DATE_TIME);
+
+ final Supplier marketColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_MARKET);
+
+ private final Function> toDetailedPriceColumn = (t) -> {
+ String colHeader = isFiatTrade.test(t)
+ ? format(COL_HEADER_DETAILED_PRICE, t.getOffer().getCounterCurrencyCode())
+ : format(COL_HEADER_DETAILED_PRICE_OF_ALTCOIN, t.getOffer().getBaseCurrencyCode());
+ return new StringColumn(colHeader, RIGHT);
+ };
+
+ final Supplier> priceColumn = () -> isTradeDetailTblBuilder.get()
+ ? toDetailedPriceColumn.apply(firstRow.get())
+ : new StringColumn(COL_HEADER_PRICE, RIGHT);
+
+ final Supplier> priceDeviationColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_DEVIATION, RIGHT);
+
+ final Supplier currencyColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_CURRENCY);
+
+ private final Function> toDetailedAmountColumn = (t) -> {
+ String headerCurrencyCode = t.getOffer().getBaseCurrencyCode();
+ String colHeader = format(COL_HEADER_DETAILED_AMOUNT, headerCurrencyCode);
+ AltcoinVolumeColumn.DISPLAY_MODE displayMode = headerCurrencyCode.equals("BSQ") ? BSQ_VOLUME : ALTCOIN_VOLUME;
+ return isFiatTrade.test(t)
+ ? new SatoshiColumn(colHeader)
+ : new AltcoinVolumeColumn(colHeader, displayMode);
+ };
+
+ // Can be fiat, btc or altcoin amount represented as longs. Placing the decimal
+ // in the displayed string representation is done in the Column implementation.
+ final Supplier> amountColumn = () -> isTradeDetailTblBuilder.get()
+ ? toDetailedAmountColumn.apply(firstRow.get())
+ : new BtcColumn(COL_HEADER_AMOUNT_IN_BTC);
+
+ final Supplier mixedAmountColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_AMOUNT, RIGHT);
+
+ final Supplier> minerTxFeeColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get()
+ ? new SatoshiColumn(COL_HEADER_TX_FEE)
+ : null;
+
+ final Supplier mixedTradeFeeColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new MixedTradeFeeColumn(COL_HEADER_TRADE_FEE);
+
+ final Supplier paymentMethodColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT);
+
+ final Supplier roleColumn = () -> {
+ if (isSwapTradeDetail.get())
+ return new StringColumn(COL_HEADER_BSQ_SWAP_TRADE_ROLE);
+ else
+ return isTradeDetailTblBuilder.get() || isOpenTradeTblBuilder.get() || isFailedTradeTblBuilder.get()
+ ? new StringColumn(COL_HEADER_TRADE_ROLE)
+ : null;
+ };
+
+ final Function> toSecurityDepositColumn = (name) -> isClosedTradeTblBuilder.get()
+ ? new SatoshiColumn(name)
+ : null;
+
+ final Supplier offerTypeColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_OFFER_TYPE);
+
+ final Supplier statusDescriptionColumn = () -> isTradeDetailTblBuilder.get()
+ ? null
+ : new StringColumn(COL_HEADER_STATUS);
+
+ private final Function> toBooleanColumn = BooleanColumn::new;
+
+ final Supplier> depositPublishedColumn = () -> {
+ if (isSwapTradeDetail.get())
+ return null;
+ else
+ return isTradeDetailTblBuilder.get()
+ ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_PUBLISHED)
+ : null;
+ };
+
+ final Supplier> depositConfirmedColumn = () -> {
+ if (isSwapTradeDetail.get())
+ return null;
+ else
+ return isTradeDetailTblBuilder.get()
+ ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_CONFIRMED)
+ : null;
+
+ };
+
+ final Supplier> payoutPublishedColumn = () -> {
+ if (isSwapTradeDetail.get())
+ return null;
+ else
+ return isTradeDetailTblBuilder.get()
+ ? toBooleanColumn.apply(COL_HEADER_TRADE_PAYOUT_PUBLISHED)
+ : null;
+ };
+
+ final Supplier> fundsWithdrawnColumn = () -> {
+ if (isSwapTradeDetail.get())
+ return null;
+ else
+ return isTradeDetailTblBuilder.get()
+ ? toBooleanColumn.apply(COL_HEADER_TRADE_WITHDRAWN)
+ : null;
+ };
+
+ final Supplier> bisqTradeDetailFeeColumn = () -> {
+ if (isTradeDetailTblBuilder.get()) {
+ TradeInfo t = firstRow.get();
+ String headerCurrencyCode = isTaker.test(t)
+ ? t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ"
+ : t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ";
+ String colHeader = isTaker.test(t)
+ ? format(COL_HEADER_TRADE_TAKER_FEE, headerCurrencyCode)
+ : format(COL_HEADER_TRADE_MAKER_FEE, headerCurrencyCode);
+ boolean isBsqSatoshis = headerCurrencyCode.equals("BSQ");
+ return new SatoshiColumn(colHeader, isBsqSatoshis);
+ } else {
+ return null;
+ }
+ };
+
+ final Function toPaymentCurrencyCode = (t) ->
+ isFiatTrade.test(t)
+ ? t.getOffer().getCounterCurrencyCode()
+ : t.getOffer().getBaseCurrencyCode();
+
+ final Supplier> paymentStartedMessageSentColumn = () -> {
+ if (isTradeDetailTblBuilder.get()) {
+ String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get());
+ String colHeader = format(COL_HEADER_TRADE_PAYMENT_SENT, headerCurrencyCode);
+ return new BooleanColumn(colHeader);
+ } else {
+ return null;
+ }
+ };
+
+ final Supplier> paymentReceivedMessageSentColumn = () -> {
+ if (isTradeDetailTblBuilder.get()) {
+ String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get());
+ String colHeader = format(COL_HEADER_TRADE_PAYMENT_RECEIVED, headerCurrencyCode);
+ return new BooleanColumn(colHeader);
+ } else {
+ return null;
+ }
+ };
+
+ final Supplier> tradeCostColumn = () -> {
+ if (isTradeDetailTblBuilder.get()) {
+ TradeInfo t = firstRow.get();
+ String headerCurrencyCode = t.getOffer().getCounterCurrencyCode();
+ String colHeader = format(COL_HEADER_TRADE_BUYER_COST, headerCurrencyCode);
+ return new StringColumn(colHeader, RIGHT);
+ } else {
+ return null;
+ }
+ };
+
+ final Supplier> bsqSwapTxIdColumn = () -> isSwapTradeDetail.get()
+ ? new StringColumn(COL_HEADER_TX_ID)
+ : null;
+
+ final Supplier> bsqSwapStatusColumn = () -> isSwapTradeDetail.get()
+ ? new StringColumn(COL_HEADER_STATUS)
+ : null;
+
+ final Supplier> numConfirmationsColumn = () -> isSwapTradeDetail.get()
+ ? new LongColumn(COL_HEADER_CONFIRMATIONS)
+ : null;
+
+ final Predicate showAltCoinBuyerAddress = (t) -> {
+ if (isFiatTrade.test(t)) {
+ return false;
+ } else {
+ ContractInfo contract = t.getContract();
+ boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
+ if (isTaker.test(t)) {
+ return !isBuyerMakerAndSellerTaker;
+ } else {
+ return isBuyerMakerAndSellerTaker;
+ }
+ }
+ };
+
+ @Nullable
+ final Supplier> altcoinReceiveAddressColumn = () -> {
+ if (isTradeDetailTblBuilder.get()) {
+ TradeInfo t = firstRow.get();
+ if (showAltCoinBuyerAddress.test(t)) {
+ String headerCurrencyCode = toPaymentCurrencyCode.apply(t);
+ String colHeader = format(COL_HEADER_TRADE_ALTCOIN_BUYER_ADDRESS, headerCurrencyCode);
+ return new StringColumn(colHeader);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ };
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/builder/TransactionTableBuilder.java b/java-examples/src/main/java/bisq/bots/table/builder/TransactionTableBuilder.java
new file mode 100644
index 0000000..e686960
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/builder/TransactionTableBuilder.java
@@ -0,0 +1,95 @@
+/*
+ * 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.bots.table.builder;
+
+import bisq.bots.table.Table;
+import bisq.bots.table.column.*;
+import bisq.proto.grpc.TxInfo;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+import static bisq.bots.table.builder.TableBuilderConstants.*;
+import static bisq.bots.table.builder.TableType.TRANSACTION_TBL;
+
+/**
+ * Builds a {@code bisq.bots.table.Table} from a {@code bisq.proto.grpc.TxInfo} object.
+ */
+class TransactionTableBuilder extends AbstractTableBuilder {
+
+ // Default columns not dynamically generated with tx info.
+ private final Column colTxId;
+ private final Column colIsConfirmed;
+ private final Column colInputSum;
+ private final Column colOutputSum;
+ private final Column colTxFee;
+ private final Column colTxSize;
+
+ TransactionTableBuilder(List> protos) {
+ super(TRANSACTION_TBL, protos);
+ this.colTxId = new StringColumn(COL_HEADER_TX_ID);
+ this.colIsConfirmed = new BooleanColumn(COL_HEADER_TX_IS_CONFIRMED);
+ this.colInputSum = new SatoshiColumn(COL_HEADER_TX_INPUT_SUM);
+ this.colOutputSum = new SatoshiColumn(COL_HEADER_TX_OUTPUT_SUM);
+ this.colTxFee = new SatoshiColumn(COL_HEADER_TX_FEE);
+ this.colTxSize = new LongColumn(COL_HEADER_TX_SIZE);
+ }
+
+ public Table build() {
+ // TODO Add 'gettransactions' api method & show multiple tx in the console.
+ // For now, a tx tbl is only one row.
+ TxInfo tx = (TxInfo) protos.get(0);
+
+ // Declare the columns derived from tx info.
+
+ @Nullable
+ Column colMemo = tx.getMemo().isEmpty()
+ ? null
+ : new StringColumn(COL_HEADER_TX_MEMO);
+
+ // Populate columns with tx info.
+
+ colTxId.addRow(tx.getTxId());
+ colIsConfirmed.addRow(!tx.getIsPending());
+ colInputSum.addRow(tx.getInputSum());
+ colOutputSum.addRow(tx.getOutputSum());
+ colTxFee.addRow(tx.getFee());
+ colTxSize.addRow((long) tx.getSize());
+ if (colMemo != null)
+ colMemo.addRow(tx.getMemo());
+
+ // Define and return the table instance with populated columns.
+
+ if (colMemo != null) {
+ return new Table(colTxId,
+ colIsConfirmed.asStringColumn(),
+ colInputSum.asStringColumn(),
+ colOutputSum.asStringColumn(),
+ colTxFee.asStringColumn(),
+ colTxSize.asStringColumn(),
+ colMemo);
+ } else {
+ return new Table(colTxId,
+ colIsConfirmed.asStringColumn(),
+ colInputSum.asStringColumn(),
+ colOutputSum.asStringColumn(),
+ colTxFee.asStringColumn(),
+ colTxSize.asStringColumn());
+ }
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/AbstractColumn.java b/java-examples/src/main/java/bisq/bots/table/column/AbstractColumn.java
new file mode 100644
index 0000000..4a71588
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/AbstractColumn.java
@@ -0,0 +1,86 @@
+/*
+ * 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.bots.table.column;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+import static com.google.common.base.Strings.padEnd;
+import static com.google.common.base.Strings.padStart;
+
+/**
+ * Partial implementation of the {@link Column} interface.
+ */
+abstract class AbstractColumn, T> implements Column {
+
+ // We create an encapsulated StringColumn up front to populate with formatted
+ // strings in each this.addRow(Long value) call. But we will not know how
+ // to justify the cached, formatted string until the column is fully populated.
+ protected final StringColumn stringColumn;
+
+ // The name field is not final, so it can be re-set for column alignment.
+ protected String name;
+ protected final JUSTIFICATION justification;
+ // The max width is not known until after column is fully populated.
+ protected int maxWidth;
+
+ public AbstractColumn(String name, JUSTIFICATION justification) {
+ this.name = name;
+ this.justification = justification;
+ this.stringColumn = this instanceof StringColumn ? null : new StringColumn(name, justification);
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public int getWidth() {
+ return maxWidth;
+ }
+
+ @Override
+ public JUSTIFICATION getJustification() {
+ return this.justification;
+ }
+
+ @Override
+ public Column justify() {
+ if (this instanceof StringColumn && this.justification.equals(RIGHT))
+ return this.justify();
+ else
+ return this; // no-op
+ }
+
+ protected final String toJustifiedString(String s) {
+ switch (justification) {
+ case LEFT:
+ return padEnd(s, maxWidth, ' ');
+ case RIGHT:
+ return padStart(s, maxWidth, ' ');
+ case NONE:
+ default:
+ return s;
+ }
+ }
+}
+
diff --git a/java-examples/src/main/java/bisq/bots/table/column/AltcoinVolumeColumn.java b/java-examples/src/main/java/bisq/bots/table/column/AltcoinVolumeColumn.java
new file mode 100644
index 0000000..8e38e69
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/AltcoinVolumeColumn.java
@@ -0,0 +1,88 @@
+/*
+ * 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.bots.table.column;
+
+import java.math.BigDecimal;
+import java.util.function.BiFunction;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying altcoin volume with appropriate precision.
+ */
+public class AltcoinVolumeColumn extends LongColumn {
+
+ public enum DISPLAY_MODE {
+ ALTCOIN_VOLUME,
+ BSQ_VOLUME,
+ }
+
+ private final DISPLAY_MODE displayMode;
+
+ // The default AltcoinVolumeColumn JUSTIFICATION is RIGHT.
+ public AltcoinVolumeColumn(String name, DISPLAY_MODE displayMode) {
+ this(name, RIGHT, displayMode);
+ }
+
+ public AltcoinVolumeColumn(String name,
+ JUSTIFICATION justification,
+ DISPLAY_MODE displayMode) {
+ super(name, justification);
+ this.displayMode = displayMode;
+ }
+
+ @Override
+ public void addRow(Long value) {
+ rows.add(value);
+
+ String s = toFormattedString.apply(value, displayMode);
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return toFormattedString.apply(getRow(rowIndex), displayMode);
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ // We cached the formatted altcoin value strings, but we did
+ // not know how much padding each string needed until now.
+ IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
+ String unjustified = stringColumn.getRow(rowIndex);
+ String justified = stringColumn.toJustifiedString(unjustified);
+ stringColumn.updateRow(rowIndex, justified);
+ });
+ return this.stringColumn;
+ }
+
+ private final BiFunction toFormattedString = (value, displayMode) -> {
+ switch (displayMode) {
+ case ALTCOIN_VOLUME:
+ return value > 0 ? new BigDecimal(value).movePointLeft(8).toString() : "";
+ case BSQ_VOLUME:
+ return value > 0 ? new BigDecimal(value).movePointLeft(2).toString() : "";
+ default:
+ throw new IllegalStateException("invalid display mode: " + displayMode);
+ }
+ };
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/BooleanColumn.java b/java-examples/src/main/java/bisq/bots/table/column/BooleanColumn.java
new file mode 100644
index 0000000..37d8b14
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/BooleanColumn.java
@@ -0,0 +1,131 @@
+/*
+ * 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.bots.table.column;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.LEFT;
+
+/**
+ * For displaying boolean values as YES, NO, or user's choice for 'true' and 'false'.
+ */
+public class BooleanColumn extends AbstractColumn {
+
+ private static final String DEFAULT_TRUE_AS_STRING = "YES";
+ private static final String DEFAULT_FALSE_AS_STRING = "NO";
+
+ private final List rows = new ArrayList<>();
+
+ private final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
+
+ private final String trueAsString;
+ private final String falseAsString;
+
+ // The default BooleanColumn JUSTIFICATION is LEFT.
+ // The default BooleanColumn True AsString value is YES.
+ // The default BooleanColumn False AsString value is NO.
+ public BooleanColumn(String name) {
+ this(name, LEFT, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING);
+ }
+
+ // Use this constructor to override default LEFT justification.
+ @SuppressWarnings("unused")
+ public BooleanColumn(String name, JUSTIFICATION justification) {
+ this(name, justification, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING);
+ }
+
+ // Use this constructor to override default true/false as string defaults.
+ public BooleanColumn(String name, String trueAsString, String falseAsString) {
+ this(name, LEFT, trueAsString, falseAsString);
+ }
+
+ // Use this constructor to override default LEFT justification.
+ public BooleanColumn(String name,
+ JUSTIFICATION justification,
+ String trueAsString,
+ String falseAsString) {
+ super(name, justification);
+ this.trueAsString = trueAsString;
+ this.falseAsString = falseAsString;
+ this.maxWidth = name.length();
+ }
+
+ @Override
+ public void addRow(Boolean value) {
+ rows.add(value);
+
+ // We do not know how much padding each StringColumn value needs until it has all the values.
+ String s = asString(value);
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public List getRows() {
+ return rows;
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return rows.isEmpty();
+ }
+
+ @Override
+ public Boolean getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ @Override
+ public void updateRow(int rowIndex, Boolean newValue) {
+ rows.set(rowIndex, newValue);
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return getRow(rowIndex)
+ ? trueAsString
+ : falseAsString;
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ // We cached the formatted satoshi strings, but we did
+ // not know how much padding each string needed until now.
+ IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
+ String unjustified = stringColumn.getRow(rowIndex);
+ String justified = stringColumn.toJustifiedString(unjustified);
+ stringColumn.updateRow(rowIndex, justified);
+ });
+ return stringColumn;
+ }
+
+ private String asString(boolean value) {
+ return value ? trueAsString : falseAsString;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/BtcColumn.java b/java-examples/src/main/java/bisq/bots/table/column/BtcColumn.java
new file mode 100644
index 0000000..a2e037f
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/BtcColumn.java
@@ -0,0 +1,48 @@
+package bisq.bots.table.column;
+
+import java.util.stream.IntStream;
+
+import static bisq.bots.CurrencyFormat.formatBtc;
+import static com.google.common.base.Strings.padEnd;
+import static java.util.Comparator.comparingInt;
+
+public class BtcColumn extends SatoshiColumn {
+
+ public BtcColumn(String name) {
+ super(name);
+ }
+
+ @Override
+ public void addRow(Long value) {
+ rows.add(value);
+
+ String s = formatBtc(value);
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return formatBtc(getRow(rowIndex));
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ // We cached the formatted satoshi strings, but we did
+ // not know how much zero padding each string needed until now.
+ int maxColumnValueWidth = stringColumn.getRows().stream()
+ .max(comparingInt(String::length))
+ .get()
+ .length();
+ IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
+ String btcString = stringColumn.getRow(rowIndex);
+ if (btcString.length() < maxColumnValueWidth) {
+ String paddedBtcString = padEnd(btcString, maxColumnValueWidth, '0');
+ stringColumn.updateRow(rowIndex, paddedBtcString);
+ }
+ });
+ return stringColumn.justify();
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/Column.java b/java-examples/src/main/java/bisq/bots/table/column/Column.java
new file mode 100644
index 0000000..08cacb6
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/Column.java
@@ -0,0 +1,122 @@
+/*
+ * 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.bots.table.column;
+
+import java.util.List;
+
+public interface Column {
+
+ enum JUSTIFICATION {
+ LEFT,
+ RIGHT,
+ NONE
+ }
+
+ /**
+ * Returns the column's name.
+ *
+ * @return name as String
+ */
+ String getName();
+
+ /**
+ * Sets the column name.
+ *
+ * @param name of the column
+ */
+ void setName(String name);
+
+ /**
+ * Add column value.
+ *
+ * @param value added to column's data (row)
+ */
+ void addRow(T value);
+
+ /**
+ * Returns the column data.
+ *
+ * @return rows as List
+ */
+ List getRows();
+
+ /**
+ * Returns the maximum width of the column name, or longest,
+ * formatted string value -- whichever is greater.
+ *
+ * @return width of the populated column as int
+ */
+ int getWidth();
+
+ /**
+ * Returns the number of rows in the column.
+ *
+ * @return number of rows in the column as int.
+ */
+ int rowCount();
+
+ /**
+ * Returns true if the column has no data.
+ *
+ * @return true if empty, false if not
+ */
+ boolean isEmpty();
+
+ /**
+ * Returns the column value (data) at given row index.
+ *
+ * @return value object
+ */
+ T getRow(int rowIndex);
+
+ /**
+ * Update an existing value at the given row index to a new value.
+ *
+ * @param rowIndex row index of value to be updated
+ * @param newValue new value
+ */
+ void updateRow(int rowIndex, T newValue);
+
+ /**
+ * Returns the row value as a formatted String.
+ *
+ * @return a row value as formatted String
+ */
+ String getRowAsFormattedString(int rowIndex);
+
+ /**
+ * Return the column with all of its data as a StringColumn with all of its
+ * formatted string data.
+ *
+ * @return StringColumn
+ */
+ StringColumn asStringColumn();
+
+ /**
+ * Convenience for justifying populated StringColumns before being displayed.
+ * Is only useful for StringColumn instances.
+ */
+ Column justify();
+
+ /**
+ * Returns JUSTIFICATION value (RIGHT|LEFT|NONE) for the column.
+ *
+ * @return column JUSTIFICATION
+ */
+ JUSTIFICATION getJustification();
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/DoubleColumn.java b/java-examples/src/main/java/bisq/bots/table/column/DoubleColumn.java
new file mode 100644
index 0000000..1040e5b
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/DoubleColumn.java
@@ -0,0 +1,93 @@
+/*
+ * 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.bots.table.column;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying Double values.
+ */
+public class DoubleColumn extends NumberColumn {
+
+ protected final List rows = new ArrayList<>();
+
+ protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
+
+ // The default DoubleColumn JUSTIFICATION is RIGHT.
+ public DoubleColumn(String name) {
+ this(name, RIGHT);
+ }
+
+ public DoubleColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ this.maxWidth = name.length();
+ }
+
+ @Override
+ public void addRow(Double value) {
+ rows.add(value);
+
+ String s = String.valueOf(value);
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public List getRows() {
+ return rows;
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return rows.isEmpty();
+ }
+
+ @Override
+ public Double getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ @Override
+ public void updateRow(int rowIndex, Double newValue) {
+ rows.set(rowIndex, newValue);
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ String s = String.valueOf(getRow(rowIndex));
+ return toJustifiedString(s);
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ IntStream.range(0, rows.size()).forEachOrdered(rowIndex ->
+ stringColumn.addRow(getRowAsFormattedString(rowIndex)));
+
+ return stringColumn;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/FiatColumn.java b/java-examples/src/main/java/bisq/bots/table/column/FiatColumn.java
new file mode 100644
index 0000000..ffb314a
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/FiatColumn.java
@@ -0,0 +1,83 @@
+/*
+ * 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.bots.table.column;
+
+import java.util.stream.IntStream;
+
+import static bisq.bots.CurrencyFormat.formatFiatVolume;
+import static bisq.bots.CurrencyFormat.formatPrice;
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying fiat volume or price with appropriate precision.
+ */
+public class FiatColumn extends LongColumn {
+
+ public enum DISPLAY_MODE {
+ FIAT_PRICE,
+ FIAT_VOLUME
+ }
+
+ private final DISPLAY_MODE displayMode;
+
+ // The default FiatColumn JUSTIFICATION is RIGHT.
+ // The default FiatColumn DISPLAY_MODE is PRICE.
+ public FiatColumn(String name) {
+ this(name, RIGHT, DISPLAY_MODE.FIAT_PRICE);
+ }
+
+ public FiatColumn(String name, DISPLAY_MODE displayMode) {
+ this(name, RIGHT, displayMode);
+ }
+
+ public FiatColumn(String name,
+ JUSTIFICATION justification,
+ DISPLAY_MODE displayMode) {
+ super(name, justification);
+ this.displayMode = displayMode;
+ }
+
+ @Override
+ public void addRow(Long value) {
+ rows.add(value);
+
+ String s = displayMode.equals(DISPLAY_MODE.FIAT_PRICE) ? formatPrice(value) : formatFiatVolume(value);
+
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return getRow(rowIndex).toString();
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ // We cached the formatted fiat price strings, but we did
+ // not know how much padding each string needed until now.
+ IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
+ String unjustified = stringColumn.getRow(rowIndex);
+ String justified = stringColumn.toJustifiedString(unjustified);
+ stringColumn.updateRow(rowIndex, justified);
+ });
+ return this.stringColumn;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/IntegerColumn.java b/java-examples/src/main/java/bisq/bots/table/column/IntegerColumn.java
new file mode 100644
index 0000000..9b8da9a
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/IntegerColumn.java
@@ -0,0 +1,93 @@
+/*
+ * 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.bots.table.column;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying Integer values.
+ */
+public class IntegerColumn extends NumberColumn {
+
+ protected final List rows = new ArrayList<>();
+
+ protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
+
+ // The default IntegerColumn JUSTIFICATION is RIGHT.
+ public IntegerColumn(String name) {
+ this(name, RIGHT);
+ }
+
+ public IntegerColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ this.maxWidth = name.length();
+ }
+
+ @Override
+ public void addRow(Integer value) {
+ rows.add(value);
+
+ String s = String.valueOf(value);
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public List getRows() {
+ return rows;
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return rows.isEmpty();
+ }
+
+ @Override
+ public Integer getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ @Override
+ public void updateRow(int rowIndex, Integer newValue) {
+ rows.set(rowIndex, newValue);
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ String s = String.valueOf(getRow(rowIndex));
+ return toJustifiedString(s);
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ IntStream.range(0, rows.size()).forEachOrdered(rowIndex ->
+ stringColumn.addRow(getRowAsFormattedString(rowIndex)));
+
+ return stringColumn;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/Iso8601DateTimeColumn.java b/java-examples/src/main/java/bisq/bots/table/column/Iso8601DateTimeColumn.java
new file mode 100644
index 0000000..7e44e6a
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/Iso8601DateTimeColumn.java
@@ -0,0 +1,63 @@
+/*
+ * 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.bots.table.column;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.LEFT;
+import static com.google.common.base.Strings.padEnd;
+import static com.google.common.base.Strings.padStart;
+import static java.lang.System.currentTimeMillis;
+import static java.util.TimeZone.getTimeZone;
+
+/**
+ * For displaying (long) timestamp values as ISO-8601 dates in UTC time zone.
+ */
+public class Iso8601DateTimeColumn extends LongColumn {
+
+ protected final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+ // The default Iso8601DateTimeColumn JUSTIFICATION is LEFT.
+ public Iso8601DateTimeColumn(String name) {
+ this(name, LEFT);
+ }
+
+ public Iso8601DateTimeColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ iso8601DateFormat.setTimeZone(getTimeZone("UTC"));
+ this.maxWidth = Math.max(name.length(), String.valueOf(currentTimeMillis()).length());
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ long time = getRow(rowIndex);
+ return justification.equals(LEFT)
+ ? padEnd(iso8601DateFormat.format(new Date(time)), maxWidth, ' ')
+ : padStart(iso8601DateFormat.format(new Date(time)), maxWidth, ' ');
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ IntStream.range(0, rows.size()).forEachOrdered(rowIndex ->
+ stringColumn.addRow(getRowAsFormattedString(rowIndex)));
+
+ return stringColumn;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/LongColumn.java b/java-examples/src/main/java/bisq/bots/table/column/LongColumn.java
new file mode 100644
index 0000000..0b36f2d
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/LongColumn.java
@@ -0,0 +1,93 @@
+/*
+ * 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.bots.table.column;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying Long values.
+ */
+public class LongColumn extends NumberColumn {
+
+ protected final List rows = new ArrayList<>();
+
+ protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
+
+ // The default LongColumn JUSTIFICATION is RIGHT.
+ public LongColumn(String name) {
+ this(name, RIGHT);
+ }
+
+ public LongColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ this.maxWidth = name.length();
+ }
+
+ @Override
+ public void addRow(Long value) {
+ rows.add(value);
+
+ String s = String.valueOf(value);
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public List getRows() {
+ return rows;
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return rows.isEmpty();
+ }
+
+ @Override
+ public Long getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ @Override
+ public void updateRow(int rowIndex, Long newValue) {
+ rows.set(rowIndex, newValue);
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ String s = String.valueOf(getRow(rowIndex));
+ return toJustifiedString(s);
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ IntStream.range(0, rows.size()).forEachOrdered(rowIndex ->
+ stringColumn.addRow(getRowAsFormattedString(rowIndex)));
+
+ return stringColumn;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/MixedTradeFeeColumn.java b/java-examples/src/main/java/bisq/bots/table/column/MixedTradeFeeColumn.java
new file mode 100644
index 0000000..1481546
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/MixedTradeFeeColumn.java
@@ -0,0 +1,59 @@
+/*
+ * 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.bots.table.column;
+
+import static bisq.bots.CurrencyFormat.formatBsq;
+import static bisq.bots.CurrencyFormat.formatSatoshis;
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying a mix of BSQ and BTC trade fees with appropriate precision.
+ */
+public class MixedTradeFeeColumn extends LongColumn {
+
+ public MixedTradeFeeColumn(String name) {
+ super(name, RIGHT);
+ }
+
+ @Override
+ public void addRow(Long value) {
+ throw new UnsupportedOperationException("use public void addRow(Long value, boolean isBsq) instead");
+ }
+
+ public void addRow(Long value, boolean isBsq) {
+ rows.add(value);
+
+ String s = isBsq
+ ? formatBsq(value) + " BSQ"
+ : formatSatoshis(value) + " BTC";
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return getRow(rowIndex).toString();
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ return stringColumn.justify();
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/NumberColumn.java b/java-examples/src/main/java/bisq/bots/table/column/NumberColumn.java
new file mode 100644
index 0000000..acf52a5
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/NumberColumn.java
@@ -0,0 +1,32 @@
+/*
+ * 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.bots.table.column;
+
+/**
+ * Abstract superclass for numeric Columns.
+ *
+ * @param the subclass column's type (LongColumn, IntegerColumn, ...)
+ * @param the subclass column's numeric Java type (Long, Integer, ...)
+ */
+abstract class NumberColumn,
+ T extends Number> extends AbstractColumn implements Column {
+
+ public NumberColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/SatoshiColumn.java b/java-examples/src/main/java/bisq/bots/table/column/SatoshiColumn.java
new file mode 100644
index 0000000..7ab5dd6
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/SatoshiColumn.java
@@ -0,0 +1,72 @@
+/*
+ * 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.bots.table.column;
+
+import static bisq.bots.CurrencyFormat.formatBsq;
+import static bisq.bots.CurrencyFormat.formatSatoshis;
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying BTC or BSQ satoshi values with appropriate precision.
+ */
+public class SatoshiColumn extends LongColumn {
+
+ protected final boolean isBsqSatoshis;
+
+ // The default SatoshiColumn JUSTIFICATION is RIGHT.
+ public SatoshiColumn(String name) {
+ this(name, RIGHT, false);
+ }
+
+ public SatoshiColumn(String name, boolean isBsqSatoshis) {
+ this(name, RIGHT, isBsqSatoshis);
+ }
+
+ public SatoshiColumn(String name, JUSTIFICATION justification) {
+ this(name, justification, false);
+ }
+
+ public SatoshiColumn(String name, JUSTIFICATION justification, boolean isBsqSatoshis) {
+ super(name, justification);
+ this.isBsqSatoshis = isBsqSatoshis;
+ }
+
+ @Override
+ public void addRow(Long value) {
+ rows.add(value);
+
+ // We do not know how much padding each StringColumn value needs until it has all the values.
+ String s = isBsqSatoshis ? formatBsq(value) : formatSatoshis(value);
+ stringColumn.addRow(s);
+
+ if (isNewMaxWidth.test(s))
+ maxWidth = s.length();
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return isBsqSatoshis
+ ? formatBsq(getRow(rowIndex))
+ : formatSatoshis(getRow(rowIndex));
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ return stringColumn.justify();
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/StringColumn.java b/java-examples/src/main/java/bisq/bots/table/column/StringColumn.java
new file mode 100644
index 0000000..9197208
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/StringColumn.java
@@ -0,0 +1,102 @@
+/*
+ * 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.bots.table.column;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static bisq.bots.table.column.Column.JUSTIFICATION.LEFT;
+import static bisq.bots.table.column.Column.JUSTIFICATION.RIGHT;
+
+/**
+ * For displaying justified string values.
+ */
+public class StringColumn extends AbstractColumn {
+
+ private final List rows = new ArrayList<>();
+
+ private final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth;
+
+ // The default StringColumn JUSTIFICATION is LEFT.
+ public StringColumn(String name) {
+ this(name, LEFT);
+ }
+
+ // Use this constructor to override default LEFT justification.
+ public StringColumn(String name, JUSTIFICATION justification) {
+ super(name, justification);
+ this.maxWidth = name.length();
+ }
+
+ @Override
+ public void addRow(String value) {
+ rows.add(value);
+ if (isNewMaxWidth.test(value))
+ maxWidth = value.length();
+ }
+
+ @Override
+ public List getRows() {
+ return rows;
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return rows.isEmpty();
+ }
+
+ @Override
+ public String getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ @Override
+ public void updateRow(int rowIndex, String newValue) {
+ rows.set(rowIndex, newValue);
+ }
+
+ @Override
+ public String getRowAsFormattedString(int rowIndex) {
+ return getRow(rowIndex);
+ }
+
+ @Override
+ public StringColumn asStringColumn() {
+ return this;
+ }
+
+ @Override
+ public StringColumn justify() {
+ if (justification.equals(RIGHT)) {
+ IntStream.range(0, getRows().size()).forEach(rowIndex -> {
+ String unjustified = getRow(rowIndex);
+ String justified = toJustifiedString(unjustified);
+ updateRow(rowIndex, justified);
+ });
+ }
+ return this;
+ }
+}
diff --git a/java-examples/src/main/java/bisq/bots/table/column/ZippedStringColumns.java b/java-examples/src/main/java/bisq/bots/table/column/ZippedStringColumns.java
new file mode 100644
index 0000000..f57a78b
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/table/column/ZippedStringColumns.java
@@ -0,0 +1,124 @@
+/*
+ * 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.bots.table.column;
+
+import bisq.bots.table.column.Column.JUSTIFICATION;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * For zipping multiple StringColumns into a single StringColumn.
+ * Useful for displaying amount and volume range values.
+ */
+public class ZippedStringColumns {
+
+ public enum DUPLICATION_MODE {
+ EXCLUDE_DUPLICATES,
+ INCLUDE_DUPLICATES
+ }
+
+ private final String name;
+ private final JUSTIFICATION justification;
+ private final String delimiter;
+ private final StringColumn[] columns;
+
+ public ZippedStringColumns(String name,
+ JUSTIFICATION justification,
+ String delimiter,
+ StringColumn... columns) {
+ this.name = name;
+ this.justification = justification;
+ this.delimiter = delimiter;
+ this.columns = columns;
+ validateColumnData();
+ }
+
+ public StringColumn asStringColumn(DUPLICATION_MODE duplicationMode) {
+ StringColumn stringColumn = new StringColumn(name, justification);
+
+ buildRows(stringColumn, duplicationMode);
+
+ // Re-set the column name field to its justified value, in case any of the column
+ // values are longer than the name passed to this constructor.
+ stringColumn.setName(stringColumn.toJustifiedString(name));
+
+ return stringColumn;
+ }
+
+ private void buildRows(StringColumn stringColumn, DUPLICATION_MODE duplicationMode) {
+ // Populate the StringColumn with unjustified zipped values; we cannot justify
+ // the zipped values until stringColumn knows its final maxWidth.
+ IntStream.range(0, columns[0].getRows().size()).forEach(rowIndex -> {
+ String row = buildRow(rowIndex, duplicationMode);
+ stringColumn.addRow(row);
+ });
+
+ formatRows(stringColumn);
+ }
+
+ private String buildRow(int rowIndex, DUPLICATION_MODE duplicationMode) {
+ StringBuilder rowBuilder = new StringBuilder();
+ @Nullable
+ List processedValues = duplicationMode.equals(DUPLICATION_MODE.EXCLUDE_DUPLICATES)
+ ? new ArrayList<>()
+ : null;
+ IntStream.range(0, columns.length).forEachOrdered(colIndex -> {
+ // For each column @ rowIndex ...
+ var value = columns[colIndex].getRows().get(rowIndex);
+ if (duplicationMode.equals(DUPLICATION_MODE.INCLUDE_DUPLICATES)) {
+ if (rowBuilder.length() > 0)
+ rowBuilder.append(delimiter);
+
+ rowBuilder.append(value);
+ } else if (!processedValues.contains(value)) {
+ if (rowBuilder.length() > 0)
+ rowBuilder.append(delimiter);
+
+ rowBuilder.append(value);
+ processedValues.add(value);
+ }
+ });
+ return rowBuilder.toString();
+ }
+
+ private void formatRows(StringColumn stringColumn) {
+ // Now we can justify the zipped string values in the new StringColumn.
+ IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> {
+ String unjustified = stringColumn.getRow(rowIndex);
+ String justified = stringColumn.toJustifiedString(unjustified);
+ stringColumn.updateRow(rowIndex, justified);
+ });
+ }
+
+ private void validateColumnData() {
+ if (columns.length == 0)
+ throw new IllegalStateException("cannot zip columns because they do not have any data");
+
+ StringColumn firstColumn = columns[0];
+ if (firstColumn.getRows().isEmpty())
+ throw new IllegalStateException("1st column has no data");
+
+ IntStream.range(1, columns.length).forEach(colIndex -> {
+ if (columns[colIndex].getRows().size() != firstColumn.getRows().size())
+ throw new IllegalStateException("columns do not have same number of rows");
+ });
+ }
+}