diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index 6fde51bcc26..f27f8f93f27 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -81,8 +81,8 @@ public void testBsqWalletFunding(final TestInfo testInfo) { bsqWalletTest.testGetUnusedBsqAddress(); bsqWalletTest.testInitialBsqBalances(testInfo); - // bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); - // bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); + bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); } @Test diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index c4a8fe6a533..1842f324c53 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -22,16 +22,25 @@ import bisq.core.api.model.BsqBalanceInfo; import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.Balances; +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.BsqTransferModel; +import bisq.core.btc.wallet.BsqTransferService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; +import bisq.core.util.coin.BsqFormatter; import bisq.common.Timer; import bisq.common.UserThread; import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.crypto.KeyCrypterScrypt; @@ -43,6 +52,8 @@ import org.bouncycastle.crypto.params.KeyParameter; +import java.math.BigDecimal; + import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -52,6 +63,7 @@ import javax.annotation.Nullable; +import static bisq.core.util.ParsingUtils.parseToCoin; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.SECONDS; @@ -61,6 +73,8 @@ class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; private final BsqWalletService bsqWalletService; + private final BsqTransferService bsqTransferService; + private final BsqFormatter bsqFormatter; private final BtcWalletService btcWalletService; @Nullable @@ -73,10 +87,14 @@ class CoreWalletsService { public CoreWalletsService(Balances balances, WalletsManager walletsManager, BsqWalletService bsqWalletService, + BsqTransferService bsqTransferService, + BsqFormatter bsqFormatter, BtcWalletService btcWalletService) { this.balances = balances; this.walletsManager = walletsManager; this.bsqWalletService = bsqWalletService; + this.bsqTransferService = bsqTransferService; + this.bsqFormatter = bsqFormatter; this.btcWalletService = btcWalletService; } @@ -197,15 +215,9 @@ String getUnusedBsqAddress() { return bsqWalletService.getUnusedBsqAddressAsString(); } - @SuppressWarnings("unused") void sendBsq(String address, double amount, TxBroadcaster.Callback callback) { - - throw new UnsupportedOperationException("sendbsq not implemented"); - - // TODO Uncomment after desktop::BsqSendView refactoring. - /* try { LegacyAddress legacyAddress = getValidBsqLegacyAddress(address); Coin receiverAmount = getValidBsqTransferAmount(amount); @@ -218,7 +230,6 @@ void sendBsq(String address, log.error("", ex); throw new IllegalStateException(ex); } - */ } int getNumConfirmationsForMostRecentTransaction(String addressString) { @@ -331,6 +342,25 @@ private void verifyEncryptedWalletIsUnlocked() { throw new IllegalStateException("wallet is locked"); } + // Returns a LegacyAddress for the string, or a RuntimeException if invalid. + private LegacyAddress getValidBsqLegacyAddress(String address) { + try { + return bsqFormatter.getAddressFromBsqAddress(address); + } catch (Throwable t) { + log.error("", t); + throw new IllegalStateException(format("%s is not a valid bsq address", address)); + } + } + + // Returns a Coin for the double amount, or a RuntimeException if invalid. + private Coin getValidBsqTransferAmount(double amount) { + Coin amountAsCoin = parseToCoin(new BigDecimal(amount).toString(), bsqFormatter); + if (amountAsCoin.equals(Coin.ZERO)) + throw new IllegalStateException(format("%.2f bsq is an invalid send amount", amount)); + + return amountAsCoin; + } + private KeyCrypterScrypt getKeyCrypterScrypt() { KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); if (keyCrypterScrypt == null) diff --git a/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java new file mode 100644 index 00000000000..3033c769eee --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java @@ -0,0 +1,77 @@ +package bisq.core.btc.model; + +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.util.coin.CoinUtil; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import lombok.Getter; + +@Getter +public final class BsqTransferModel { + + private final LegacyAddress receiverAddress; + private final Coin receiverAmount; + private final Transaction preparedSendTx; + private final Transaction txWithBtcFee; + private final Transaction signedTx; + private final Coin miningFee; + private final int txSize; + private final TxType txType; + + public BsqTransferModel(LegacyAddress receiverAddress, + Coin receiverAmount, + Transaction preparedSendTx, + Transaction txWithBtcFee, + Transaction signedTx) { + this.receiverAddress = receiverAddress; + this.receiverAmount = receiverAmount; + this.preparedSendTx = preparedSendTx; + this.txWithBtcFee = txWithBtcFee; + this.signedTx = signedTx; + this.miningFee = signedTx.getFee(); + this.txSize = signedTx.bitcoinSerialize().length; + this.txType = TxType.TRANSFER_BSQ; + } + + public String getReceiverAddressAsString() { + return receiverAddress.toString(); + } + + public double getMiningFeeInSatoshisPerByte() { + return CoinUtil.getFeePerByte(miningFee, txSize); + } + + public double getTxSizeInKb() { + return txSize / 1000d; + } + + public String toShortString() { + return "{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", txWithBtcFee.txId=" + txWithBtcFee.getTxId() + "\n" + + ", miningFee=" + miningFee + "\n" + + ", miningFeeInSatoshisPerByte=" + getMiningFeeInSatoshisPerByte() + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + '}'; + } + + @Override + public String toString() { + return "BsqTransferModel{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", preparedSendTx=" + preparedSendTx + "\n" + + ", txWithBtcFee=" + txWithBtcFee + "\n" + + ", signedTx=" + signedTx + "\n" + + ", miningFee=" + miningFee + "\n" + + ", miningFeeInSatoshisPerByte=" + getMiningFeeInSatoshisPerByte() + "\n" + + ", txSize=" + txSize + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + ", txType=" + txType + "\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 new file mode 100644 index 00000000000..b6cc83e8c77 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java @@ -0,0 +1,59 @@ +package bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.BsqTransferModel; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class BsqTransferService { + + private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + + @Inject + public BsqTransferService(WalletsManager walletsManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService) { + this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + } + + public BsqTransferModel getBsqTransferModel(LegacyAddress address, + Coin receiverAmount) + throws TransactionVerificationException, + WalletException, + BsqChangeBelowDustException, + InsufficientMoneyException { + + Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); + + return new BsqTransferModel(address, + receiverAmount, + preparedSendTx, + txWithBtcFee, + signedTx); + } + + public void sendFunds(BsqTransferModel bsqTransferModel, TxBroadcaster.Callback callback) { + log.info("Publishing BSQ transfer {}", bsqTransferModel.toShortString()); + walletsManager.publishAndCommitBsqTx(bsqTransferModel.getTxWithBtcFee(), + bsqTransferModel.getTxType(), + callback); + } +} 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 fa638c4a19c..c3dee6eb0e4 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 @@ -35,9 +35,13 @@ import bisq.desktop.util.validation.BtcValidator; import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.model.BsqTransferModel; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqTransferService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; @@ -46,10 +50,10 @@ import bisq.core.dao.state.model.blockchain.TxType; import bisq.core.locale.Res; import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; -import bisq.core.util.ParsingUtils; import bisq.core.util.validation.BtcAddressValidator; import bisq.network.p2p.P2PService; @@ -59,6 +63,7 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -90,6 +95,7 @@ public class BsqSendView extends ActivatableView implements BsqB private final BtcValidator btcValidator; private final BsqAddressValidator bsqAddressValidator; private final BtcAddressValidator btcAddressValidator; + private final BsqTransferService bsqTransferService; private final WalletPasswordWindow walletPasswordWindow; private int gridRow = 0; @@ -119,6 +125,7 @@ private BsqSendView(BsqWalletService bsqWalletService, BtcValidator btcValidator, BsqAddressValidator bsqAddressValidator, BtcAddressValidator btcAddressValidator, + BsqTransferService bsqTransferService, WalletPasswordWindow walletPasswordWindow) { this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; @@ -133,6 +140,7 @@ private BsqSendView(BsqWalletService bsqWalletService, this.btcValidator = btcValidator; this.bsqAddressValidator = bsqAddressValidator; this.btcAddressValidator = btcAddressValidator; + this.bsqTransferService = bsqTransferService; this.walletPasswordWindow = walletPasswordWindow; } @@ -241,22 +249,15 @@ private void addSendBsqGroup() { sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send")); sendBsqButton.setOnAction((event) -> { - // TODO break up in methods if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { - String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); - Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); try { - Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); - Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); - Coin miningFee = signedTx.getFee(); - int txSize = signedTx.bitcoinSerialize().length; - showPublishTxPopup(receiverAmount, - txWithBtcFee, - TxType.TRANSFER_BSQ, - miningFee, - txSize, - receiversAddressInputTextField.getText(), + BsqTransferModel model = getBsqTransferModel(); + showPublishTxPopup(model.getReceiverAmount(), + model.getTxWithBtcFee(), + model.getTxType(), + model.getMiningFee(), + model.getTxSize(), + model.getReceiverAddressAsString(), bsqFormatter, btcFormatter, () -> { @@ -273,6 +274,16 @@ private void addSendBsqGroup() { }); } + private BsqTransferModel getBsqTransferModel() + throws InsufficientMoneyException, + TransactionVerificationException, + BsqChangeBelowDustException, + WalletException { + Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); + LegacyAddress legacyAddress = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()); + return bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount); + } + private void setSendBtcGroupVisibleState(boolean visible) { btcTitledGroupBg.setVisible(visible); receiversBtcAddressInputTextField.setVisible(visible);