diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 0a43258d9a5..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; @@ -48,10 +49,12 @@ import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcRequest; import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; +import bisq.proto.grpc.TxInfo; import bisq.proto.grpc.UnlockWalletRequest; import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; import bisq.proto.grpc.WithdrawFundsRequest; @@ -160,8 +163,26 @@ protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() { return GetUnusedBsqAddressRequest.newBuilder().build(); } - protected final SendBsqRequest createSendBsqRequest(String address, String amount) { - return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build(); + protected final SendBsqRequest createSendBsqRequest(String address, + String amount, + String txFeeRate) { + return SendBsqRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .build(); + } + + protected final SendBtcRequest createSendBtcRequest(String address, + String amount, + String txFeeRate, + String memo) { + return SendBtcRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .setMemo(memo) + .build(); } protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { @@ -208,10 +229,13 @@ protected final KeepFundsRequest createKeepFundsRequest(String tradeId) { .build(); } - protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) { + protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, + String address, + String memo) { return WithdrawFundsRequest.newBuilder() .setTradeId(tradeId) .setAddress(address) + .setMemo(memo) .build(); } @@ -247,9 +271,36 @@ protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) { return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress(); } - protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, String amount) { + protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig, + String address, + String amount) { + return sendBsq(bisqAppConfig, address, amount, ""); + } + + protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig, + String address, + String amount, + String txFeeRate) { //noinspection ResultOfMethodCallIgnored - grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount)); + return grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, + amount, + txFeeRate)) + .getTxInfo(); + } + + protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, String address, String amount) { + return sendBtc(bisqAppConfig, address, amount, "", ""); + } + + protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, + String address, + String amount, + String txFeeRate, + String memo) { + //noinspection ResultOfMethodCallIgnored + return grpcStubs(bisqAppConfig).walletsService.sendBtc( + createSendBtcRequest(address, amount, txFeeRate, memo)) + .getTxInfo(); } protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { @@ -354,8 +405,11 @@ protected final void keepFunds(BisqAppConfig bisqAppConfig, String tradeId) { } @SuppressWarnings("ResultOfMethodCallIgnored") - protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) { - var req = createWithdrawFundsRequest(tradeId, address); + protected final void withdrawFunds(BisqAppConfig bisqAppConfig, + String tradeId, + String address, + String memo) { + var req = createWithdrawFundsRequest(tradeId, address, memo); grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req); } @@ -379,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..fe9daf27df0 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -65,9 +65,12 @@ import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static bisq.core.locale.CurrencyUtil.*; +import static bisq.core.locale.CurrencyUtil.getAllAdvancedCashCurrencies; +import static bisq.core.locale.CurrencyUtil.getAllMoneyGramCurrencies; +import static bisq.core.locale.CurrencyUtil.getAllRevolutCurrencies; +import static bisq.core.locale.CurrencyUtil.getAllUpholdCurrencies; import static bisq.core.payment.payload.PaymentMethod.*; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @@ -746,7 +749,10 @@ public void testCreateTransferwiseAccount(TestInfo testInfo) { String jsonString = getCompletedFormAsJsonString(); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString); verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); - verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount); + // As per commit 88f26f93241af698ae689bf081205d0f9dc929fa + // Do not autofill all currencies by default but keep all unselected. + // verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount); + 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/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index 2b88e4f2700..7f4038a0344 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -64,9 +64,11 @@ protected final void logTrade(Logger log, TestInfo testInfo, String description, TradeInfo trade) { - log.info(String.format("%s %s%n%s", - testName(testInfo), - description.toUpperCase(), - format(trade))); + if (log.isDebugEnabled()) { + log.debug(String.format("%s %s%n%s", + testName(testInfo), + description.toUpperCase(), + format(trade))); + } } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 2278ce315cd..673792c4f55 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -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); + withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO); genBtcBlocksThenWait(1, 2250); @@ -158,7 +160,7 @@ public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade); BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon); - log.info("{} Bob's current available balance: {} BTC", + log.debug("{} Bob's current available balance: {} BTC", testName(testInfo), formatSatoshis(currentBalance.getAvailableBalance())); } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index f9202af778a..2884555e3c7 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -108,7 +108,7 @@ public void testInitialBsqBalances(final TestInfo testInfo) { @Order(3) public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) { String bobsBsqAddress = getUnusedBsqAddress(bobdaemon); - sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT); + sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT, "100"); sleep(2000); BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); 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 daee479b89a..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,8 @@ 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; @@ -31,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 = @@ -92,6 +97,50 @@ public void testFundAlicesBtcWallet(final TestInfo testInfo) { formatBtcBalanceInfoTbl(btcBalanceInfo)); } + @Test + @Order(3) + public void testAliceSendBTCToBob(TestInfo testInfo) { + String bobsBtcAddress = getUnusedBtcAddress(bobdaemon); + log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress); + + TxInfo txInfo = sendBtc(alicedaemon, + bobsBtcAddress, + "5.50", + "100", + TX_MEMO); + assertTrue(txInfo.getIsPending()); + + // Note that the memo is not set on the tx yet. + assertTrue(txInfo.getMemo().isEmpty()); + genBtcBlocksThenWait(1, 3000); + + // 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)); + bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = + bisq.core.api.model.BtcBalanceInfo.valueOf(700000000, + 0, + 700000000, + 0); + verifyBtcBalances(alicesExpectedBalances, alicesBalances); + + BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon); + log.debug("{} Bob's BTC Balances:\n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(bobsBalances)); + // We cannot (?) predict the exact tx size and calculate how much in tx fees were + // deducted from the 5.5 BTC sent to Bob, but we do know Bob should have something + // between 15.49978000 and 15.49978100 BTC. + assertTrue(bobsBalances.getAvailableBalance() >= 1549978000); + assertTrue(bobsBalances.getAvailableBalance() <= 1549978100); + } + @AfterAll public static void tearDown() { tearDownScaffold(); diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index 6fadf3cc251..0fff4bf694d 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -67,6 +67,7 @@ public void testBtcWalletFunding(final TestInfo testInfo) { btcWalletTest.testInitialBtcBalances(testInfo); btcWalletTest.testFundAlicesBtcWallet(testInfo); + btcWalletTest.testAliceSendBTCToBob(testInfo); } @Test diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 44590cfbd6a..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; @@ -40,9 +41,11 @@ import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcRequest; import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TxInfo; import bisq.proto.grpc.UnlockWalletRequest; import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; import bisq.proto.grpc.WithdrawFundsRequest; @@ -110,9 +113,11 @@ private enum Method { getfundingaddresses, getunusedbsqaddress, sendbsq, + sendbtc, gettxfeerate, settxfeerate, unsettxfeerate, + gettransaction, lockwallet, unlockwallet, removewalletpassword, @@ -259,19 +264,56 @@ public static void run(String[] args) { throw new IllegalArgumentException("no bsq amount specified"); var amount = nonOptionArgs.get(2); + verifyStringIsValidDecimal(amount); - try { - Double.parseDouble(amount); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(format("'%s' is not a number", amount)); - } + var txFeeRate = nonOptionArgs.size() == 4 ? nonOptionArgs.get(3) : ""; + if (!txFeeRate.isEmpty()) + verifyStringIsValidLong(txFeeRate); var request = SendBsqRequest.newBuilder() .setAddress(address) .setAmount(amount) + .setTxFeeRate(txFeeRate) .build(); - walletsService.sendBsq(request); - out.printf("%s BSQ sent to %s%n", amount, address); + var reply = walletsService.sendBsq(request); + TxInfo txInfo = reply.getTxInfo(); + out.printf("%s bsq sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); + return; + } + case sendbtc: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("no btc address specified"); + + var address = nonOptionArgs.get(1); + + if (nonOptionArgs.size() < 3) + throw new IllegalArgumentException("no btc amount specified"); + + var amount = nonOptionArgs.get(2); + verifyStringIsValidDecimal(amount); + + // TODO Find a better way to handle the two optional parameters. + var txFeeRate = nonOptionArgs.size() >= 4 ? nonOptionArgs.get(3) : ""; + if (!txFeeRate.isEmpty()) + verifyStringIsValidLong(txFeeRate); + + var memo = nonOptionArgs.size() == 5 ? nonOptionArgs.get(4) : ""; + + var request = SendBtcRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .setMemo(memo) + .build(); + var reply = walletsService.sendBtc(request); + TxInfo txInfo = reply.getTxInfo(); + out.printf("%s btc sent to %s in tx %s%n", + amount, + address, + txInfo.getTxId()); return; } case gettxfeerate: { @@ -284,13 +326,7 @@ public static void run(String[] args) { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no tx fee rate specified"); - long txFeeRate; - try { - txFeeRate = Long.parseLong(nonOptionArgs.get(2)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2))); - } - + var txFeeRate = toLong(nonOptionArgs.get(2)); var request = SetTxFeeRatePreferenceRequest.newBuilder() .setTxFeeRatePreference(txFeeRate) .build(); @@ -304,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," @@ -413,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]"); @@ -472,16 +520,21 @@ public static void run(String[] args) { case withdrawfunds: { if (nonOptionArgs.size() < 3) throw new IllegalArgumentException("incorrect parameter count, " - + " expecting trade id, bitcoin wallet address"); + + " expecting trade id, bitcoin wallet address [,\"memo\"]"); var tradeId = nonOptionArgs.get(1); var address = nonOptionArgs.get(2); + // A multi-word memo must be double quoted. + var memo = nonOptionArgs.size() == 4 + ? nonOptionArgs.get(3) + : ""; var request = WithdrawFundsRequest.newBuilder() .setTradeId(tradeId) .setAddress(address) + .setMemo(memo) .build(); tradesService.withdrawFunds(request); - out.printf("funds from trade %s sent to btc address %s%n", tradeId, address); + out.printf("trade %s funds sent to btc address %s%n", tradeId, address); return; } case getpaymentmethods: { @@ -560,12 +613,7 @@ public static void run(String[] args) { if (nonOptionArgs.size() < 3) throw new IllegalArgumentException("no unlock timeout specified"); - long timeout; - try { - timeout = Long.parseLong(nonOptionArgs.get(2)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2))); - } + var timeout = toLong(nonOptionArgs.get(2)); var request = UnlockWalletRequest.newBuilder() .setPassword(nonOptionArgs.get(1)) .setTimeout(timeout).build(); @@ -627,6 +675,30 @@ private static Method getMethodFromCmd(String methodName) { return Method.valueOf(methodName.toLowerCase()); } + private static void verifyStringIsValidDecimal(String param) { + try { + Double.parseDouble(param); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("'%s' is not a number", param)); + } + } + + private static void verifyStringIsValidLong(String param) { + try { + Long.parseLong(param); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("'%s' is not a number", param)); + } + } + + private static long toLong(String param) { + try { + return Long.parseLong(param); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("'%s' is not a number", param)); + } + } + private static File saveFileToDisk(String prefix, @SuppressWarnings("SameParameterValue") String suffix, String text) { @@ -663,10 +735,12 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); - stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ"); + stream.format(rowFormat, "sendbsq", "address, amount [,tx fee rate (sats/byte)]", "Send BSQ"); + stream.format(rowFormat, "sendbtc", "address, amount [,tx fee rate (sats/byte), \"memo\"]", "Send BTC"); 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 (%) \\", ""); @@ -679,7 +753,8 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet"); - stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address", "Withdraw received funds to external wallet address"); + stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address [,\"memo\"]", + "Withdraw received funds to external wallet address"); stream.format(rowFormat, "getpaymentmethods", "", "Get list of supported payment account method ids"); stream.format(rowFormat, "getpaymentacctform", "payment method id", "Get a new payment account form"); stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account"); 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..1c286f03772 100644 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -66,18 +66,19 @@ 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 return headerLine + (isTaker 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 bc5f8a4b200..6709bf42ff1 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -34,10 +34,13 @@ import bisq.common.handlers.ResultHandler; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; import javax.inject.Inject; import javax.inject.Singleton; +import com.google.common.util.concurrent.FutureCallback; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -45,8 +48,6 @@ import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - /** * Provides high level interface to functionality of core Bisq features. * E.g. useful for different APIs to access data of different domains of Bisq. @@ -210,7 +211,7 @@ public void keepFunds(String tradeId) { coreTradesService.keepFunds(tradeId); } - public void withdrawFunds(String tradeId, String address, @Nullable String memo) { + public void withdrawFunds(String tradeId, String address, String memo) { coreTradesService.withdrawFunds(tradeId, address, memo); } @@ -246,8 +247,19 @@ public String getUnusedBsqAddress() { return walletsService.getUnusedBsqAddress(); } - public void sendBsq(String address, String amount, TxBroadcaster.Callback callback) { - walletsService.sendBsq(address, amount, callback); + public void sendBsq(String address, + String amount, + String txFeeRate, + TxBroadcaster.Callback callback) { + walletsService.sendBsq(address, amount, txFeeRate, callback); + } + + public void sendBtc(String address, + String amount, + String txFeeRate, + String memo, + FutureCallback callback) { + walletsService.sendBtc(address, amount, txFeeRate, memo, callback); } public void getTxFeeRate(ResultHandler resultHandler) { @@ -267,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/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 2f75f241407..10d21d6415d 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -41,8 +41,6 @@ import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT; import static java.lang.String.format; @@ -85,6 +83,8 @@ void takeOffer(Offer offer, String paymentAccountId, String takerFeeCurrencyCode, Consumer resultHandler) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode); @@ -149,6 +149,9 @@ void confirmPaymentReceived(String tradeId) { } void keepFunds(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); @@ -156,8 +159,10 @@ void keepFunds(String tradeId) { tradeManager.onTradeCompleted(trade); } - void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) { - // An encrypted wallet must be unlocked for this operation. + void withdrawFunds(String tradeId, String toAddress, String memo) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); + verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); @@ -172,21 +177,21 @@ void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) { var receiverAmount = amount.subtract(fee); log.info(format("Withdrawing funds received from trade %s:" - + "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s", + + "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s%n Memo %s%n", tradeId, fromAddressEntry.getAddressString(), toAddress, amount.toFriendlyString(), fee.toFriendlyString(), - receiverAmount.toFriendlyString())); - + receiverAmount.toFriendlyString(), + memo)); tradeManager.onWithdrawRequest( toAddress, amount, fee, coreWalletsService.getKey(), trade, - memo, + memo.isEmpty() ? null : memo, () -> { }, (errorMessage, throwable) -> { @@ -196,10 +201,14 @@ void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) { } String getTradeRole(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); return tradeUtil.getRole(getTrade(tradeId)); } Trade getTrade(String tradeId) { + coreWalletsService.verifyWalletsAreAvailable(); + coreWalletsService.verifyEncryptedWalletIsUnlocked(); return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId)) diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 346c97a78de..c107259ff98 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -23,7 +23,9 @@ import bisq.core.api.model.BtcBalanceInfo; import bisq.core.api.model.TxFeeRateInfo; import bisq.core.btc.Balances; +import bisq.core.btc.exceptions.AddressEntryException; import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.InsufficientFundsException; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.model.AddressEntry; @@ -35,7 +37,9 @@ import bisq.core.btc.wallet.WalletsManager; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; import bisq.common.Timer; import bisq.common.UserThread; @@ -46,10 +50,12 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.crypto.KeyCrypterScrypt; import javax.inject.Inject; +import javax.inject.Named; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; @@ -64,6 +70,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -85,6 +92,7 @@ class CoreWalletsService { private final BsqTransferService bsqTransferService; private final BsqFormatter bsqFormatter; private final BtcWalletService btcWalletService; + private final CoinFormatter btcFormatter; private final FeeService feeService; private final Preferences preferences; @@ -103,6 +111,7 @@ public CoreWalletsService(Balances balances, BsqTransferService bsqTransferService, BsqFormatter bsqFormatter, BtcWalletService btcWalletService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, FeeService feeService, Preferences preferences) { this.balances = balances; @@ -111,6 +120,7 @@ public CoreWalletsService(Balances balances, this.bsqTransferService = bsqTransferService; this.bsqFormatter = bsqFormatter; this.btcWalletService = btcWalletService; + this.btcFormatter = btcFormatter; this.feeService = feeService; this.preferences = preferences; } @@ -189,13 +199,27 @@ String getUnusedBsqAddress() { void sendBsq(String address, String amount, + String txFeeRate, TxBroadcaster.Callback callback) { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + try { LegacyAddress legacyAddress = getValidBsqLegacyAddress(address); - Coin receiverAmount = getValidBsqTransferAmount(amount); - BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount); + Coin receiverAmount = getValidTransferAmount(amount, bsqFormatter); + Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate); + BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, + receiverAmount, + txFeePerVbyte); + log.info("Sending {} BSQ to {} with tx fee rate {} sats/byte.", + amount, + address, + txFeePerVbyte.value); bsqTransferService.sendFunds(model, callback); - } catch (InsufficientMoneyException + } catch (InsufficientMoneyException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send bsq due to insufficient funds", ex); + } catch (NumberFormatException | BsqChangeBelowDustException | TransactionVerificationException | WalletException ex) { @@ -204,6 +228,61 @@ void sendBsq(String address, } } + void sendBtc(String address, + String amount, + String txFeeRate, + String memo, + FutureCallback callback) { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + try { + Set fromAddresses = btcWalletService.getAddressEntriesForAvailableBalanceStream() + .map(AddressEntry::getAddressString) + .collect(Collectors.toSet()); + Coin receiverAmount = getValidTransferAmount(amount, btcFormatter); + Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate); + + // TODO Support feeExcluded (or included), default is fee included. + // See WithdrawalView # onWithdraw (and refactor). + Transaction feeEstimationTransaction = + btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, + receiverAmount, + txFeePerVbyte); + if (feeEstimationTransaction == null) + throw new IllegalStateException("could not estimate the transaction fee"); + + Coin dust = btcWalletService.getDust(feeEstimationTransaction); + Coin fee = feeEstimationTransaction.getFee().add(dust); + if (dust.isPositive()) { + fee = feeEstimationTransaction.getFee().add(dust); + log.info("Dust txo ({} sats) was detected, the dust amount has been added to the fee (was {}, now {})", + dust.value, + feeEstimationTransaction.getFee(), + fee.value); + } + log.info("Sending {} BTC to {} with tx fee of {} sats (fee rate {} sats/byte).", + amount, + address, + fee.value, + txFeePerVbyte.value); + btcWalletService.sendFundsForMultipleAddresses(fromAddresses, + address, + receiverAmount, + fee, + null, + tempAesKey, + memo.isEmpty() ? null : memo, + callback); + } catch (AddressEntryException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send btc from any addresses in wallet", ex); + } catch (InsufficientFundsException | InsufficientMoneyException ex) { + log.error("", ex); + throw new IllegalStateException("cannot send btc due to insufficient funds", ex); + } + } + void getTxFeeRate(ResultHandler resultHandler) { try { @SuppressWarnings({"unchecked", "Convert2MethodRef"}) @@ -252,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); @@ -342,13 +441,13 @@ void removeWalletPassword(String password) { } // Throws a RuntimeException if wallets are not available (encrypted or not). - private void verifyWalletsAreAvailable() { + void verifyWalletsAreAvailable() { if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); } // Throws a RuntimeException if wallets are not available or not encrypted. - private void verifyWalletIsAvailableAndEncrypted() { + void verifyWalletIsAvailableAndEncrypted() { if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); @@ -357,7 +456,7 @@ private void verifyWalletIsAvailableAndEncrypted() { } // Throws a RuntimeException if wallets are encrypted and locked. - private void verifyEncryptedWalletIsUnlocked() { + void verifyEncryptedWalletIsUnlocked() { if (walletsManager.areWalletsEncrypted() && tempAesKey == null) throw new IllegalStateException("wallet is locked"); } @@ -423,15 +522,22 @@ private LegacyAddress getValidBsqLegacyAddress(String address) { } } - // Returns a Coin for the amount string, or a RuntimeException if invalid. - private Coin getValidBsqTransferAmount(String amount) { - Coin amountAsCoin = parseToCoin(amount, bsqFormatter); + // Returns a Coin for the transfer amount string, or a RuntimeException if invalid. + private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) { + Coin amountAsCoin = parseToCoin(amount, coinFormatter); if (amountAsCoin.isLessThan(getMinNonDustOutput())) - throw new IllegalStateException(format("%s bsq is an invalid send amount", amount)); + throw new IllegalStateException(format("%s is an invalid transfer amount", amount)); return amountAsCoin; } + private Coin getTxFeeRateFromParamOrPreferenceOrFeeService(String txFeeRate) { + // A non txFeeRate String value overrides the fee service and custom fee. + return txFeeRate.isEmpty() + ? btcWalletService.getTxFeeForWithdrawalPerVbyte() + : Coin.valueOf(Long.parseLong(txFeeRate)); + } + private KeyCrypterScrypt getKeyCrypterScrypt() { KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); if (keyCrypterScrypt == null) diff --git a/core/src/main/java/bisq/core/api/model/TxInfo.java b/core/src/main/java/bisq/core/api/model/TxInfo.java new file mode 100644 index 00000000000..16d8f5fc108 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TxInfo.java @@ -0,0 +1,160 @@ +/* + * 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.core.api.model; + +import bisq.common.Payload; + +import org.bitcoinj.core.Transaction; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public class TxInfo implements Payload { + + // 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(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.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(); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + ////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.TxInfo toProtoMessage() { + return bisq.proto.grpc.TxInfo.newBuilder() + .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.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" + + " txId='" + txId + '\'' + "\n" + + ", inputSum=" + inputSum + "\n" + + ", outputSum=" + outputSum + "\n" + + ", fee=" + fee + "\n" + + ", size=" + size + "\n" + + ", isPending=" + isPending + "\n" + + ", memo='" + memo + '\'' + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java index b6cc83e8c77..4558b0acf74 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java @@ -33,14 +33,15 @@ public BsqTransferService(WalletsManager walletsManager, } public BsqTransferModel getBsqTransferModel(LegacyAddress address, - Coin receiverAmount) + Coin receiverAmount, + Coin txFeePerVbyte) throws TransactionVerificationException, WalletException, BsqChangeBelowDustException, InsufficientMoneyException { Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, txFeePerVbyte); Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); return new BsqTransferModel(address, diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 449ee734c6c..d4be1563c4f 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -439,8 +439,7 @@ public Transaction completePreparedVoteRevealTx(Transaction preparedTx, byte[] o // Add fee input to prepared BSQ send tx /////////////////////////////////////////////////////////////////////////////////////////// - - public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean isSendTx) throws + public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs @@ -454,13 +453,26 @@ public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean // outputs [0-1] BSQ change output // outputs [0-1] BTC change output // mining fee: BTC mining fee - return completePreparedBsqTx(preparedBsqTx, isSendTx, null); + Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte(); + return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte); + } + + public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, Coin txFeePerVbyte) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { + return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte); } public Transaction completePreparedBsqTx(Transaction preparedBsqTx, - boolean useCustomTxFee, @Nullable byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { + Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte(); + return completePreparedBsqTx(preparedBsqTx, opReturnData, txFeePerVbyte); + } + + public Transaction completePreparedBsqTx(Transaction preparedBsqTx, + @Nullable byte[] opReturnData, + Coin txFeePerVbyte) throws + TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs @@ -487,8 +499,6 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, int sigSizePerInput = 106; // typical size for a tx with 2 inputs int txVsizeWithUnsignedInputs = 203; - // If useCustomTxFee we allow overriding the estimated fee from preferences - Coin txFeePerVbyte = useCustomTxFee ? getTxFeeForWithdrawalPerVbyte() : feeService.getTxFeePerVbyte(); // In case there are no change outputs we force a change by adding min dust to the BTC input Coin forcedChangeValue = Coin.ZERO; @@ -963,7 +973,7 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes } if (sendResult != null) { log.info("Broadcasting double spending transaction. " + sendResult.tx); - Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() { + Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() { @Override public void onSuccess(Transaction result) { log.info("Double spending transaction published. " + result); @@ -1043,6 +1053,14 @@ public Transaction getFeeEstimationTransaction(String fromAddress, public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, Coin amount) throws AddressFormatException, AddressEntryException, InsufficientFundsException { + Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); + return getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amount, txFeeForWithdrawalPerVbyte); + } + + public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, + Coin amount, + Coin txFeeForWithdrawalPerVbyte) + throws AddressFormatException, AddressEntryException, InsufficientFundsException { Set addressEntries = fromAddresses.stream() .map(address -> { Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); @@ -1065,7 +1083,6 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f int counter = 0; int txVsize = 0; Transaction tx; - Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); do { counter++; fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); @@ -1092,7 +1109,11 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f } private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { - long targetFee = getTxFeeForWithdrawalPerVbyte().multiply(tx.getVsize()).value; + return feeEstimationNotSatisfied(counter, tx, getTxFeeForWithdrawalPerVbyte()); + } + + private boolean feeEstimationNotSatisfied(int counter, Transaction tx, Coin txFeeForWithdrawalPerVbyte) { + long targetFee = txFeeForWithdrawalPerVbyte.multiply(tx.getVsize()).value; return counter < 10 && (tx.getFee().value < targetFee || tx.getFee().value - targetFee > 1000); @@ -1199,7 +1220,7 @@ private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses Coin fee, @Nullable String changeAddress, @Nullable KeyParameter aesKey) throws - AddressFormatException, AddressEntryException, InsufficientMoneyException { + AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); final Coin netValue = amount.subtract(fee); checkArgument(Restrictions.isAboveDust(netValue), @@ -1232,12 +1253,12 @@ private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries), preferences.getIgnoreDustThreshold()); - Optional addressEntryOptional = Optional.empty(); - AddressEntry changeAddressAddressEntry = null; + Optional addressEntryOptional = Optional.empty(); + if (changeAddress != null) addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE); - changeAddressAddressEntry = addressEntryOptional.orElseGet(() -> getFreshAddressEntry()); + AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry); checkNotNull(changeAddressAddressEntry, "change address must not be null"); sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); return sendRequest; diff --git a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java index 4a466811e0c..901793c74de 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java @@ -103,7 +103,7 @@ private Transaction getLockupTx(Coin lockupAmount, int lockTime, LockupReason lo throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException { byte[] opReturnData = BondConsensus.getLockupOpReturnData(lockTime, lockupReason, hash); Transaction preparedTx = bsqWalletService.getPreparedLockupTx(lockupAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, opReturnData); + Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, opReturnData); Transaction transaction = bsqWalletService.signTx(txWithBtcFee); log.info("Lockup tx: " + transaction); return transaction; diff --git a/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java index 0defe5e4752..a4c6691cf67 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java @@ -103,7 +103,7 @@ private Transaction getUnlockTx(String lockupTxId) checkArgument(optionalLockupTxOutput.isPresent(), "lockupTxOutput must be present"); TxOutput lockupTxOutput = optionalLockupTxOutput.get(); Transaction preparedTx = bsqWalletService.getPreparedUnlockTx(lockupTxOutput); - Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, null); + Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, null); Transaction transaction = bsqWalletService.signTx(txWithBtcFee); log.info("Unlock tx: " + transaction); return transaction; diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index bb9dbebd273..40efdd07ac2 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -17,6 +17,7 @@ package bisq.daemon.grpc; +import bisq.common.UserThread; import bisq.common.config.Config; import io.grpc.Server; @@ -48,6 +49,7 @@ public GrpcServer(Config config, GrpcTradesService tradesService, GrpcWalletsService walletsService) { this.server = ServerBuilder.forPort(config.apiPort) + .executor(UserThread.getExecutor()) .addService(disputeAgentsService) .addService(offersService) .addService(paymentAccountsService) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 1dbb453a22a..1028c0cf9d7 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -144,8 +144,7 @@ public void keepFunds(KeepFundsRequest req, public void withdrawFunds(WithdrawFundsRequest req, StreamObserver responseObserver) { try { - //TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI) - coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null); + coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), req.getMemo()); var reply = WithdrawFundsReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index b138a6c6931..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; @@ -39,6 +41,8 @@ import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SendBsqReply; import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcReply; +import bisq.proto.grpc.SendBtcRequest; import bisq.proto.grpc.SetTxFeeRatePreferenceReply; import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; import bisq.proto.grpc.SetWalletPasswordReply; @@ -57,11 +61,17 @@ import javax.inject.Inject; +import com.google.common.util.concurrent.FutureCallback; + import java.util.List; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import static bisq.core.api.model.TxInfo.toTxInfo; + @Slf4j class GrpcWalletsService extends WalletsGrpc.WalletsImplBase { @@ -145,24 +155,29 @@ public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req, public void sendBsq(SendBsqRequest req, StreamObserver responseObserver) { try { - coreApi.sendBsq(req.getAddress(), req.getAmount(), new TxBroadcaster.Callback() { - @Override - public void onSuccess(Transaction tx) { - log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes", - tx.getTxId().toString(), - tx.getOutputSum(), - tx.getFee(), - tx.getMessageSize()); - var reply = SendBsqReply.newBuilder().build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } + coreApi.sendBsq(req.getAddress(), + req.getAmount(), + req.getTxFeeRate(), + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction tx) { + log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes", + tx.getTxId().toString(), + tx.getOutputSum(), + tx.getFee(), + tx.getMessageSize()); + var reply = SendBsqReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } - @Override - public void onFailure(TxBroadcastException ex) { - throw new IllegalStateException(ex); - } - }); + @Override + public void onFailure(TxBroadcastException ex) { + throw new IllegalStateException(ex); + } + }); } catch (IllegalStateException cause) { var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); responseObserver.onError(ex); @@ -170,6 +185,46 @@ public void onFailure(TxBroadcastException ex) { } } + @Override + public void sendBtc(SendBtcRequest req, + StreamObserver responseObserver) { + try { + coreApi.sendBtc(req.getAddress(), + req.getAmount(), + req.getTxFeeRate(), + req.getMemo(), + new FutureCallback<>() { + @Override + public void onSuccess(Transaction tx) { + if (tx != null) { + log.info("Successfully published BTC tx: id {}, output sum {} sats, fee {} sats, size {} bytes", + tx.getTxId().toString(), + tx.getOutputSum(), + tx.getFee(), + tx.getMessageSize()); + var reply = SendBtcReply.newBuilder() + .setTxInfo(toTxInfo(tx).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } else { + throw new IllegalStateException("btc transaction is null"); + } + } + + @Override + public void onFailure(@NotNull Throwable t) { + log.error("", t); + throw new IllegalStateException(t); + } + }); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + @Override public void getTxFeeRate(GetTxFeeRateRequest req, StreamObserver responseObserver) { @@ -227,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/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index 917d1290aae..533064b62c1 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -247,7 +247,7 @@ private void addSendBsqGroup() { Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); try { Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); Coin miningFee = signedTx.getFee(); int txVsize = signedTx.getVsize(); @@ -305,7 +305,7 @@ private void addSendBtcGroup() { Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText()); try { Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); Coin miningFee = signedTx.getFee(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 517cfc4ea46..df37f08fd82 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -255,6 +255,7 @@ message KeepFundsReply { message WithdrawFundsRequest { string tradeId = 1; string address = 2; + string memo = 3; } message WithdrawFundsReply { @@ -287,6 +288,27 @@ message TradeInfo { string contractAsJson = 24; } +/////////////////////////////////////////////////////////////////////////////////////////// +// Transactions +/////////////////////////////////////////////////////////////////////////////////////////// + +message TxFeeRateInfo { + bool useCustomTxFeeRate = 1; + uint64 customTxFeeRate = 2; + uint64 feeServiceRate = 3; + uint64 lastFeeServiceRequestTs = 4; +} + +message TxInfo { + string txId = 1; + uint64 inputSum = 2; + uint64 outputSum = 3; + uint64 fee = 4; + int32 size = 5; + bool isPending = 6; + string memo = 7; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// @@ -300,11 +322,15 @@ service Wallets { } rpc SendBsq (SendBsqRequest) returns (SendBsqReply) { } + rpc SendBtc (SendBtcRequest) returns (SendBtcReply) { + } rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) { } 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) { } @@ -344,9 +370,22 @@ message GetUnusedBsqAddressReply { message SendBsqRequest { string address = 1; string amount = 2; + string txFeeRate = 3; } message SendBsqReply { + TxInfo txInfo = 1; +} + +message SendBtcRequest { + string address = 1; + string amount = 2; + string txFeeRate = 3; + string memo = 4; +} + +message SendBtcReply { + TxInfo txInfo = 1; } message GetTxFeeRateRequest { @@ -371,6 +410,14 @@ message UnsetTxFeeRatePreferenceReply { TxFeeRateInfo txFeeRateInfo = 1; } +message GetTransactionRequest { + string txId = 1; +} + +message GetTransactionReply { + TxInfo txInfo = 1; +} + message GetFundingAddressesRequest { } @@ -437,13 +484,6 @@ message AddressBalanceInfo { int64 numConfirmations = 3; } -message TxFeeRateInfo { - bool useCustomTxFeeRate = 1; - uint64 customTxFeeRate = 2; - uint64 feeServiceRate = 3; - uint64 lastFeeServiceRequestTs = 4; -} - /////////////////////////////////////////////////////////////////////////////////////////// // Version ///////////////////////////////////////////////////////////////////////////////////////////