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 dfc14188091..45a4333b56c 100644
--- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java
+++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java
@@ -440,8 +440,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
@@ -455,13 +454,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
@@ -488,8 +500,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;
@@ -968,7 +978,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);
@@ -1048,6 +1058,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);
@@ -1070,7 +1088,6 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f
int counter = 0;
int txVsize = 0;
Transaction tx;
- Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
@@ -1097,7 +1114,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);
@@ -1213,7 +1234,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),
@@ -1246,12 +1267,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/GrpcDisputeAgentsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java
index 24fd192fea8..cd336da436a 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java
@@ -6,8 +6,6 @@
import bisq.proto.grpc.RegisterDisputeAgentReply;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@@ -18,10 +16,12 @@
class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcDisputeAgentsService(CoreApi coreApi) {
+ public GrpcDisputeAgentsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -32,14 +32,8 @@ public void registerDisputeAgent(RegisterDisputeAgentRequest req,
var reply = RegisterDisputeAgentReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java
new file mode 100644
index 00000000000..8a1c4c2836e
--- /dev/null
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java
@@ -0,0 +1,93 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.daemon.grpc;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.StreamObserver;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import java.util.function.Predicate;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static io.grpc.Status.INVALID_ARGUMENT;
+import static io.grpc.Status.UNKNOWN;
+
+/**
+ * The singleton instance of this class handles any expected core api Throwable by
+ * wrapping its message in a gRPC StatusRuntimeException and sending it to the client.
+ * An unexpected Throwable's message will be replaced with an 'unexpected' error message.
+ */
+@Singleton
+@Slf4j
+class GrpcExceptionHandler {
+
+ private final Predicate isExpectedException = (t) ->
+ t instanceof IllegalStateException || t instanceof IllegalArgumentException;
+
+ @Inject
+ public GrpcExceptionHandler() {
+ }
+
+ public void handleException(Throwable t, StreamObserver> responseObserver) {
+ // Log the core api error (this is last chance to do that), wrap it in a new
+ // gRPC StatusRuntimeException, then send it to the client in the gRPC response.
+ log.error("", t);
+ var grpcStatusRuntimeException = wrapException(t);
+ responseObserver.onError(grpcStatusRuntimeException);
+ throw grpcStatusRuntimeException;
+ }
+
+ private StatusRuntimeException wrapException(Throwable t) {
+ // We want to be careful about what kinds of exception messages we send to the
+ // client. Expected core exceptions should be wrapped in an IllegalStateException
+ // or IllegalArgumentException, with a consistently styled and worded error
+ // message. But only a small number of the expected error types are currently
+ // handled this way; there is much work to do to handle the variety of errors
+ // that can occur in the api. In the meantime, we take care to not pass full,
+ // unexpected error messages to the client. If the exception type is unexpected,
+ // we omit details from the gRPC exception sent to the client.
+ if (isExpectedException.test(t)) {
+ if (t.getCause() != null)
+ return new StatusRuntimeException(mapGrpcErrorStatus(t.getCause(), t.getCause().getMessage()));
+ else
+ return new StatusRuntimeException(mapGrpcErrorStatus(t, t.getMessage()));
+ } else {
+ return new StatusRuntimeException(mapGrpcErrorStatus(t, "unexpected error on server"));
+ }
+ }
+
+ private Status mapGrpcErrorStatus(Throwable t, String description) {
+ // We default to the UNKNOWN status, except were the mapping of a core api
+ // exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy
+ // to support RESTful clients, we will need to have more specific mappings
+ // to support correct HTTP 1.1. status codes.
+ //noinspection SwitchStatementWithTooFewBranches
+ switch (t.getClass().getSimpleName()) {
+ // We go ahead and use a switch statement instead of if, in anticipation
+ // of more, specific exception mappings.
+ case "IllegalArgumentException":
+ return INVALID_ARGUMENT.withDescription(description);
+ default:
+ return UNKNOWN.withDescription(description);
+ }
+ }
+}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java
index 4c98e939af3..18cd5ef1c7c 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcGetTradeStatisticsService.java
@@ -16,22 +16,27 @@
class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcGetTradeStatisticsService(CoreApi coreApi) {
+ public GrpcGetTradeStatisticsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
public void getTradeStatistics(GetTradeStatisticsRequest req,
StreamObserver responseObserver) {
-
- var tradeStatistics = coreApi.getTradeStatistics().stream()
- .map(TradeStatistics3::toProtoTradeStatistics3)
- .collect(Collectors.toList());
-
- var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
- responseObserver.onNext(reply);
- responseObserver.onCompleted();
+ try {
+ var tradeStatistics = coreApi.getTradeStatistics().stream()
+ .map(TradeStatistics3::toProtoTradeStatistics3)
+ .collect(Collectors.toList());
+
+ var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
+ }
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java
index 1efed108ca6..9b6690864f2 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java
@@ -31,8 +31,6 @@
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OffersGrpc;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@@ -48,10 +46,12 @@
class GrpcOffersService extends OffersGrpc.OffersImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcOffersService(CoreApi coreApi) {
+ public GrpcOffersService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -64,26 +64,28 @@ public void getOffer(GetOfferRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@Override
public void getOffers(GetOffersRequest req,
StreamObserver responseObserver) {
- List result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
- .stream().map(OfferInfo::toOfferInfo)
- .collect(Collectors.toList());
- var reply = GetOffersReply.newBuilder()
- .addAllOffers(result.stream()
- .map(OfferInfo::toProtoMessage)
- .collect(Collectors.toList()))
- .build();
- responseObserver.onNext(reply);
- responseObserver.onCompleted();
+ try {
+ List result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
+ .stream().map(OfferInfo::toOfferInfo)
+ .collect(Collectors.toList());
+ var reply = GetOffersReply.newBuilder()
+ .addAllOffers(result.stream()
+ .map(OfferInfo::toProtoMessage)
+ .collect(Collectors.toList()))
+ .build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
+ }
}
@Override
@@ -111,10 +113,8 @@ public void createOffer(CreateOfferRequest req,
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -126,10 +126,8 @@ public void cancelOffer(CancelOfferRequest req,
var reply = CancelOfferReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
index 0438d33655d..4decb5b1bae 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
@@ -31,8 +31,6 @@
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.PaymentAccountsGrpc;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@@ -43,10 +41,12 @@
class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcPaymentAccountsService(CoreApi coreApi) {
+ public GrpcPaymentAccountsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -59,14 +59,8 @@ public void createPaymentAccount(CreatePaymentAccountRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -81,14 +75,8 @@ public void getPaymentAccounts(GetPaymentAccountsRequest req,
.addAllPaymentAccounts(paymentAccounts).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -103,14 +91,8 @@ public void getPaymentMethods(GetPaymentMethodsRequest req,
.addAllPaymentMethods(paymentMethods).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -124,14 +106,8 @@ public void getPaymentAccountForm(GetPaymentAccountFormRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java
index 11410140fdc..c8d20fcf29b 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java
@@ -23,8 +23,6 @@
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.PriceGrpc;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@@ -35,10 +33,12 @@
class GrpcPriceService extends PriceGrpc.PriceImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcPriceService(CoreApi coreApi) {
+ public GrpcPriceService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -49,10 +49,8 @@ public void getMarketPrice(MarketPriceRequest req,
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
}
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..b862e04ac95 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java
@@ -35,8 +35,6 @@
import bisq.proto.grpc.WithdrawFundsReply;
import bisq.proto.grpc.WithdrawFundsRequest;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@@ -49,10 +47,12 @@
class GrpcTradesService extends TradesGrpc.TradesImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcTradesService(CoreApi coreApi) {
+ public GrpcTradesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -66,10 +66,8 @@ public void getTrade(GetTradeRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -88,10 +86,8 @@ public void takeOffer(TakeOfferRequest req,
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -103,10 +99,8 @@ public void confirmPaymentStarted(ConfirmPaymentStartedRequest req,
var reply = ConfirmPaymentStartedReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -118,10 +112,8 @@ public void confirmPaymentReceived(ConfirmPaymentReceivedRequest req,
var reply = ConfirmPaymentReceivedReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -133,10 +125,8 @@ public void keepFunds(KeepFundsRequest req,
var reply = KeepFundsReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -144,15 +134,12 @@ 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();
- } catch (IllegalStateException | IllegalArgumentException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java
index 658ef10e27f..48c4d8a6b12 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcVersionService.java
@@ -13,16 +13,22 @@
class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcVersionService(CoreApi coreApi) {
+ public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
public void getVersion(GetVersionRequest req, StreamObserver responseObserver) {
- var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
- responseObserver.onNext(reply);
- responseObserver.onCompleted();
+ try {
+ var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
+ }
}
}
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java
index b138a6c6931..f8b06758070 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;
@@ -49,27 +53,33 @@
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WalletsGrpc;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.bitcoinj.core.Transaction;
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 {
private final CoreApi coreApi;
+ private final GrpcExceptionHandler exceptionHandler;
@Inject
- public GrpcWalletsService(CoreApi coreApi) {
+ public GrpcWalletsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
+ this.exceptionHandler = exceptionHandler;
}
@Override
@@ -81,10 +91,8 @@ public void getBalances(GetBalancesRequest req, StreamObserver
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -97,10 +105,8 @@ public void getAddressBalance(GetAddressBalanceRequest req,
.setAddressBalanceInfo(balanceInfo.toProtoMessage()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -117,10 +123,8 @@ public void getFundingAddresses(GetFundingAddressesRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -134,10 +138,8 @@ public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req,
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -145,28 +147,69 @@ 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);
- }
- });
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ @Override
+ public void onFailure(TxBroadcastException ex) {
+ throw new IllegalStateException(ex);
+ }
+ });
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
+ }
+ }
+
+ @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 (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -182,10 +225,8 @@ public void getTxFeeRate(GetTxFeeRateRequest req,
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -201,10 +242,8 @@ public void setTxFeeRatePreference(SetTxFeeRatePreferenceRequest req,
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -220,10 +259,23 @@ public void unsetTxFeeRatePreference(UnsetTxFeeRatePreferenceRequest req,
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
+ }
+ }
+
+ @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 (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -235,10 +287,8 @@ public void setWalletPassword(SetWalletPasswordRequest req,
var reply = SetWalletPasswordReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -250,10 +300,8 @@ public void removeWalletPassword(RemoveWalletPasswordRequest req,
var reply = RemoveWalletPasswordReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -265,10 +313,8 @@ public void lockWallet(LockWalletRequest req,
var reply = LockWalletReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, responseObserver);
}
}
@@ -280,10 +326,8 @@ public void unlockWallet(UnlockWalletRequest req,
var reply = UnlockWalletReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
- } catch (IllegalStateException cause) {
- var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
- responseObserver.onError(ex);
- throw ex;
+ } catch (Throwable cause) {
+ exceptionHandler.handleException(cause, 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
///////////////////////////////////////////////////////////////////////////////////////////