diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 5d896670..5378432e 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -171,7 +171,7 @@ class CoreWalletsService { String getXmrNewSubaddress() { accountService.checkAccountOpen(); - return xmrWalletService.getWallet().createSubaddress(0).getAddress(); + return xmrWalletService.getNewAddressEntry().getAddressString(); } List<MoneroTxWallet> getXmrTxs() { diff --git a/core/src/main/java/bisq/core/btc/listeners/XmrBalanceListener.java b/core/src/main/java/bisq/core/btc/listeners/XmrBalanceListener.java index 04f20617..8d60825f 100644 --- a/core/src/main/java/bisq/core/btc/listeners/XmrBalanceListener.java +++ b/core/src/main/java/bisq/core/btc/listeners/XmrBalanceListener.java @@ -25,8 +25,8 @@ public class XmrBalanceListener { public XmrBalanceListener() { } - public XmrBalanceListener(Integer accountIndex) { - this.subaddressIndex = accountIndex; + public XmrBalanceListener(Integer subaddressIndex) { + this.subaddressIndex = subaddressIndex; } public Integer getSubaddressIndex() { diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index 14234cfc..993a445c 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -55,6 +55,7 @@ import monero.wallet.model.MoneroCheckTx; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; +import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; @@ -70,6 +71,7 @@ public class XmrWalletService { // Monero configuration // TODO: don't hard code configuration, inject into classes? + public static final int NUM_BLOCKS_UNLOCK = 10; private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType(); private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager(); private static final String MONERO_WALLET_RPC_DIR = System.getProperty("user.dir") + File.separator + ".localnet"; // .localnet contains monero-wallet-rpc and wallet files @@ -634,6 +636,7 @@ public class XmrWalletService { // clear wallets wallet = null; multisigWallets.clear(); + walletListeners.clear(); } private void backupWallet(String walletName) { @@ -649,7 +652,17 @@ public class XmrWalletService { } // ----------------------------- LEGACY APP ------------------------------- - + + public XmrAddressEntry getNewAddressEntry() { + return getOrCreateAddressEntry(XmrAddressEntry.Context.AVAILABLE, Optional.empty()); + } + + public XmrAddressEntry getFreshAddressEntry() { + List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries(); + if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); + else return unusedAddressEntries.get(0); + } + public XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE); if (!available.isPresent()) return null; @@ -761,12 +774,21 @@ public class XmrWalletService { return xmrAddressEntryList.getAddressEntriesAsListImmutable(); } - public boolean isSubaddressUnused(int subaddressIndex) { - return subaddressIndex != 0 && getBalanceForSubaddress(subaddressIndex).value == 0; - // return !wallet.getSubaddress(accountIndex, 0).isUsed(); // TODO: isUsed() - // does not include unconfirmed funds + public List<XmrAddressEntry> getUnusedAddressEntries() { + return getAvailableAddressEntries().stream() + .filter(e -> isSubaddressUnused(e.getSubaddressIndex())) + .collect(Collectors.toList()); } + public boolean isSubaddressUnused(int subaddressIndex) { + return getNumTxOutputsForSubaddress(subaddressIndex) == 0; + } + + public Coin getBalanceForAddress(String address) { + return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex()); + } + + // TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile public Coin getBalanceForSubaddress(int subaddressIndex) { // get subaddress balance @@ -786,6 +808,24 @@ public class XmrWalletService { return Coin.valueOf(balance.longValueExact()); } + public int getNumTxOutputsForSubaddress(int subaddressIndex) { + + // get txs with transfers to the subaddress + List<MoneroTxWallet> txs = wallet.getTxs(new MoneroTxQuery() + .setTransferQuery((new MoneroTransferQuery() + .setAccountIndex(0) + .setSubaddressIndex(subaddressIndex) + .setIsIncoming(true))) + .setIncludeOutputs(true)); + + // count num outputs + int numUnspentOutputs = 0; + for (MoneroTxWallet tx : txs) { + numUnspentOutputs += tx.isConfirmed() ? tx.getOutputs().size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs + } + return numUnspentOutputs; + } + public Coin getAvailableConfirmedBalance() { return wallet != null ? Coin.valueOf(wallet.getUnlockedBalance(0).longValueExact()) : Coin.ZERO; } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index c130d5bf..c2b27c43 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -26,7 +26,6 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferDirection; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; -import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.support.dispute.refund.RefundResultState; import bisq.core.support.messages.ChatMessage; @@ -886,7 +885,7 @@ public abstract class Trade implements Tradable, Model { // check if deposit txs unlocked if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) { - long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + 9; + long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK - 1; if (havenoWallet.getHeight() >= unlockHeight) { setConfirmedState(); return; @@ -926,7 +925,7 @@ public abstract class Trade implements Tradable, Model { // compute unlock height if (unlockHeight == null && txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) { - unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + 9; + unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK - 1; } // check if txs unlocked diff --git a/core/src/main/java/bisq/core/util/ParsingUtils.java b/core/src/main/java/bisq/core/util/ParsingUtils.java index de6c8fd2..304f959e 100644 --- a/core/src/main/java/bisq/core/util/ParsingUtils.java +++ b/core/src/main/java/bisq/core/util/ParsingUtils.java @@ -25,6 +25,10 @@ public class ParsingUtils { return centinerosToAtomicUnits(coin.value); } + public static double coinToXmr(Coin coin) { + return atomicUnitsToXmr(coinToAtomicUnits(coin)); + } + public static BigInteger centinerosToAtomicUnits(long centineros) { return BigInteger.valueOf(centineros).multiply(ParsingUtils.CENTINEROS_AU_MULTIPLIER); } @@ -39,7 +43,11 @@ public class ParsingUtils { public static long atomicUnitsToCentineros(BigInteger atomicUnits) { return atomicUnits.divide(CENTINEROS_AU_MULTIPLIER).longValueExact(); - } + } + + public static Coin atomicUnitsToCoin(BigInteger atomicUnits) { + return Coin.valueOf(atomicUnitsToCentineros(atomicUnits)); + } public static double atomicUnitsToXmr(BigInteger atomicUnits) { return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 8d8209ca..ba89e6c2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -127,7 +127,7 @@ shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date -shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? +shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3}\n\nThe recipient will receive: {4}\n\nAre you sure you want to withdraw this amount? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard diff --git a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java index 90a26a35..73b99b56 100644 --- a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java @@ -20,19 +20,17 @@ package bisq.desktop.components; import bisq.desktop.components.indicator.TxConfidenceIndicator; import bisq.desktop.util.GUIUtil; -import bisq.core.btc.listeners.TxConfidenceListener; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.common.util.Utilities; -import org.bitcoinj.core.TransactionConfidence; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; - +import java.math.BigInteger; import com.jfoenix.controls.JFXTextField; import javafx.scene.control.Label; @@ -42,21 +40,23 @@ import javafx.scene.layout.AnchorPane; import lombok.Getter; import lombok.Setter; - +import monero.wallet.model.MoneroTxWallet; +import monero.wallet.model.MoneroWalletListener; import javax.annotation.Nullable; public class TxIdTextField extends AnchorPane { @Setter private static Preferences preferences; @Setter - private static BtcWalletService walletService; + private static XmrWalletService xmrWalletService; @Getter private final TextField textField; private final Tooltip progressIndicatorTooltip; private final TxConfidenceIndicator txConfidenceIndicator; private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon; - private TxConfidenceListener txConfidenceListener; + + private MoneroWalletListener txUpdater; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -113,8 +113,10 @@ public class TxIdTextField extends AnchorPane { } public void setup(@Nullable String txId) { - if (txConfidenceListener != null) - walletService.removeTxConfidenceListener(txConfidenceListener); + if (txUpdater != null) { + xmrWalletService.removeWalletListener(txUpdater); + txUpdater = null; + } if (txId == null) { textField.setText(Res.get("shared.na")); @@ -128,15 +130,22 @@ public class TxIdTextField extends AnchorPane { missingTxWarningIcon.setManaged(true); return; } - - txConfidenceListener = new TxConfidenceListener(txId) { + + // listen for tx updates + // TODO: this only listens for new blocks, listen for double spend + txUpdater = new MoneroWalletListener() { @Override - public void onTransactionConfidenceChanged(TransactionConfidence confidence) { - updateConfidence(confidence); + public void onNewBlock(long height) { + updateConfidence(txId); + } + @Override + public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + updateConfidence(txId); } }; - walletService.addTxConfidenceListener(txConfidenceListener); - updateConfidence(walletService.getConfidenceForTxId(txId)); + xmrWalletService.addWalletListener(txUpdater); + + updateConfidence(txId); textField.setText(txId); textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); @@ -145,9 +154,10 @@ public class TxIdTextField extends AnchorPane { } public void cleanup() { - if (walletService != null && txConfidenceListener != null) - walletService.removeTxConfidenceListener(txConfidenceListener); - + if (xmrWalletService != null && txUpdater != null) { + xmrWalletService.removeWalletListener(txUpdater); + txUpdater = null; + } textField.setOnMouseClicked(null); blockExplorerIcon.setOnMouseClicked(null); copyIcon.setOnMouseClicked(null); @@ -165,9 +175,15 @@ public class TxIdTextField extends AnchorPane { } } - private void updateConfidence(TransactionConfidence confidence) { - GUIUtil.updateConfidence(confidence, progressIndicatorTooltip, txConfidenceIndicator); - if (confidence != null) { + private void updateConfidence(String txId) { + MoneroTxWallet tx = null; + try { + tx = xmrWalletService.getWallet().getTx(txId); + } catch (Exception e) { + // do nothing + } + GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator); + if (tx != null) { if (txConfidenceIndicator.getProgress() != 0) { txConfidenceIndicator.setVisible(true); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 8db799fb..1a9345a8 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -200,8 +200,8 @@ public class MainView extends InitializableView<StackPane, MainViewModel> { sellButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT4, keyEvent)) { portfolioButton.fire(); -// } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT5, keyEvent)) { -// fundsButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT5, keyEvent)) { + fundsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT6, keyEvent)) { supportButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { @@ -304,7 +304,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> { }); HBox primaryNav = new HBox(marketButton, getNavigationSeparator(), buyButton, getNavigationSeparator(), - sellButton, getNavigationSeparator(), portfolioButtonWithBadge, getNavigationSeparator()); + sellButton, getNavigationSeparator(), portfolioButtonWithBadge, getNavigationSeparator(), fundsButton); primaryNav.setAlignment(Pos.CENTER_LEFT); primaryNav.getStyleClass().add("nav-primary"); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 5d065b6f..d5d4dfd1 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -43,7 +43,7 @@ import bisq.core.alert.PrivateNotificationManager; import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.app.HavenoSetup; import bisq.core.btc.nodes.LocalBitcoinNode; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.CryptoCurrency; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -153,7 +153,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @Inject public MainViewModel(HavenoSetup bisqSetup, CoreMoneroConnectionsService connectionService, - BtcWalletService btcWalletService, + XmrWalletService xmrWalletService, User user, BalancePresentation balancePresentation, TradePresentation tradePresentation, @@ -202,7 +202,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener TxIdTextField.setPreferences(preferences); - TxIdTextField.setWalletService(btcWalletService); + TxIdTextField.setXmrWalletService(xmrWalletService); GUIUtil.setFeeService(feeService); GUIUtil.setPreferences(preferences); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositListItem.java index fa1681c8..97f7f4ce 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositListItem.java @@ -17,117 +17,60 @@ package bisq.desktop.main.funds.deposit; -import bisq.desktop.components.indicator.TxConfidenceIndicator; -import bisq.desktop.util.GUIUtil; - -import bisq.core.btc.listeners.BalanceListener; -import bisq.core.btc.listeners.TxConfidenceListener; -import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.listeners.XmrBalanceListener; +import bisq.core.btc.model.XmrAddressEntry; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinFormatter; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; - -import com.google.common.base.Suppliers; - -import javafx.scene.control.Tooltip; - +import java.math.BigInteger; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; - -import java.util.function.Supplier; - import lombok.extern.slf4j.Slf4j; +import org.bitcoinj.core.Coin; @Slf4j class DepositListItem { private final StringProperty balance = new SimpleStringProperty(); - private final BtcWalletService walletService; + private final XmrAddressEntry addressEntry; + private final XmrWalletService xmrWalletService; private Coin balanceAsCoin; - private final String addressString; private String usage = "-"; - private TxConfidenceListener txConfidenceListener; - private BalanceListener balanceListener; + private XmrBalanceListener balanceListener; private int numTxOutputs = 0; - private final Supplier<LazyFields> lazyFieldsSupplier; - private static class LazyFields { - TxConfidenceIndicator txConfidenceIndicator; - Tooltip tooltip; - } + DepositListItem(XmrAddressEntry addressEntry, XmrWalletService xmrWalletService, CoinFormatter formatter) { + this.xmrWalletService = xmrWalletService; + this.addressEntry = addressEntry; - private LazyFields lazy() { - return lazyFieldsSupplier.get(); - } - - DepositListItem(AddressEntry addressEntry, BtcWalletService walletService, CoinFormatter formatter) { - this.walletService = walletService; - - addressString = addressEntry.getAddressString(); - - Address address = addressEntry.getAddress(); - TransactionConfidence confidence = walletService.getConfidenceForAddress(address); - - // confidence - lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ - txConfidenceIndicator = new TxConfidenceIndicator(); - txConfidenceIndicator.setId("funds-confidence"); - tooltip = new Tooltip(Res.get("shared.notUsedYet")); - txConfidenceIndicator.setProgress(0); - txConfidenceIndicator.setTooltip(tooltip); - if (confidence != null) { - GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator); - } - }}); - - if (confidence != null) { - txConfidenceListener = new TxConfidenceListener(confidence.getTransactionHash().toString()) { - @Override - public void onTransactionConfidenceChanged(TransactionConfidence confidence) { - GUIUtil.updateConfidence(confidence, lazy().tooltip, lazy().txConfidenceIndicator); - } - }; - walletService.addTxConfidenceListener(txConfidenceListener); - } - - balanceListener = new BalanceListener(address) { + balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { @Override - public void onBalanceChanged(Coin balanceAsCoin, Transaction tx) { - DepositListItem.this.balanceAsCoin = balanceAsCoin; + public void onBalanceChanged(BigInteger balance) { + DepositListItem.this.balanceAsCoin = ParsingUtils.atomicUnitsToCoin(balance); DepositListItem.this.balance.set(formatter.formatCoin(balanceAsCoin)); - var confidence = walletService.getConfidenceForTxId(tx.getTxId().toString()); - GUIUtil.updateConfidence(confidence, lazy().tooltip, lazy().txConfidenceIndicator); - updateUsage(address); + updateUsage(addressEntry.getSubaddressIndex()); } }; - walletService.addBalanceListener(balanceListener); + xmrWalletService.addBalanceListener(balanceListener); - balanceAsCoin = walletService.getBalanceForAddress(address); + balanceAsCoin = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); // TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile + balanceAsCoin = Coin.valueOf(ParsingUtils.atomicUnitsToCentineros(balanceAsCoin.longValue())); // in centineros balance.set(formatter.formatCoin(balanceAsCoin)); - updateUsage(address); + updateUsage(addressEntry.getSubaddressIndex()); } - private void updateUsage(Address address) { - numTxOutputs = walletService.getNumTxOutputsForAddress(address); + private void updateUsage(int subaddressIndex) { + numTxOutputs = xmrWalletService.getNumTxOutputsForSubaddress(addressEntry.getSubaddressIndex()); usage = numTxOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxOutputs); } public void cleanup() { - walletService.removeTxConfidenceListener(txConfidenceListener); - walletService.removeBalanceListener(balanceListener); - } - - public TxConfidenceIndicator getTxConfidenceIndicator() { - return lazy().txConfidenceIndicator; + xmrWalletService.removeBalanceListener(balanceListener); } public String getAddressString() { - return addressString; + return addressEntry.getAddressString(); } public String getUsage() { @@ -149,4 +92,8 @@ class DepositListItem { public int getNumTxOutputs() { return numTxOutputs; } + + public int getNumConfirmationsSinceFirstUsed() { + throw new RuntimeException("Not implemented"); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java index e03dd18d..2352db17 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java @@ -30,9 +30,9 @@ import bisq.desktop.main.overlays.windows.QRCodeWindow; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; -import bisq.core.btc.listeners.BalanceListener; -import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.listeners.XmrBalanceListener; +import bisq.core.btc.model.XmrAddressEntry; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; @@ -41,21 +41,16 @@ import bisq.core.util.coin.CoinFormatter; import bisq.common.UserThread; import bisq.common.app.DevEnv; -import bisq.common.config.Config; import bisq.common.util.Tuple3; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.SegwitAddress; -import org.bitcoinj.core.Transaction; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import javax.inject.Inject; import javax.inject.Named; - +import monero.wallet.model.MoneroTxConfig; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -85,7 +80,7 @@ import javafx.collections.transformation.SortedList; import javafx.util.Callback; import java.io.ByteArrayInputStream; - +import java.math.BigInteger; import java.util.Comparator; import java.util.concurrent.TimeUnit; @@ -105,17 +100,16 @@ public class DepositView extends ActivatableView<VBox, Void> { private ImageView qrCodeImageView; private AddressTextField addressTextField; private Button generateNewAddressButton; - private CheckBox generateNewAddressSegwitCheckbox; private TitledGroupBg titledGroupBg; private InputTextField amountTextField; - private final BtcWalletService walletService; + private final XmrWalletService xmrWalletService; private final Preferences preferences; private final CoinFormatter formatter; private String paymentLabelString; private final ObservableList<DepositListItem> observableList = FXCollections.observableArrayList(); private final SortedList<DepositListItem> sortedList = new SortedList<>(observableList); - private BalanceListener balanceListener; + private XmrBalanceListener balanceListener; private Subscription amountTextFieldSubscription; private ChangeListener<DepositListItem> tableViewSelectionListener; private int gridRow = 0; @@ -125,10 +119,10 @@ public class DepositView extends ActivatableView<VBox, Void> { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - private DepositView(BtcWalletService walletService, + private DepositView(XmrWalletService xmrWalletService, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { - this.walletService = walletService; + this.xmrWalletService = xmrWalletService; this.preferences = preferences; this.formatter = formatter; } @@ -143,7 +137,7 @@ public class DepositView extends ActivatableView<VBox, Void> { usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage"))); // trigger creation of at least 1 savings address - walletService.getFreshAddressEntry(); + xmrWalletService.getFreshAddressEntry(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses"))); @@ -161,7 +155,7 @@ public class DepositView extends ActivatableView<VBox, Void> { addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsCoin)); - confirmationsColumn.setComparator(Comparator.comparingDouble(o -> o.getTxConfidenceIndicator().getProgress())); + confirmationsColumn.setComparator(Comparator.comparingInt(o -> o.getNumConfirmationsSinceFirstUsed())); usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxOutputs)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); @@ -174,7 +168,7 @@ public class DepositView extends ActivatableView<VBox, Void> { Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( () -> UserThread.runAfter( - () -> new QRCodeWindow(getBitcoinURI()).show(), + () -> new QRCodeWindow(getPaymentUri()).show(), 200, TimeUnit.MILLISECONDS))); GridPane.setRowIndex(qrCodeImageView, gridRow); GridPane.setRowSpan(qrCodeImageView, 4); @@ -201,23 +195,17 @@ public class DepositView extends ActivatableView<VBox, Void> { Tuple3<Button, CheckBox, HBox> buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow, Res.get("funds.deposit.generateAddress"), - Res.get("funds.deposit.generateAddressSegwit"), + null, 15); buttonCheckBoxHBox.third.setSpacing(25); generateNewAddressButton = buttonCheckBoxHBox.first; - generateNewAddressSegwitCheckbox = buttonCheckBoxHBox.second; - generateNewAddressSegwitCheckbox.setAllowIndeterminate(false); - generateNewAddressSegwitCheckbox.setSelected(true); generateNewAddressButton.setOnAction(event -> { - boolean segwit = generateNewAddressSegwitCheckbox.isSelected(); - NetworkParameters params = Config.baseCurrencyNetworkParameters(); - boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getNumTxOutputs() == 0 - && (Address.fromString(params, e.getAddressString()) instanceof SegwitAddress) == segwit); + boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getNumTxOutputs() == 0); if (hasUnUsedAddress) { new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); } else { - AddressEntry newSavingsAddressEntry = walletService.getFreshAddressEntry(segwit); + XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry(); updateList(); observableList.stream() .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) @@ -226,9 +214,9 @@ public class DepositView extends ActivatableView<VBox, Void> { } }); - balanceListener = new BalanceListener() { + balanceListener = new XmrBalanceListener() { @Override - public void onBalanceChanged(Coin balance, Transaction tx) { + public void onBalanceChanged(BigInteger balance) { updateList(); } }; @@ -243,7 +231,7 @@ public class DepositView extends ActivatableView<VBox, Void> { updateList(); - walletService.addBalanceListener(balanceListener); + xmrWalletService.addBalanceListener(balanceListener); amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { addressTextField.setAmountAsCoin(ParsingUtils.parseToCoin(t, formatter)); updateQRCode(); @@ -258,7 +246,7 @@ public class DepositView extends ActivatableView<VBox, Void> { tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener); sortedList.comparatorProperty().unbind(); observableList.forEach(DepositListItem::cleanup); - walletService.removeBalanceListener(balanceListener); + xmrWalletService.removeBalanceListener(balanceListener); amountTextFieldSubscription.unsubscribe(); } @@ -267,7 +255,6 @@ public class DepositView extends ActivatableView<VBox, Void> { // UI handlers /////////////////////////////////////////////////////////////////////////////////////////// - private void fillForm(String address) { titledGroupBg.setVisible(true); titledGroupBg.setManaged(true); @@ -287,7 +274,7 @@ public class DepositView extends ActivatableView<VBox, Void> { private void updateQRCode() { if (addressTextField.getAddress() != null && !addressTextField.getAddress().isEmpty()) { final byte[] imageBytes = QRCode - .from(getBitcoinURI()) + .from(getPaymentUri()) .withSize(150, 150) // code has 41 elements 8 px is border with 150 we get 3x scale and min. border .to(ImageType.PNG) .stream() @@ -309,8 +296,8 @@ public class DepositView extends ActivatableView<VBox, Void> { private void updateList() { observableList.forEach(DepositListItem::cleanup); observableList.clear(); - walletService.getAvailableAddressEntries() - .forEach(e -> observableList.add(new DepositListItem(e, walletService, formatter))); + xmrWalletService.getAvailableAddressEntries() + .forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter))); } private Coin getAmountAsCoin() { @@ -318,10 +305,11 @@ public class DepositView extends ActivatableView<VBox, Void> { } @NotNull - private String getBitcoinURI() { - return GUIUtil.getBitcoinURI(addressTextField.getAddress(), - getAmountAsCoin(), - paymentLabelString); + private String getPaymentUri() { + return xmrWalletService.getWallet().createPaymentUri(new MoneroTxConfig() + .setAddress(addressTextField.getAddress()) + .setAmount(ParsingUtils.coinToAtomicUnits(getAmountAsCoin())) + .setNote(paymentLabelString)); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -434,7 +422,7 @@ public class DepositView extends ActivatableView<VBox, Void> { super.updateItem(item, empty); if (item != null && !empty) { - setGraphic(item.getTxConfidenceIndicator()); + //setGraphic(item.getTxConfidenceIndicator()); } else { setGraphic(null); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java index c0dc9231..33d74992 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java @@ -17,34 +17,34 @@ package bisq.desktop.main.funds.transactions; -import bisq.desktop.components.indicator.TxConfidenceIndicator; - -import bisq.core.btc.listeners.TxConfidenceListener; import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinFormatter; - -import org.bitcoinj.core.Coin; - +import bisq.desktop.components.indicator.TxConfidenceIndicator; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.GUIUtil; +import com.google.common.base.Supplier; import com.google.common.base.Suppliers; - -import javafx.scene.control.Tooltip; - +import java.math.BigInteger; import java.util.Date; -import java.util.function.Supplier; - +import java.util.Optional; +import javafx.scene.control.Tooltip; +import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nullable; - - - +import monero.wallet.model.MoneroIncomingTransfer; +import monero.wallet.model.MoneroOutgoingTransfer; import monero.wallet.model.MoneroTxWallet; +import monero.wallet.model.MoneroWalletListener; +import org.bitcoinj.core.Coin; @Slf4j class TransactionsListItem { - private final XmrWalletService xmrWalletService; private final CoinFormatter formatter; private String dateString; private final Date date; @@ -54,12 +54,11 @@ class TransactionsListItem { private String details = ""; private String addressString = ""; private String direction = ""; - private TxConfidenceListener txConfidenceListener; private boolean received; private boolean detailsAvailable; private Coin amountAsCoin = Coin.ZERO; private String memo = ""; - private int confirmations = 0; + private long confirmations = 0; @Getter private final boolean isDustAttackTx; private boolean initialTxConfidenceVisibility = true; @@ -77,187 +76,140 @@ class TransactionsListItem { // used at exportCSV TransactionsListItem() { date = null; - xmrWalletService = null; txId = null; formatter = null; isDustAttackTx = false; lazyFieldsSupplier = null; } - TransactionsListItem(MoneroTxWallet transaction, + TransactionsListItem(MoneroTxWallet tx, XmrWalletService xmrWalletService, TransactionAwareTradable transactionAwareTradable, CoinFormatter formatter, long ignoreDustThreshold) { - throw new RuntimeException("TransactionsListItem needs updated to use XMR wallet"); -// this.btcWalletService = btcWalletService; -// this.formatter = formatter; -// this.memo = transaction.getMemo(); -// -// txId = transaction.getTxId().toString(); -// -// Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable) -// .map(TransactionAwareTradable::asTradable); -// -// Coin valueSentToMe = btcWalletService.getValueSentToMeForTransaction(transaction); -// Coin valueSentFromMe = btcWalletService.getValueSentFromMeForTransaction(transaction); -// -// // TODO check and refactor -// if (valueSentToMe.isZero()) { -// amountAsCoin = valueSentFromMe.multiply(-1); -// for (TransactionOutput output : transaction.getOutputs()) { -// if (!btcWalletService.isTransactionOutputMine(output)) { -// received = false; -// if (WalletService.isOutputScriptConvertibleToAddress(output)) { -// addressString = WalletService.getAddressStringFromOutput(output); -// direction = Res.get("funds.tx.direction.sentTo"); -// break; -// } -// } -// } -// } else if (valueSentFromMe.isZero()) { -// amountAsCoin = valueSentToMe; -// direction = Res.get("funds.tx.direction.receivedWith"); -// received = true; -// for (TransactionOutput output : transaction.getOutputs()) { -// if (btcWalletService.isTransactionOutputMine(output) && -// WalletService.isOutputScriptConvertibleToAddress(output)) { -// addressString = WalletService.getAddressStringFromOutput(output); -// break; -// } -// } -// } else { -// amountAsCoin = valueSentToMe.subtract(valueSentFromMe); -// boolean outgoing = false; -// for (TransactionOutput output : transaction.getOutputs()) { -// if (!btcWalletService.isTransactionOutputMine(output)) { -// if (WalletService.isOutputScriptConvertibleToAddress(output)) { -// addressString = WalletService.getAddressStringFromOutput(output); -// outgoing = true; -// break; -// } -// } else { -// addressString = WalletService.getAddressStringFromOutput(output); -// outgoing = (valueSentToMe.getValue() < valueSentFromMe.getValue()); -// if (!outgoing) { -// direction = Res.get("funds.tx.direction.receivedWith"); -// received = true; -// } -// } -// } -// -// if (outgoing) { -// direction = Res.get("funds.tx.direction.sentTo"); -// received = false; -// } -// } -// -// -// if (optionalTradable.isPresent()) { -// tradable = optionalTradable.get(); -// detailsAvailable = true; -// String tradeId = tradable.getShortId(); -// if (tradable instanceof OpenOffer) { -// details = Res.get("funds.tx.createOfferFee", tradeId); -// } else if (tradable instanceof Trade) { -// Trade trade = (Trade) tradable; -// TransactionAwareTrade transactionAwareTrade = (TransactionAwareTrade) transactionAwareTradable; -// if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().equals(txId)) { -// details = Res.get("funds.tx.takeOfferFee", tradeId); -// } else { -// Offer offer = trade.getOffer(); -// String offerFeePaymentTxID = offer.getOfferFeePaymentTxId(); -// if (offerFeePaymentTxID != null && offerFeePaymentTxID.equals(txId)) { -// details = Res.get("funds.tx.createOfferFee", tradeId); -// } else if (trade.getDepositTx() != null && -// trade.getDepositTx().getTxId().equals(Sha256Hash.wrap(txId))) { -// details = Res.get("funds.tx.multiSigDeposit", tradeId); -// } else if (trade.getPayoutTx() != null && -// trade.getPayoutTx().getTxId().equals(Sha256Hash.wrap(txId))) { -// details = Res.get("funds.tx.multiSigPayout", tradeId); -// -// if (amountAsCoin.isZero()) { -// initialTxConfidenceVisibility = false; -// } -// } else { -// Trade.DisputeState disputeState = trade.getDisputeState(); -// if (disputeState == Trade.DisputeState.DISPUTE_CLOSED) { -// if (valueSentToMe.isPositive()) { -// details = Res.get("funds.tx.disputePayout", tradeId); -// } else { -// details = Res.get("funds.tx.disputeLost", tradeId); -// initialTxConfidenceVisibility = false; -// } -// } else if (disputeState == Trade.DisputeState.REFUND_REQUEST_CLOSED || -// disputeState == Trade.DisputeState.REFUND_REQUESTED || -// disputeState == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { -// if (valueSentToMe.isPositive()) { -// details = Res.get("funds.tx.refund", tradeId); -// } else { -// // We have spent the deposit tx outputs to the Bisq donation address to enable -// // the refund process (refund agent -> reimbursement). As the funds have left our wallet -// // already when funding the deposit tx we show 0 BTC as amount. -// // Confirmation is not known from the BitcoinJ side (not 100% clear why) as no funds -// // left our wallet nor we received funds. So we set indicator invisible. -// amountAsCoin = Coin.ZERO; -// details = Res.get("funds.tx.collateralForRefund", tradeId); -// initialTxConfidenceVisibility = false; -// } -// } else { -// if (transactionAwareTrade.isDelayedPayoutTx(txId)) { -// details = Res.get("funds.tx.timeLockedPayoutTx", tradeId); -// initialTxConfidenceVisibility = false; -// } else { -// details = Res.get("funds.tx.unknown", tradeId); -// } -// } -// } -// } -// } -// } else { -// if (amountAsCoin.isZero()) { -// details = Res.get("funds.tx.noFundsFromDispute"); -// initialTxConfidenceVisibility = false; -// } -// // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() -// date = transaction.getIncludedInBestChainAt() != null ? transaction.getIncludedInBestChainAt() : transaction.getUpdateTime(); -// dateString = DisplayUtils.formatDateTime(date); -// -// isDustAttackTx = received && valueSentToMe.value < ignoreDustThreshold; -// if (isDustAttackTx) { -// details = Res.get("funds.tx.dustAttackTx"); -// } -// -// // confidence -// lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ -// txConfidenceIndicator = new TxConfidenceIndicator(); -// txConfidenceIndicator.setId("funds-confidence"); -// tooltip = new Tooltip(Res.get("shared.notUsedYet")); -// txConfidenceIndicator.setProgress(0); -// txConfidenceIndicator.setTooltip(tooltip); -// txConfidenceIndicator.setVisible(initialTxConfidenceVisibility); -// -// TransactionConfidence confidence = transaction.getConfidence(); -// GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator); -// confirmations = confidence.getDepthInBlocks(); -// }}); -// -// txConfidenceListener = new TxConfidenceListener(txId) { -// @Override -// public void onTransactionConfidenceChanged(TransactionConfidence confidence) { -// GUIUtil.updateConfidence(confidence, lazy().tooltip, lazy().txConfidenceIndicator); -// confirmations = confidence.getDepthInBlocks(); -// } -// }; -// btcWalletService.addTxConfidenceListener(txConfidenceListener); + this.formatter = formatter; + this.memo = tx.getNote(); + this.txId = tx.getHash(); + + Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable) + .map(TransactionAwareTradable::asTradable); + + Coin valueSentToMe = ParsingUtils.atomicUnitsToCoin(tx.getIncomingAmount() == null ? new BigInteger("0") : tx.getIncomingAmount()); + Coin valueSentFromMe = ParsingUtils.atomicUnitsToCoin(tx.getOutgoingAmount() == null ? new BigInteger("0") : tx.getOutgoingAmount()); + + if (tx.getTransfers().get(0).isIncoming()) { + addressString = ((MoneroIncomingTransfer) tx.getTransfers().get(0)).getAddress(); + } else { + MoneroOutgoingTransfer transfer = (MoneroOutgoingTransfer) tx.getTransfers().get(0); + if (transfer.getDestinations() != null) addressString = transfer.getDestinations().get(0).getAddress(); + else addressString = "unavailable"; + } + + if (valueSentFromMe.isZero()) { + amountAsCoin = valueSentToMe; + direction = Res.get("funds.tx.direction.receivedWith"); + received = true; + } else { + amountAsCoin = valueSentFromMe.multiply(-1); + received = false; + direction = Res.get("funds.tx.direction.sentTo"); + } + + if (optionalTradable.isPresent()) { + tradable = optionalTradable.get(); + detailsAvailable = true; + String tradeId = tradable.getShortId(); + if (tradable instanceof OpenOffer) { + details = Res.get("funds.tx.createOfferFee", tradeId); + } else if (tradable instanceof Trade) { + Trade trade = (Trade) tradable; + if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().equals(txId)) { + details = Res.get("funds.tx.takeOfferFee", tradeId); + } else { + Offer offer = trade.getOffer(); + String offerFeePaymentTxID = offer.getOfferFeePaymentTxId(); + if (offerFeePaymentTxID != null && offerFeePaymentTxID.equals(txId)) { + details = Res.get("funds.tx.createOfferFee", tradeId); + } else if (trade.getSelf().getDepositTxHash() != null && + trade.getSelf().getDepositTxHash().equals(txId)) { + details = Res.get("funds.tx.multiSigDeposit", tradeId); + } else if (trade.getPayoutTxId() != null && + trade.getPayoutTxId().equals(txId)) { + details = Res.get("funds.tx.multiSigPayout", tradeId); + if (amountAsCoin.isZero()) { + initialTxConfidenceVisibility = false; + } + } else { + Trade.DisputeState disputeState = trade.getDisputeState(); + if (disputeState == Trade.DisputeState.DISPUTE_CLOSED) { + if (valueSentToMe.isPositive()) { + details = Res.get("funds.tx.disputePayout", tradeId); + } else { + details = Res.get("funds.tx.disputeLost", tradeId); + } + } else if (disputeState == Trade.DisputeState.REFUND_REQUEST_CLOSED || + disputeState == Trade.DisputeState.REFUND_REQUESTED || + disputeState == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { + if (valueSentToMe.isPositive()) { + details = Res.get("funds.tx.refund", tradeId); + } else { + // We have spent the deposit tx outputs to the Bisq donation address to enable + // the refund process (refund agent -> reimbursement). As the funds have left our wallet + // already when funding the deposit tx we show 0 BTC as amount. + // Confirmation is not known from the BitcoinJ side (not 100% clear why) as no funds + // left our wallet nor we received funds. So we set indicator invisible. + amountAsCoin = Coin.ZERO; + details = Res.get("funds.tx.collateralForRefund", tradeId); + initialTxConfidenceVisibility = false; + } + } else { + details = Res.get("funds.tx.unknown", tradeId); + } + } + } + } + } else { + if (amountAsCoin.isZero()) { + details = Res.get("funds.tx.noFundsFromDispute"); + } + } + + this.date = new Date(0); // TODO: convert height to date + dateString = DisplayUtils.formatDateTime(date); + + isDustAttackTx = received && valueSentToMe.value < ignoreDustThreshold; + if (isDustAttackTx) { + details = Res.get("funds.tx.dustAttackTx"); + } + + // confidence + lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ + txConfidenceIndicator = new TxConfidenceIndicator(); + txConfidenceIndicator.setId("funds-confidence"); + tooltip = new Tooltip(Res.get("shared.notUsedYet")); + txConfidenceIndicator.setProgress(0); + txConfidenceIndicator.setTooltip(tooltip); + txConfidenceIndicator.setVisible(initialTxConfidenceVisibility); + + GUIUtil.updateConfidence(tx, tooltip, txConfidenceIndicator); + confirmations = tx.getNumConfirmations(); + }}); + + // listen for tx updates + // TODO: this only listens for new blocks, listen for double spend + xmrWalletService.addWalletListener(new MoneroWalletListener() { + @Override + public void onNewBlock(long height) { + MoneroTxWallet tx = xmrWalletService.getWallet().getTx(txId); + GUIUtil.updateConfidence(tx, lazy().tooltip, lazy().txConfidenceIndicator); + confirmations = tx.getNumConfirmations(); + } + }); } public void cleanup() { - // TODO (woodser): remove wallet listener - //xmrWalletService.removeTxConfidenceListener(txConfidenceListener); } - public TxConfidenceIndicator getTxConfidenceIndicator() { return lazy().txConfidenceIndicator; } @@ -309,8 +261,8 @@ class TransactionsListItem { return tradable; } - public String getNumConfirmations() { - return String.valueOf(confirmations); + public long getNumConfirmations() { + return confirmations; } public String getMemo() { diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index 2ee13f5f..cbc57d3f 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -29,10 +29,9 @@ import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.util.GUIUtil; import bisq.core.api.CoreMoneroConnectionsService; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; -import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.user.Preferences; @@ -40,13 +39,10 @@ import bisq.network.p2p.P2PService; import bisq.common.util.Utilities; -import org.bitcoinj.core.TransactionConfidence; -import org.bitcoinj.wallet.listeners.WalletChangeEventListener; - import com.googlecode.jcsv.writer.CSVEntryConverter; import javax.inject.Inject; - +import monero.wallet.model.MoneroWalletListener; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.fxml.FXML; @@ -77,11 +73,9 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.util.Callback; - +import java.math.BigInteger; import java.util.Comparator; -import javax.annotation.Nullable; - @FxmlView public class TransactionsView extends ActivatableView<VBox, Void> { @@ -100,33 +94,40 @@ public class TransactionsView extends ActivatableView<VBox, Void> { private final DisplayedTransactions displayedTransactions; private final SortedList<TransactionsListItem> sortedDisplayedTransactions; - private final BtcWalletService btcWalletService; - private final P2PService p2PService; - private final CoreMoneroConnectionsService connectionService; + private final XmrWalletService xmrWalletService; private final Preferences preferences; private final TradeDetailsWindow tradeDetailsWindow; private final OfferDetailsWindow offerDetailsWindow; - private WalletChangeEventListener walletChangeEventListener; - private EventHandler<KeyEvent> keyEventEventHandler; private Scene scene; + private TransactionsUpdater transactionsUpdater = new TransactionsUpdater(); + + private class TransactionsUpdater extends MoneroWalletListener { + @Override + public void onNewBlock(long height) { + displayedTransactions.update(); + } + @Override + public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + displayedTransactions.update(); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject - private TransactionsView(BtcWalletService btcWalletService, + private TransactionsView(XmrWalletService xmrWalletService, P2PService p2PService, CoreMoneroConnectionsService connectionService, Preferences preferences, TradeDetailsWindow tradeDetailsWindow, OfferDetailsWindow offerDetailsWindow, DisplayedTransactionsFactory displayedTransactionsFactory) { - this.btcWalletService = btcWalletService; - this.p2PService = p2PService; - this.connectionService = connectionService; + this.xmrWalletService = xmrWalletService; this.preferences = preferences; this.tradeDetailsWindow = tradeDetailsWindow; this.offerDetailsWindow = offerDetailsWindow; @@ -168,16 +169,12 @@ public class TransactionsView extends ActivatableView<VBox, Void> { addressColumn.setComparator(Comparator.comparing(item -> item.getDirection() + item.getAddressString())); transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId)); amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmountAsCoin)); - confidenceColumn.setComparator(Comparator.comparingDouble(item -> item.getTxConfidenceIndicator().getProgress())); + confidenceColumn.setComparator(Comparator.comparingLong(item -> item.getNumConfirmations())); memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); - walletChangeEventListener = wallet -> { - displayedTransactions.update(); - }; - keyEventEventHandler = event -> { // Not intended to be public to users as the feature is not well tested if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { @@ -202,7 +199,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> { tableView.setItems(sortedDisplayedTransactions); displayedTransactions.update(); - btcWalletService.addChangeEventListener(walletChangeEventListener); + xmrWalletService.addWalletListener(transactionsUpdater); scene = root.getScene(); if (scene != null) @@ -226,7 +223,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> { columns[3] = item.getTxId(); columns[4] = item.getAmount(); columns[5] = item.getMemo() == null ? "" : item.getMemo(); - columns[6] = item.getNumConfirmations(); + columns[6] = String.valueOf(item.getNumConfirmations()); return columns; }; @@ -239,7 +236,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> { protected void deactivate() { sortedDisplayedTransactions.comparatorProperty().unbind(); displayedTransactions.forEach(TransactionsListItem::cleanup); - btcWalletService.removeChangeEventListener(walletChangeEventListener); + xmrWalletService.removeWalletListener(transactionsUpdater); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); @@ -505,49 +502,15 @@ public class TransactionsView extends ActivatableView<VBox, Void> { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - TransactionConfidence confidence = btcWalletService.getConfidenceForTxId(item.getTxId()); - if (confidence != null) { - if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING) { - if (button == null) { - button = new AutoTooltipButton(Res.get("funds.tx.revert")); - setGraphic(button); - } - button.setOnAction(e -> revertTransaction(item.getTxId(), item.getTradable())); - } else { - setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } - } - } - } else { - setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; } } }; } }); } - - private void revertTransaction(String txId, @Nullable Tradable tradable) { - if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) { - try { - btcWalletService.doubleSpendTransaction(txId, () -> { - if (tradable != null) - btcWalletService.swapAnyTradeEntryContextToAvailableEntry(tradable.getId()); - - new Popup().information(Res.get("funds.tx.txSent")).show(); - }, errorMessage -> new Popup().warning(errorMessage).show()); - } catch (Throwable e) { - new Popup().warning(e.getMessage()).show(); - } - } - } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalListItem.java index 9928a6cc..b37d3958 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalListItem.java @@ -23,6 +23,7 @@ import bisq.core.btc.listeners.XmrBalanceListener; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinFormatter; import org.bitcoinj.core.Coin; @@ -71,7 +72,8 @@ class WithdrawalListItem { } private void updateBalance() { - balance = walletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); + balance = walletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); // TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile + balance = Coin.valueOf(ParsingUtils.atomicUnitsToCentineros(balance.longValue())); // in centineros if (balance != null) balanceLabel.setText(formatter.formatCoin(this.balance)); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index e171e536..846b0b5e 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -26,16 +26,20 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.TxDetails; import bisq.desktop.main.overlays.windows.WalletPasswordWindow; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; import bisq.core.btc.listeners.XmrBalanceListener; +import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.provider.fee.FeeService; +import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; +import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; @@ -43,17 +47,15 @@ import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.BtcAddressValidator; import bisq.network.p2p.P2PService; - +import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; -import bisq.common.util.Tuple4; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Transaction; import javax.inject.Inject; import javax.inject.Named; - -import com.google.common.util.concurrent.FutureCallback; +import monero.wallet.model.MoneroTxConfig; +import monero.wallet.model.MoneroTxWallet; import org.apache.commons.lang3.StringUtils; @@ -69,7 +71,6 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.Toggle; -import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; @@ -88,12 +89,12 @@ import javafx.collections.transformation.SortedList; import javafx.util.Callback; -import org.bouncycastle.crypto.params.KeyParameter; import java.math.BigInteger; - +import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -109,36 +110,27 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { @FXML TableColumn<WithdrawalListItem, WithdrawalListItem> addressColumn, balanceColumn, selectColumn; - private RadioButton useAllInputsRadioButton, useCustomInputsRadioButton, feeExcludedRadioButton, feeIncludedRadioButton; + private RadioButton useAllInputsRadioButton, useCustomInputsRadioButton; private Label amountLabel; - private TextField amountTextField, withdrawFromTextField, withdrawToTextField, withdrawMemoTextField, transactionFeeInputTextField; + private TextField amountTextField, withdrawFromTextField, withdrawToTextField, withdrawMemoTextField; private final XmrWalletService xmrWalletService; private final TradeManager tradeManager; private final P2PService p2PService; - private final WalletsSetup walletsSetup; private final CoinFormatter formatter; private final Preferences preferences; - private final BtcAddressValidator btcAddressValidator; - private final WalletPasswordWindow walletPasswordWindow; private final ObservableList<WithdrawalListItem> observableList = FXCollections.observableArrayList(); private final SortedList<WithdrawalListItem> sortedList = new SortedList<>(observableList); private final Set<WithdrawalListItem> selectedItems = new HashSet<>(); private XmrBalanceListener balanceListener; - private Set<String> fromAddresses = new HashSet<>(); private Coin totalAvailableAmountOfSelectedItems = Coin.ZERO; private Coin amountAsCoin = Coin.ZERO; private ChangeListener<String> amountListener; - private ChangeListener<Boolean> amountFocusListener, useCustomFeeCheckboxListener, transactionFeeFocusedListener; - private ChangeListener<Toggle> feeToggleGroupListener, inputsToggleGroupListener; - private ChangeListener<Number> transactionFeeChangeListener; - private ToggleGroup feeToggleGroup, inputsToggleGroup; - private ToggleButton useCustomFee; + private ChangeListener<Boolean> amountFocusListener; + private ChangeListener<Toggle> inputsToggleGroupListener; + private ToggleGroup inputsToggleGroup; private final BooleanProperty useAllInputs = new SimpleBooleanProperty(true); - private boolean feeExcluded; private int rowIndex = 0; - private final FeeService feeService; - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -154,16 +146,11 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { BtcAddressValidator btcAddressValidator, WalletPasswordWindow walletPasswordWindow, FeeService feeService) { -// throw new RuntimeException("WithdrawalView needs updated to use XMR wallet"); this.xmrWalletService = xmrWalletService; this.tradeManager = tradeManager; this.p2PService = p2PService; - this.walletsSetup = walletsSetup; this.formatter = formatter; this.preferences = preferences; - this.btcAddressValidator = btcAddressValidator; - this.walletPasswordWindow = walletPasswordWindow; - this.feeService = feeService; } @Override @@ -189,20 +176,13 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { useAllInputsRadioButton = labelRadioButtonRadioButtonTuple3.second; useCustomInputsRadioButton = labelRadioButtonRadioButtonTuple3.third; - feeToggleGroup = new ToggleGroup(); - - final Tuple4<Label, TextField, RadioButton, RadioButton> feeTuple3 = addTopLabelTextFieldRadioButtonRadioButton(gridPane, ++rowIndex, feeToggleGroup, + final Tuple2<Label, InputTextField> feeTuple3 = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), - "", - Res.get("funds.withdrawal.feeExcluded"), - Res.get("funds.withdrawal.feeIncluded"), 0); amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; amountTextField.setMinWidth(180); - feeExcludedRadioButton = feeTuple3.third; - feeIncludedRadioButton = feeTuple3.fourth; withdrawFromTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.fromLabel", Res.getBaseCurrencyCode())).second; @@ -213,52 +193,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; - Tuple3<Label, InputTextField, ToggleButton> customFeeTuple = addTopLabelInputTextFieldSlideToggleButton(gridPane, ++rowIndex, - Res.get("funds.withdrawal.txFee"), Res.get("funds.withdrawal.useCustomFeeValue")); - transactionFeeInputTextField = customFeeTuple.second; - useCustomFee = customFeeTuple.third; - - useCustomFeeCheckboxListener = (observable, oldValue, newValue) -> { - transactionFeeInputTextField.setEditable(newValue); - if (!newValue) { - try { - transactionFeeInputTextField.setText(String.valueOf(feeService.getTxFeePerVbyte().value)); - } catch (Exception e) { - e.printStackTrace(); - } - } - }; - - transactionFeeFocusedListener = (o, oldValue, newValue) -> { - if (oldValue && !newValue) { - String estimatedFee = String.valueOf(feeService.getTxFeePerVbyte().value); - try { - int withdrawalTxFeePerVbyte = Integer.parseInt(transactionFeeInputTextField.getText()); - final long minFeePerVbyte = feeService.getMinFeePerVByte(); - if (withdrawalTxFeePerVbyte < minFeePerVbyte) { - new Popup().warning(Res.get("funds.withdrawal.txFeeMin", minFeePerVbyte)).show(); - transactionFeeInputTextField.setText(estimatedFee); - } else if (withdrawalTxFeePerVbyte > 5000) { - new Popup().warning(Res.get("funds.withdrawal.txFeeTooLarge")).show(); - transactionFeeInputTextField.setText(estimatedFee); - } else { - preferences.setWithdrawalTxFeeInVbytes(withdrawalTxFeePerVbyte); - } - } catch (NumberFormatException t) { - log.error(t.toString()); - t.printStackTrace(); - new Popup().warning(Res.get("validation.integerOnly")).show(); - transactionFeeInputTextField.setText(estimatedFee); - } catch (Throwable t) { - log.error(t.toString()); - t.printStackTrace(); - new Popup().warning(Res.get("validation.inputError", t.getMessage())).show(); - transactionFeeInputTextField.setText(estimatedFee); - } - } - }; - transactionFeeChangeListener = (observable, oldValue, newValue) -> transactionFeeInputTextField.setText(String.valueOf(feeService.getTxFeePerVbyte().value)); - final Button withdrawButton = addButton(gridPane, ++rowIndex, Res.get("funds.withdrawal.withdrawButton"), 15); withdrawButton.setOnAction(event -> onWithdraw()); @@ -304,14 +238,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { amountTextField.setText(""); } }; - feeExcludedRadioButton.setToggleGroup(feeToggleGroup); - feeIncludedRadioButton.setToggleGroup(feeToggleGroup); - feeToggleGroupListener = (observable, oldValue, newValue) -> { - feeExcluded = newValue == feeExcludedRadioButton; - amountLabel.setText(feeExcluded ? - Res.get("funds.withdrawal.receiverAmount") : - Res.get("funds.withdrawal.senderAmount")); - }; + amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); } private void updateInputSelection() { @@ -333,22 +260,11 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { amountTextField.textProperty().addListener(amountListener); amountTextField.focusedProperty().addListener(amountFocusListener); xmrWalletService.addBalanceListener(balanceListener); - feeToggleGroup.selectedToggleProperty().addListener(feeToggleGroupListener); inputsToggleGroup.selectedToggleProperty().addListener(inputsToggleGroupListener); - if (feeToggleGroup.getSelectedToggle() == null) - feeToggleGroup.selectToggle(feeIncludedRadioButton); - if (inputsToggleGroup.getSelectedToggle() == null) inputsToggleGroup.selectToggle(useAllInputsRadioButton); - useCustomFee.setSelected(false); - transactionFeeInputTextField.setEditable(false); - transactionFeeInputTextField.setText(String.valueOf(feeService.getTxFeePerVbyte().value)); - feeService.feeUpdateCounterProperty().addListener(transactionFeeChangeListener); - useCustomFee.selectedProperty().addListener(useCustomFeeCheckboxListener); - transactionFeeInputTextField.focusedProperty().addListener(transactionFeeFocusedListener); - updateInputSelection(); GUIUtil.requestFocus(withdrawToTextField); } @@ -360,12 +276,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { xmrWalletService.removeBalanceListener(balanceListener); amountTextField.textProperty().removeListener(amountListener); amountTextField.focusedProperty().removeListener(amountFocusListener); - feeToggleGroup.selectedToggleProperty().removeListener(feeToggleGroupListener); inputsToggleGroup.selectedToggleProperty().removeListener(inputsToggleGroupListener); - transactionFeeInputTextField.focusedProperty().removeListener(transactionFeeFocusedListener); - if (transactionFeeChangeListener != null) - feeService.feeUpdateCounterProperty().removeListener(transactionFeeChangeListener); - useCustomFee.selectedProperty().removeListener(useCustomFeeCheckboxListener); } @@ -374,108 +285,72 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { /////////////////////////////////////////////////////////////////////////////////////////// private void onWithdraw() { - throw new RuntimeException("WithdrawalView.onWithdraw() not updated to XMR"); -// if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { -// try { -// final String withdrawToAddress = withdrawToTextField.getText(); -// final Coin sendersAmount; -// -// // We do not know sendersAmount if senderPaysFee is true. We repeat fee calculation after first attempt if senderPaysFee is true. -// Transaction feeEstimationTransaction = btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amountAsCoin); -// if (feeExcluded && feeEstimationTransaction != null) { -// feeEstimationTransaction = btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amountAsCoin.add(feeEstimationTransaction.getFee())); -// } -// checkNotNull(feeEstimationTransaction, "feeEstimationTransaction must not be null"); -// -// Coin dust = btcWalletService.getDust(feeEstimationTransaction); -// Coin fee = feeEstimationTransaction.getFee().add(dust); -// Coin receiverAmount; -// // amountAsCoin is what the user typed into the withdrawal field. -// // this can be interpreted as either the senders amount or receivers amount depending -// // on a radio button "fee excluded / fee included". -// // therefore we calculate the actual sendersAmount and receiverAmount as follows: -// if (feeExcluded) { -// receiverAmount = amountAsCoin; -// sendersAmount = receiverAmount.add(fee); -// } else { -// sendersAmount = amountAsCoin.add(dust); // sendersAmount bumped up to UTXO size when dust is in play -// receiverAmount = sendersAmount.subtract(fee); -// } -// if (dust.isPositive()) { -// log.info("Dust output ({} satoshi) was detected, the dust amount has been added to the fee (was {}, now {})", -// dust.value, -// feeEstimationTransaction.getFee(), -// fee.value); -// } -// -// if (areInputsValid(sendersAmount)) { -// int txVsize = feeEstimationTransaction.getVsize(); -// log.info("Fee for tx with size {}: {} " + Res.getBaseCurrencyCode() + "", txVsize, fee.toPlainString()); -// -// if (receiverAmount.isPositive()) { -// double vkb = txVsize / 1000d; -// -// String messageText = Res.get("shared.sendFundsDetailsWithFee", -// formatter.formatCoinWithCode(sendersAmount), -// withdrawFromTextField.getText(), -// withdrawToAddress, -// formatter.formatCoinWithCode(fee), -// Double.parseDouble(transactionFeeInputTextField.getText()), -// vkb, -// formatter.formatCoinWithCode(receiverAmount)); -// if (dust.isPositive()) { -// messageText = Res.get("shared.sendFundsDetailsDust", -// dust.value, dust.value > 1 ? "s" : "") -// + messageText; -// } -// -// new Popup().headLine(Res.get("funds.withdrawal.confirmWithdrawalRequest")) -// .confirmation(messageText) -// .actionButtonText(Res.get("shared.yes")) -// .onAction(() -> doWithdraw(sendersAmount, fee, new FutureCallback<>() { -// @Override -// public void onSuccess(@javax.annotation.Nullable Transaction transaction) { -// if (transaction != null) { -// String key = "showTransactionSent"; -// if (DontShowAgainLookup.showAgain(key)) { -// new TxDetails(transaction.getTxId().toString(), withdrawToAddress, formatter.formatCoinWithCode(sendersAmount)) -// .dontShowAgainId(key) -// .show(); -// } -// log.debug("onWithdraw onSuccess tx ID:{}", transaction.getTxId().toString()); -// } else { -// log.error("onWithdraw transaction is null"); -// } -// -// List<Trade> trades = new ArrayList<>(tradeManager.getObservableList()); -// trades.stream() -// .filter(Trade::isPayoutPublished) -// .forEach(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT) -// .ifPresent(addressEntry -> { -// if (btcWalletService.getBalanceForAddress(addressEntry.getAddress()).isZero()) -// tradeManager.onTradeCompleted(trade); -// })); -// } -// -// @Override -// public void onFailure(@NotNull Throwable t) { -// log.error("onWithdraw onFailure"); -// } -// })) -// .closeButtonText(Res.get("shared.cancel")) -// .show(); -// } else { -// new Popup().warning(Res.get("portfolio.pending.step5_buyer.amountTooLow")).show(); -// } -// } -// } catch (InsufficientFundsException e) { -// new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds") + "\n\nError message:\n" + e.getMessage()).show(); -// } catch (Throwable e) { -// e.printStackTrace(); -// log.error(e.toString()); -// new Popup().warning(e.toString()).show(); -// } -// } + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, xmrWalletService.getConnectionsService())) { + try { + + // get withdraw address + final String withdrawToAddress = withdrawToTextField.getText(); + + // get receiver amount + Coin receiverAmount = amountAsCoin; + if (!receiverAmount.isPositive()) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); + + // create tx + MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() + .setAccountIndex(0) + .setAmount(ParsingUtils.coinToAtomicUnits(receiverAmount)) // TODO: rename to centinerosToAtomicUnits()? + .setAddress(withdrawToAddress)); + + // create confirmation message + Coin fee = ParsingUtils.atomicUnitsToCoin(tx.getFee()); + Coin sendersAmount = receiverAmount.add(fee); + String messageText = Res.get("shared.sendFundsDetailsWithFee", + formatter.formatCoinWithCode(sendersAmount), + withdrawFromTextField.getText(), + withdrawToAddress, + formatter.formatCoinWithCode(fee), + formatter.formatCoinWithCode(receiverAmount)); + + // popup confirmation message + new Popup().headLine(Res.get("funds.withdrawal.confirmWithdrawalRequest")) + .confirmation(messageText) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + + // relay tx + try { + xmrWalletService.getWallet().relayTx(tx); + String key = "showTransactionSent"; + if (DontShowAgainLookup.showAgain(key)) { + new TxDetails(tx.getHash(), withdrawToAddress, formatter.formatCoinWithCode(sendersAmount)) + .dontShowAgainId(key) + .show(); + } + log.debug("onWithdraw onSuccess tx ID:{}", tx.getHash()); + + List<Trade> trades = new ArrayList<>(tradeManager.getObservableList()); + trades.stream() + .filter(Trade::isPayoutPublished) + .forEach(trade -> xmrWalletService.getAddressEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT) + .ifPresent(addressEntry -> { + if (xmrWalletService.getBalanceForAddress(addressEntry.getAddressString()).isZero()) + tradeManager.onTradeCompleted(trade); + })); + } catch (Exception e) { + e.printStackTrace(); + } + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } catch (Throwable e) { + if (e.getMessage().contains("enough")) new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds") + "\n\nError message:\n" + e.getMessage()).show(); + else { + e.printStackTrace(); + log.error(e.toString()); + new Popup().warning(e.toString()).show(); + } + } + } } private void selectForWithdrawal(WithdrawalListItem item) { @@ -484,10 +359,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { else selectedItems.remove(item); - fromAddresses = selectedItems.stream() - .map(WithdrawalListItem::getAddressString) - .collect(Collectors.toSet()); - if (!selectedItems.isEmpty()) { totalAvailableAmountOfSelectedItems = Coin.valueOf(selectedItems.stream().mapToLong(e -> e.getBalance().getValue()).sum()); if (totalAvailableAmountOfSelectedItems.isPositive()) { @@ -533,7 +404,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { - //throw new RuntimeException("WithdrawalView.updateList() needs updated to use XMR"); observableList.forEach(WithdrawalListItem::cleanup); observableList.setAll(xmrWalletService.getAddressEntriesForAvailableBalanceStream() .map(addressEntry -> new WithdrawalListItem(addressEntry, xmrWalletService, formatter)) @@ -542,51 +412,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { updateInputSelection(); } - private void doWithdraw(Coin amount, Coin fee, FutureCallback<Transaction> callback) { - throw new RuntimeException("WithdrawalView.doWithdraw() not updated to XMR"); -// if (xmrWalletService.isEncrypted()) { -// UserThread.runAfter(() -> walletPasswordWindow.onAesKey(aesKey -> -// sendFunds(amount, fee, aesKey, callback)) -// .show(), 300, TimeUnit.MILLISECONDS); -// } else { -// sendFunds(amount, fee, null, callback); -// } - } - - private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallback<Transaction> callback) { - throw new RuntimeException("WithdrawalView.sendFunds() not updated to XMR"); -// try { -// String memo = withdrawMemoTextField.getText(); -// if (memo.isEmpty()) { -// memo = null; -// } -// Transaction transaction = btcWalletService.sendFundsForMultipleAddresses(fromAddresses, -// withdrawToTextField.getText(), -// amount, -// fee, -// null, -// aesKey, -// memo, -// callback); -// -// reset(); -// updateList(); -// } catch (AddressFormatException e) { -// new Popup().warning(Res.get("validation.btc.invalidAddress")).show(); -// } catch (Wallet.DustySendRequested e) { -// new Popup().warning(Res.get("validation.amountBelowDust", -// formatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))).show(); -// } catch (AddressEntryException e) { -// new Popup().error(e.getMessage()).show(); -// } catch (InsufficientMoneyException e) { -// log.warn(e.getMessage()); -// new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds") + "\n\nError message:\n" + e.getMessage()).show(); -// } catch (Throwable e) { -// log.warn(e.toString()); -// new Popup().warning(e.toString()).show(); -// } - } - private void reset() { withdrawFromTextField.setText(""); withdrawFromTextField.setPromptText(Res.get("funds.withdrawal.selectAddress")); @@ -603,37 +428,10 @@ public class WithdrawalView extends ActivatableView<VBox, Void> { withdrawMemoTextField.setText(""); withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo")); - transactionFeeInputTextField.setText(""); - transactionFeeInputTextField.setPromptText(Res.get("funds.withdrawal.useCustomFeeValueInfo")); - selectedItems.clear(); tableView.getSelectionModel().clearSelection(); } - private boolean areInputsValid(Coin sendersAmount) { - if (!sendersAmount.isPositive()) { - new Popup().warning(Res.get("validation.negative")).show(); - return false; - } - - if (!btcAddressValidator.validate(withdrawToTextField.getText()).isValid) { - new Popup().warning(Res.get("validation.btc.invalidAddress")).show(); - return false; - } - if (!totalAvailableAmountOfSelectedItems.isPositive()) { - new Popup().warning(Res.get("funds.withdrawal.warn.noSourceAddressSelected")).show(); - return false; - } - - if (sendersAmount.compareTo(totalAvailableAmountOfSelectedItems) > 0) { - new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds")).show(); - return false; - } - - return true; - } - - /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java index 5b6df5af..c1c627f0 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java @@ -21,7 +21,7 @@ import bisq.desktop.components.TxIdTextField; import bisq.desktop.main.shared.PriceFeedComboBoxItem; import bisq.desktop.util.GUIUtil; -import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; @@ -86,7 +86,7 @@ public class MarketPricePresentation { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public MarketPricePresentation(BtcWalletService btcWalletService, + public MarketPricePresentation(XmrWalletService xmrWalletService, PriceFeedService priceFeedService, Preferences preferences, FeeService feeService) { @@ -96,7 +96,7 @@ public class MarketPricePresentation { TxIdTextField.setPreferences(preferences); // TODO - TxIdTextField.setWalletService(btcWalletService); + TxIdTextField.setXmrWalletService(xmrWalletService); GUIUtil.setFeeService(feeService); } diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 79cea95d..229f53fb 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -1047,11 +1047,12 @@ public class FormBuilder { String checkBoxTitle, double top) { Button button = new AutoTooltipButton(buttonTitle); - CheckBox checkBox = new AutoTooltipCheckBox(checkBoxTitle); + CheckBox checkBox = checkBoxTitle == null ? null : new AutoTooltipCheckBox(checkBoxTitle); HBox hBox = new HBox(20); hBox.setAlignment(Pos.CENTER_LEFT); - hBox.getChildren().addAll(button, checkBox); + hBox.getChildren().add(button); + if (checkBox != null) hBox.getChildren().add(button); GridPane.setRowIndex(hBox, rowIndex); hBox.setPadding(new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 10b1be3f..bf5ad296 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -32,6 +32,7 @@ import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.app.HavenoSetup; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; @@ -134,7 +135,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; - +import monero.wallet.model.MoneroTxWallet; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; @@ -566,34 +567,28 @@ public class GUIUtil { }; } - public static void updateConfidence(TransactionConfidence confidence, + public static void updateConfidence(MoneroTxWallet tx, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { - if (confidence != null) { - switch (confidence.getConfidenceType()) { - case UNKNOWN: - tooltip.setText(Res.get("confidence.unknown")); - txConfidenceIndicator.setProgress(0); - break; - case PENDING: - tooltip.setText(Res.get("confidence.seen", confidence.numBroadcastPeers())); - txConfidenceIndicator.setProgress(-1.0); - break; - case BUILDING: - tooltip.setText(Res.get("confidence.confirmed", confidence.getDepthInBlocks())); - txConfidenceIndicator.setProgress(Math.min(1, confidence.getDepthInBlocks() / 6.0)); - break; - case DEAD: - tooltip.setText(Res.get("confidence.invalid")); - txConfidenceIndicator.setProgress(0); - break; + if (tx != null) { + if (!tx.isRelayed()) { + tooltip.setText(Res.get("confidence.unknown")); + txConfidenceIndicator.setProgress(0); + } else if (tx.isFailed()) { + tooltip.setText(Res.get("confidence.invalid")); + txConfidenceIndicator.setProgress(0); + } else if (tx.isConfirmed()) { + tooltip.setText(Res.get("confidence.confirmed", tx.getNumConfirmations())); + txConfidenceIndicator.setProgress(Math.min(1, tx.getNumConfirmations() / (double) XmrWalletService.NUM_BLOCKS_UNLOCK)); + } else { + tooltip.setText(Res.get("confidence.seen", 0)); // TODO: replace with numBroadcastPeers + txConfidenceIndicator.setProgress(-1.0); } txConfidenceIndicator.setPrefSize(24, 24); } } - public static void openWebPage(String target) { openWebPage(target, true, null); }