From 5522d0c53e96312a75dbb88da72bec8480c97bc8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 14 Dec 2020 15:01:03 -0300 Subject: [PATCH] Add new api method gettransaction This change was prompted by the recent changes in the main branch to allow a tx memo field to be set from the UI and API. This and the prior PR address the API's need to be able to fetch a tx (with a memo). The API can now get a completed trade's withdrawal txid and pass it as a gettransaction parameter. See previous PR "Append nullable withdrawalTxId field to Trade". https://github.com/bisq-network/bisq/pull/4937 A summary of changes by file: grpc.proto - Added withdrawalTxId field to existing TradeInfo proto & wrapper. - Reordered fields in TradeInfo proto. - Added new fields to be displayed by TxInfo proto in CLI. - Fixed typo: unsetTxFeeRatePreference -> UnsetTxFeeRatePreference. - Added new GetTransaction rpc. GrpcWalletsService - Added new getTransaction gRPC boilerplate. CoreWalletsService - Added new getTransaction implementation. TxInfo - Added the new fields for displaying a tx summary from CLI. This is not intended to be more than a brief summary; a block explorer or bitcoin-core client should be used to see the complete definition. TradeInfo - Added the new withdrawalTxId field defined in grpc.proto. CliMain - Added new 'case gettransaction'. TransactionFormat - Formats a TxInfo sent from the server to CLI. ColumnHeaderConstants - Added console headers used by TransactionFormat. TradeFormat - Displays a completed trade's WithdrawalTxId if present. Apitest - Adjusted affected tests: assert tx memo is persisted and test gettransaction. --- .../java/bisq/apitest/method/MethodTest.java | 6 + .../payment/CreatePaymentAccountTest.java | 7 +- .../method/trade/TakeSellBTCOfferTest.java | 23 +++- .../apitest/method/wallet/BtcWalletTest.java | 18 ++- .../java/bisq/apitest/scenario/TradeTest.java | 1 + cli/src/main/java/bisq/cli/CliMain.java | 27 ++++- .../java/bisq/cli/ColumnHeaderConstants.java | 10 ++ cli/src/main/java/bisq/cli/TradeFormat.java | 33 ++--- .../main/java/bisq/cli/TransactionFormat.java | 59 +++++++++ core/src/main/java/bisq/core/api/CoreApi.java | 4 + .../bisq/core/api/CoreWalletsService.java | 20 +++ .../java/bisq/core/api/model/TradeInfo.java | 11 ++ .../main/java/bisq/core/api/model/TxInfo.java | 114 +++++++++++++++--- .../bisq/daemon/grpc/GrpcWalletsService.java | 19 +++ proto/src/main/proto/grpc.proto | 50 +++++--- 15 files changed, 341 insertions(+), 61 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/TransactionFormat.java diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 95918189424..791e22ef6c2 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -39,6 +39,7 @@ import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetPaymentMethodsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetTransactionRequest; import bisq.proto.grpc.GetTxFeeRateRequest; import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.KeepFundsRequest; @@ -432,6 +433,11 @@ protected final TxFeeRateInfo unsetTxFeeRate(BisqAppConfig bisqAppConfig) { grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo()); } + protected final TxInfo getTransaction(BisqAppConfig bisqAppConfig, String txId) { + var req = GetTransactionRequest.newBuilder().setTxId(txId).build(); + return grpcStubs(bisqAppConfig).walletsService.getTransaction(req).getTxInfo(); + } + // Static conveniences for test methods and test case fixture setups. protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) { diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 9795eec28c4..ac66cc7993e 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -746,7 +746,12 @@ public void testCreateTransferwiseAccount(TestInfo testInfo) { String jsonString = getCompletedFormAsJsonString(); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString); verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); - verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + // As per commit 88f26f93241af698ae689bf081205d0f9dc929fa + // Do not autofill all currencies by default but keep all unselected. + // verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount); + // TODO uncomment after master/merge + // assertEquals(0, paymentAccount.getTradeCurrencies().size()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 69445657416..ce5bceff7a1 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -33,6 +32,7 @@ import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.TransactionFormat.format; import static bisq.core.trade.Trade.Phase.*; import static bisq.core.trade.Trade.State.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -42,7 +42,7 @@ import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OpenOffer.State.AVAILABLE; -@Disabled +// @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeSellBTCOfferTest extends AbstractTradeTest { @@ -52,6 +52,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { // Maker and Taker fees are in BTC. private static final String TRADE_FEE_CURRENCY_CODE = "btc"; + private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; + @Test @Order(1) public void testTakeAlicesSellOffer(final TestInfo testInfo) { @@ -147,7 +149,7 @@ public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade); String toAddress = bitcoinCli.getNewBtcAddress(); - withdrawFunds(bobdaemon, tradeId, toAddress, "to whom it may concern"); + withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO); genBtcBlocksThenWait(1, 2250); @@ -162,4 +164,19 @@ public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { testName(testInfo), formatSatoshis(currentBalance.getAvailableBalance())); } + + @Test + @Order(5) + public void testGetTradeWithdrawalTx(final TestInfo testInfo) { + var trade = getTrade(bobdaemon, tradeId); + var withdrawalTxId = trade.getWithdrawalTxId(); + assertNotNull(withdrawalTxId); + + var txInfo = getTransaction(bobdaemon, withdrawalTxId); + assertEquals(WITHDRAWAL_TX_MEMO, txInfo.getMemo()); + + log.debug("{} Trade withdrawal Tx:\n{}", + testName(testInfo), + format(txInfo)); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java index 7af31dcb3fc..90c46a3c814 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -1,6 +1,7 @@ package bisq.apitest.method.wallet; import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.TxInfo; import lombok.extern.slf4j.Slf4j; @@ -20,6 +21,7 @@ import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @@ -32,6 +34,8 @@ @TestMethodOrder(OrderAnnotation.class) public class BtcWalletTest extends MethodTest { + private static final String TX_MEMO = "tx memo"; + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets // are initialized with 10 BTC during the scaffolding setup. private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = @@ -99,15 +103,23 @@ public void testAliceSendBTCToBob(TestInfo testInfo) { String bobsBtcAddress = getUnusedBtcAddress(bobdaemon); log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress); - sendBtc(alicedaemon, + TxInfo txInfo = sendBtc(alicedaemon, bobsBtcAddress, "5.50", "100", - "to whom it may concern"); + TX_MEMO); + assertTrue(txInfo.getIsPending()); + + // Note that the memo is not set on the tx yet. + assertTrue(txInfo.getMemo().isEmpty()); genBtcBlocksThenWait(1, 3000); - BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon); + // Fetch the tx and check for confirmation and memo. + txInfo = getTransaction(alicedaemon, txInfo.getTxId()); + assertFalse(txInfo.getIsPending()); + assertEquals(TX_MEMO, txInfo.getMemo()); + BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon); log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances)); diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java index 4c07452abc6..410b72ca6cb 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -60,5 +60,6 @@ public void testTakeSellBTCOffer(final TestInfo testInfo) { test.testBobsConfirmPaymentStarted(testInfo); test.testAlicesConfirmPaymentReceived(testInfo); test.testBobsBtcWithdrawalToExternalAddress(testInfo); + test.testGetTradeWithdrawalTx(testInfo); } } diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index b9cb4ede0fd..4853e3a76b3 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -31,6 +31,7 @@ import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetPaymentMethodsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetTransactionRequest; import bisq.proto.grpc.GetTxFeeRateRequest; import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.GetVersionRequest; @@ -116,6 +117,7 @@ private enum Method { gettxfeerate, settxfeerate, unsettxfeerate, + gettransaction, lockwallet, unlockwallet, removewalletpassword, @@ -275,7 +277,10 @@ public static void run(String[] args) { .build(); var reply = walletsService.sendBsq(request); TxInfo txInfo = reply.getTxInfo(); - out.printf("%s bsq sent to %s in tx %s%n", amount, address, txInfo.getId()); + out.printf("%s bsq sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); return; } case sendbtc: { @@ -305,7 +310,10 @@ public static void run(String[] args) { .build(); var reply = walletsService.sendBtc(request); TxInfo txInfo = reply.getTxInfo(); - out.printf("%s btc sent to %s in tx %s%n", amount, address, txInfo.getId()); + out.printf("%s btc sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); return; } case gettxfeerate: { @@ -332,6 +340,18 @@ public static void run(String[] args) { out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo())); return; } + case gettransaction: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("no tx id specified"); + + var txId = nonOptionArgs.get(1); + var request = GetTransactionRequest.newBuilder() + .setTxId(txId) + .build(); + var reply = walletsService.getTransaction(request); + out.println(TransactionFormat.format(reply.getTxInfo())); + return; + } case createoffer: { if (nonOptionArgs.size() < 9) throw new IllegalArgumentException("incorrect parameter count," @@ -441,7 +461,7 @@ public static void run(String[] args) { return; } case gettrade: { - // TODO make short-id a valid argument + // TODO make short-id a valid argument? if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("incorrect parameter count, " + " expecting trade id [,showcontract = true|false]"); @@ -720,6 +740,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "gettxfeerate", "", "Get current tx fee rate in sats/byte"); stream.format(rowFormat, "settxfeerate", "satoshis (per byte)", "Set custom tx fee rate in sats/byte"); stream.format(rowFormat, "unsettxfeerate", "", "Unset custom tx fee rate"); + stream.format(rowFormat, "gettransaction", "transaction id", "Get transaction with id"); stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer"); stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), security deposit (%) \\", ""); diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java index 59b6230a2eb..e81e407d7b9 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -59,6 +59,16 @@ class ColumnHeaderConstants { static final String COL_HEADER_TRADE_SHORT_ID = "ID"; static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)"; static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)"; + static final String COL_HEADER_TRADE_WITHDRAWAL_TX_ID = "Withdrawal TX ID"; + + 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 = padEnd("%-3s(min - max)", 15, ' '); static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); } diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java index 2a28c1dccc4..0c4d4d29498 100644 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -58,6 +58,7 @@ public static String format(TradeInfo tradeInfo) { + COL_HEADER_TRADE_FIAT_RECEIVED + COL_HEADER_DELIMITER + COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER + COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER + + (tradeInfo.getIsWithdrawn() ? COL_HEADER_TRADE_WITHDRAWAL_TX_ID + COL_HEADER_DELIMITER : "") + "%n"; String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode(); @@ -66,18 +67,20 @@ public static String format(TradeInfo tradeInfo) { ? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode) : String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode); - String colDataFormat = "%-" + shortIdColWidth + "s" // left justify - + " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left justify - + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // right justify - + "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // right justify - + "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // right justify - + takerFeeHeader.get() // right justify - + " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // left justify - + " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // left justify - + " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // left justify - + " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // left justify - + " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // left justify - + " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // left justify + + String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify + + " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify + + "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify + + "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify + + takerFeeHeader.get() // rt justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s" // lt justify + + " %-" + COL_HEADER_TRADE_WITHDRAWAL_TX_ID.length() + "s"; // left return headerLine + (isTaker @@ -97,7 +100,8 @@ private static String formatTradeForMaker(String format, TradeInfo tradeInfo) { tradeInfo.getIsFiatSent() ? "YES" : "NO", tradeInfo.getIsFiatReceived() ? "YES" : "NO", tradeInfo.getIsPayoutPublished() ? "YES" : "NO", - tradeInfo.getIsWithdrawn() ? "YES" : "NO"); + tradeInfo.getIsWithdrawn() ? "YES" : "NO", + tradeInfo.getIsWithdrawn() ? tradeInfo.getWithdrawalTxId() : ""); } private static String formatTradeForTaker(String format, TradeInfo tradeInfo) { @@ -113,6 +117,7 @@ private static String formatTradeForTaker(String format, TradeInfo tradeInfo) { tradeInfo.getIsFiatSent() ? "YES" : "NO", tradeInfo.getIsFiatReceived() ? "YES" : "NO", tradeInfo.getIsPayoutPublished() ? "YES" : "NO", - tradeInfo.getIsWithdrawn() ? "YES" : "NO"); + tradeInfo.getIsWithdrawn() ? "YES" : "NO", + tradeInfo.getIsWithdrawn() ? tradeInfo.getWithdrawalTxId() : ""); } } diff --git a/cli/src/main/java/bisq/cli/TransactionFormat.java b/cli/src/main/java/bisq/cli/TransactionFormat.java new file mode 100644 index 00000000000..608c2fcb71f --- /dev/null +++ b/cli/src/main/java/bisq/cli/TransactionFormat.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.cli; + +import bisq.proto.grpc.TxInfo; + +import com.google.common.annotations.VisibleForTesting; + +import static bisq.cli.ColumnHeaderConstants.*; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static com.google.common.base.Strings.padEnd; + +@VisibleForTesting +public class TransactionFormat { + + public static String format(TxInfo txInfo) { + String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER + + COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER + + COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER + + COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER + + COL_HEADER_TX_FEE + COL_HEADER_DELIMITER + + COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER + + (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER) + + "\n"; + + String colDataFormat = "%-" + txInfo.getTxId().length() + "s" + + " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s" + + " %" + COL_HEADER_TX_INPUT_SUM.length() + "s" + + " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s" + + " %" + COL_HEADER_TX_FEE.length() + "s" + + " %" + COL_HEADER_TX_SIZE.length() + "s" + + " %s"; + + return headerLine + + String.format(colDataFormat, + txInfo.getTxId(), + txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed + formatSatoshis(txInfo.getInputSum()), + formatSatoshis(txInfo.getOutputSum()), + formatSatoshis(txInfo.getFee()), + txInfo.getSize(), + txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo()); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index dbba310f5dd..6709bf42ff1 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -279,6 +279,10 @@ public TxFeeRateInfo getMostRecentTxFeeRateInfo() { return walletsService.getMostRecentTxFeeRateInfo(); } + public Transaction getTransaction(String txId) { + return walletsService.getTransaction(txId); + } + public void setWalletPassword(String password, String newPassword) { walletsService.setWalletPassword(password, newPassword); } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 2bbd585f8dd..c107259ff98 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -331,6 +331,26 @@ TxFeeRateInfo getMostRecentTxFeeRateInfo() { feeService.getLastRequest()); } + Transaction getTransaction(String txId) { + if (txId.length() != 64) + throw new IllegalArgumentException(format("%s is not a transaction id", txId)); + + try { + Transaction tx = btcWalletService.getTransaction(txId); + if (tx == null) + throw new IllegalArgumentException(format("tx with id %s not found", txId)); + else + return tx; + + } catch (IllegalArgumentException ex) { + log.error("", ex); + throw new IllegalArgumentException( + format("could not get transaction with id %s%ncause: %s", + txId, + ex.getMessage().toLowerCase())); + } + } + int getNumConfirmationsForMostRecentTransaction(String addressString) { Address address = getAddressEntry(addressString).getAddress(); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index 1a717a7672e..ec9880d5c69 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -47,6 +47,7 @@ public class TradeInfo implements Payload { private final String takerFeeTxId; private final String depositTxId; private final String payoutTxId; + private final String withdrawalTxId; private final long tradeAmountAsLong; private final long tradePrice; private final String tradingPeerNodeAddress; @@ -73,6 +74,7 @@ public TradeInfo(TradeInfoBuilder builder) { this.takerFeeTxId = builder.takerFeeTxId; this.depositTxId = builder.depositTxId; this.payoutTxId = builder.payoutTxId; + this.withdrawalTxId = builder.withdrawalTxId; this.tradeAmountAsLong = builder.tradeAmountAsLong; this.tradePrice = builder.tradePrice; this.tradingPeerNodeAddress = builder.tradingPeerNodeAddress; @@ -106,6 +108,7 @@ public static TradeInfo toTradeInfo(Trade trade, String role) { .withTakerFeeTxId(trade.getTakerFeeTxId()) .withDepositTxId(trade.getDepositTxId()) .withPayoutTxId(trade.getPayoutTxId()) + .withWithdrawalTxId(trade.getWithdrawalTxId()) .withTradeAmountAsLong(trade.getTradeAmountAsLong()) .withTradePrice(trade.getTradePrice().getValue()) .withTradingPeerNodeAddress(Objects.requireNonNull( @@ -141,6 +144,7 @@ public bisq.proto.grpc.TradeInfo toProtoMessage() { .setTakerFeeTxId(takerFeeTxId == null ? "" : takerFeeTxId) .setDepositTxId(depositTxId == null ? "" : depositTxId) .setPayoutTxId(payoutTxId == null ? "" : payoutTxId) + .setWithdrawalTxId(withdrawalTxId == null ? "" : withdrawalTxId) .setTradeAmountAsLong(tradeAmountAsLong) .setTradePrice(tradePrice) .setTradingPeerNodeAddress(tradingPeerNodeAddress) @@ -180,6 +184,7 @@ public static class TradeInfoBuilder { private String takerFeeTxId; private String depositTxId; private String payoutTxId; + private String withdrawalTxId; private long tradeAmountAsLong; private long tradePrice; private String tradingPeerNodeAddress; @@ -249,6 +254,11 @@ public TradeInfoBuilder withPayoutTxId(String payoutTxId) { return this; } + public TradeInfoBuilder withWithdrawalTxId(String withdrawalTxId) { + this.withdrawalTxId = withdrawalTxId; + return this; + } + public TradeInfoBuilder withTradeAmountAsLong(long tradeAmountAsLong) { this.tradeAmountAsLong = tradeAmountAsLong; return this; @@ -332,6 +342,7 @@ public String toString() { ", takerFeeTxId='" + takerFeeTxId + '\'' + "\n" + ", depositTxId='" + depositTxId + '\'' + "\n" + ", payoutTxId='" + payoutTxId + '\'' + "\n" + + ", withdrawalTxId='" + withdrawalTxId + '\'' + "\n" + ", tradeAmountAsLong='" + tradeAmountAsLong + '\'' + "\n" + ", tradePrice='" + tradePrice + '\'' + "\n" + ", tradingPeerNodeAddress='" + tradingPeerNodeAddress + '\'' + "\n" + diff --git a/core/src/main/java/bisq/core/api/model/TxInfo.java b/core/src/main/java/bisq/core/api/model/TxInfo.java index f1b24f6b0fa..16d8f5fc108 100644 --- a/core/src/main/java/bisq/core/api/model/TxInfo.java +++ b/core/src/main/java/bisq/core/api/model/TxInfo.java @@ -28,26 +28,42 @@ @Getter public class TxInfo implements Payload { - private final String id; + // The client cannot see an instance of an org.bitcoinj.core.Transaction. We use the + // lighter weight TxInfo proto wrapper instead, containing just enough fields to + // view some transaction details. A block explorer or bitcoin-core client can be + // used to see more detail. + + private final String txId; + private final long inputSum; private final long outputSum; private final long fee; private final int size; + private final boolean isPending; + private final String memo; - public TxInfo(String id, long outputSum, long fee, int size) { - this.id = id; - this.outputSum = outputSum; - this.fee = fee; - this.size = size; + public TxInfo(TxInfo.TxInfoBuilder builder) { + this.txId = builder.txId; + this.inputSum = builder.inputSum; + this.outputSum = builder.outputSum; + this.fee = builder.fee; + this.size = builder.size; + this.isPending = builder.isPending; + this.memo = builder.memo; } public static TxInfo toTxInfo(Transaction transaction) { if (transaction == null) throw new IllegalStateException("server created a null transaction"); - return new TxInfo(transaction.getTxId().toString(), - transaction.getOutputSum().value, - transaction.getFee().value, - transaction.getMessageSize()); + return new TxInfo.TxInfoBuilder() + .withTxId(transaction.getTxId().toString()) + .withInputSum(transaction.getInputSum().value) + .withOutputSum(transaction.getOutputSum().value) + .withFee(transaction.getFee().value) + .withSize(transaction.getMessageSize()) + .withIsPending(transaction.isPending()) + .withMemo(transaction.getMemo()) + .build(); } ////////////////////////////////////////////////////////////////////////////////////// @@ -57,28 +73,88 @@ public static TxInfo toTxInfo(Transaction transaction) { @Override public bisq.proto.grpc.TxInfo toProtoMessage() { return bisq.proto.grpc.TxInfo.newBuilder() - .setId(id) + .setTxId(txId) + .setInputSum(inputSum) .setOutputSum(outputSum) .setFee(fee) .setSize(size) + .setIsPending(isPending) + .setMemo(memo == null ? "" : memo) .build(); } @SuppressWarnings("unused") public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) { - return new TxInfo(proto.getId(), - proto.getOutputSum(), - proto.getFee(), - proto.getSize()); + return new TxInfo.TxInfoBuilder() + .withTxId(proto.getTxId()) + .withInputSum(proto.getInputSum()) + .withOutputSum(proto.getOutputSum()) + .withFee(proto.getFee()) + .withSize(proto.getSize()) + .withIsPending(proto.getIsPending()) + .withMemo(proto.getMemo()) + .build(); + } + + public static class TxInfoBuilder { + private String txId; + private long inputSum; + private long outputSum; + private long fee; + private int size; + private boolean isPending; + private String memo; + + public TxInfo.TxInfoBuilder withTxId(String txId) { + this.txId = txId; + return this; + } + + public TxInfo.TxInfoBuilder withInputSum(long inputSum) { + this.inputSum = inputSum; + return this; + } + + public TxInfo.TxInfoBuilder withOutputSum(long outputSum) { + this.outputSum = outputSum; + return this; + } + + public TxInfo.TxInfoBuilder withFee(long fee) { + this.fee = fee; + return this; + } + + public TxInfo.TxInfoBuilder withSize(int size) { + this.size = size; + return this; + } + + public TxInfo.TxInfoBuilder withIsPending(boolean isPending) { + this.isPending = isPending; + return this; + } + + public TxInfo.TxInfoBuilder withMemo(String memo) { + this.memo = memo; + return this; + } + + public TxInfo build() { + return new TxInfo(this); + } } @Override public String toString() { return "TxInfo{" + "\n" + - " id='" + id + '\'' + "\n" + - ", outputSum=" + outputSum + " sats" + "\n" + - ", fee=" + fee + " sats" + "\n" + - ", size=" + size + " bytes" + "\n" + + " txId='" + txId + '\'' + "\n" + + ", inputSum=" + inputSum + "\n" + + ", outputSum=" + outputSum + "\n" + + ", fee=" + fee + "\n" + + ", size=" + size + "\n" + + ", isPending=" + isPending + "\n" + + ", memo='" + memo + '\'' + "\n" + '}'; } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index c7bbb4623da..51b30cb1871 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -29,6 +29,8 @@ import bisq.proto.grpc.GetBalancesRequest; import bisq.proto.grpc.GetFundingAddressesReply; import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetTransactionReply; +import bisq.proto.grpc.GetTransactionRequest; import bisq.proto.grpc.GetTxFeeRateReply; import bisq.proto.grpc.GetTxFeeRateRequest; import bisq.proto.grpc.GetUnusedBsqAddressReply; @@ -280,6 +282,23 @@ public void unsetTxFeeRatePreference(UnsetTxFeeRatePreferenceRequest req, } } + @Override + public void getTransaction(GetTransactionRequest req, + StreamObserver responseObserver) { + try { + Transaction tx = coreApi.getTransaction(req.getTxId()); + var reply = GetTransactionReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + @Override public void setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index a6e30cdb473..81795112e55 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -273,19 +273,20 @@ message TradeInfo { string takerFeeTxId = 9; string depositTxId = 10; string payoutTxId = 11; - uint64 tradeAmountAsLong = 12; - uint64 tradePrice = 13; - string tradingPeerNodeAddress = 14; - string state = 15; - string phase = 16; - string tradePeriodState = 17; - bool isDepositPublished = 18; - bool isDepositConfirmed = 19; - bool isFiatSent = 20; - bool isFiatReceived = 21; - bool isPayoutPublished = 22; - bool isWithdrawn = 23; - string contractAsJson = 24; + string withdrawalTxId = 12; + uint64 tradeAmountAsLong = 13; + uint64 tradePrice = 14; + string tradingPeerNodeAddress = 15; + string state = 16; + string phase = 17; + string tradePeriodState = 18; + bool isDepositPublished = 19; + bool isDepositConfirmed = 20; + bool isFiatSent = 21; + bool isFiatReceived = 22; + bool isPayoutPublished = 23; + bool isWithdrawn = 24; + string contractAsJson = 25; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -300,10 +301,13 @@ message TxFeeRateInfo { } message TxInfo { - string id = 1; - uint64 outputSum = 2; - uint64 fee = 3; - int32 size = 4; + string txId = 1; + uint64 inputSum = 2; + uint64 outputSum = 3; + uint64 fee = 4; + int32 size = 5; + bool isPending = 6; + string memo = 7; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -325,7 +329,9 @@ service Wallets { } rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) { } - rpc unsetTxFeeRatePreference (UnsetTxFeeRatePreferenceRequest) returns (UnsetTxFeeRatePreferenceReply) { + rpc UnsetTxFeeRatePreference (UnsetTxFeeRatePreferenceRequest) returns (UnsetTxFeeRatePreferenceReply) { + } + rpc GetTransaction (GetTransactionRequest) returns (GetTransactionReply) { } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } @@ -405,6 +411,14 @@ message UnsetTxFeeRatePreferenceReply { TxFeeRateInfo txFeeRateInfo = 1; } +message GetTransactionRequest { + string txId = 1; +} + +message GetTransactionReply { + TxInfo txInfo = 1; +} + message GetFundingAddressesRequest { }