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"); + }); + } +}