diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java index 9f64a2efe8..106dbbd807 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java @@ -43,7 +43,8 @@ public final class XmrAddressEntry implements PersistablePayload { OFFER_FUNDING, RESERVED_FOR_TRADE, MULTI_SIG, - TRADE_PAYOUT + TRADE_PAYOUT, + BASE_ADDRESS } // keyPair can be null in case the object is created from deserialization as it is transient. diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 6c864adb25..95d5866c3d 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -39,6 +39,7 @@ import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroCheckTx; import monero.wallet.model.MoneroDestination; +import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; @@ -488,7 +489,7 @@ public class XmrWalletService { daemon.flushTxPool(txHash); // flush tx from pool } catch (MoneroRpcError err) { System.out.println(daemon.getRpcConnection()); - throw err.getCode() == -32601 ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; + throw err.getCode().equals(-32601) ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; } } return new Tuple2<>(tx, actualSecurityDeposit); @@ -838,24 +839,29 @@ public class XmrWalletService { // ----------------------------- LEGACY APP ------------------------------- public synchronized XmrAddressEntry getNewAddressEntry() { - return getNewAddressEntry(XmrAddressEntry.Context.AVAILABLE); + return getNewAddressEntryAux(null, XmrAddressEntry.Context.AVAILABLE); } public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { // try to use available and not yet used entries try { - List<MoneroTxWallet> incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress + List<MoneroTxWallet> incomingTxs = getTxsWithIncomingOutputs(); // prefetch all incoming txs to avoid query per subaddress Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny(); if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); } catch (Exception e) { - log.warn("Error getting new address entriy based on incoming transactions"); + log.warn("Error getting new address entry based on incoming transactions"); e.printStackTrace(); } - // create new subaddress and entry + // create new entry + return getNewAddressEntryAux(offerId, context); + } + + private XmrAddressEntry getNewAddressEntryAux(String offerId, XmrAddressEntry.Context context) { MoneroSubaddress subaddress = wallet.createSubaddress(0); XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); + log.info("Add new XmrAddressEntry {}", entry); xmrAddressEntryList.addAddressEntry(entry); return entry; } @@ -866,6 +872,12 @@ public class XmrWalletService { else return unusedAddressEntries.get(0); } + public synchronized XmrAddressEntry getFreshAddressEntry(List<MoneroTxWallet> cachedTxs) { + List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries(cachedTxs); + if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); + else return unusedAddressEntries.get(0); + } + public synchronized XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE); if (!available.isPresent()) return null; @@ -883,7 +895,7 @@ public class XmrWalletService { Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .findAny(); - return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntry(context); + return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntryAux(null, context); } public synchronized Optional<XmrAddressEntry> getAddressEntry(String offerId, XmrAddressEntry.Context context) { @@ -923,14 +935,6 @@ public class XmrWalletService { swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } - private XmrAddressEntry getNewAddressEntry(XmrAddressEntry.Context context) { - MoneroSubaddress subaddress = wallet.createSubaddress(0); - XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, null, null); - log.info("getOrCreateAddressEntry: add new XmrAddressEntry {}", entry); - xmrAddressEntryList.addAddressEntry(entry); - return entry; - } - private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny(); } @@ -961,13 +965,25 @@ public class XmrWalletService { } public List<XmrAddressEntry> getAddressEntryListAsImmutableList() { + List<MoneroSubaddress> subaddresses = wallet.getSubaddresses(0); + for (MoneroSubaddress subaddress : subaddresses) { + boolean exists = xmrAddressEntryList.getAddressEntriesAsListImmutable().stream().filter(addressEntry -> addressEntry.getAddressString().equals(subaddress.getAddress())).findAny().isPresent(); + if (!exists) { + XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), subaddress.getIndex() == 0 ? XmrAddressEntry.Context.BASE_ADDRESS : XmrAddressEntry.Context.AVAILABLE, null, null); + log.info("Add XmrAddressEntry from existing subaddress: {}", entry); + xmrAddressEntryList.addAddressEntry(entry); + } + } return xmrAddressEntryList.getAddressEntriesAsListImmutable(); } public List<XmrAddressEntry> getUnusedAddressEntries() { - List<MoneroTxWallet> incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress + return getUnusedAddressEntries(getTxsWithIncomingOutputs()); + } + + public List<XmrAddressEntry> getUnusedAddressEntries(List<MoneroTxWallet> cachedTxs) { return getAvailableAddressEntries().stream() - .filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)) + .filter(e -> isSubaddressUnused(e.getSubaddressIndex(), cachedTxs)) .collect(Collectors.toList()); } @@ -976,34 +992,55 @@ public class XmrWalletService { } private boolean isSubaddressUnused(int subaddressIndex, List<MoneroTxWallet> incomingTxs) { - return getNumTxOutputsForSubaddress(subaddressIndex, incomingTxs) == 0; + return getNumOutputsForSubaddress(subaddressIndex, incomingTxs) == 0; } - public int getNumTxOutputsForSubaddress(int subaddressIndex) { - return getNumTxOutputsForSubaddress(subaddressIndex, null); + public int getNumOutputsForSubaddress(int subaddressIndex) { + return getNumOutputsForSubaddress(subaddressIndex, null); } - public int getNumTxOutputsForSubaddress(int subaddressIndex, List<MoneroTxWallet> incomingTxs) { - if (incomingTxs == null) incomingTxs = getIncomingTxs(subaddressIndex); + public int getNumOutputsForSubaddress(int subaddressIndex, List<MoneroTxWallet> incomingTxs) { + if (incomingTxs == null) incomingTxs = getTxsWithIncomingOutputs(subaddressIndex); int numUnspentOutputs = 0; for (MoneroTxWallet tx : incomingTxs) { if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; - numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs + numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs } return numUnspentOutputs; } - public List<MoneroTxWallet> getIncomingTxs() { - return getIncomingTxs(null); + public List<MoneroTxWallet> getTxsWithIncomingOutputs() { + return getTxsWithIncomingOutputs(null); } - public List<MoneroTxWallet> getIncomingTxs(Integer subaddressIndex) { - return wallet.getTxs(new MoneroTxQuery() - .setTransferQuery((new MoneroTransferQuery() - .setAccountIndex(0) - .setSubaddressIndex(subaddressIndex) - .setIsIncoming(true))) - .setIncludeOutputs(true)); + public List<MoneroTxWallet> getTxsWithIncomingOutputs(Integer subaddressIndex) { + List<MoneroTxWallet> txs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + return getTxsWithIncomingOutputs(txs, subaddressIndex); + } + + public static List<MoneroTxWallet> getTxsWithIncomingOutputs(List<MoneroTxWallet> txs, Integer subaddressIndex) { + List<MoneroTxWallet> incomingTxs = new ArrayList<>(); + for (MoneroTxWallet tx : txs) { + boolean isIncoming = false; + if (tx.getIncomingTransfers() != null) { + for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { + if (transfer.getAccountIndex().equals(0) && (subaddressIndex == null || transfer.getSubaddressIndex().equals(subaddressIndex))) { + isIncoming = true; + break; + } + } + } + if (tx.getOutputs() != null && !isIncoming) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (output.getAccountIndex().equals(0) && (subaddressIndex == null || output.getSubaddressIndex().equals(subaddressIndex))) { + isIncoming = true; + break; + } + } + } + if (isIncoming) incomingTxs.add(tx); + } + return incomingTxs; } public BigInteger getBalanceForAddress(String address) { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java index c4c9036c35..249ebac4b5 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java @@ -20,7 +20,6 @@ package haveno.desktop.main.funds.deposit; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import common.types.Filter; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; @@ -34,8 +33,6 @@ import javafx.beans.property.StringProperty; import javafx.scene.control.Tooltip; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroTx; -import monero.wallet.model.MoneroTransferQuery; -import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @@ -49,7 +46,7 @@ class DepositListItem { private BigInteger balanceAsBI; private String usage = "-"; private XmrBalanceListener balanceListener; - private int numTxOutputs = 0; + private int numTxsWithOutputs = 0; private final Supplier<LazyFields> lazyFieldsSupplier; private static class LazyFields { @@ -98,8 +95,8 @@ class DepositListItem { } private void updateUsage(int subaddressIndex, List<MoneroTxWallet> cachedTxs) { - numTxOutputs = xmrWalletService.getNumTxOutputsForSubaddress(addressEntry.getSubaddressIndex(), cachedTxs); - usage = numTxOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxOutputs); + numTxsWithOutputs = XmrWalletService.getTxsWithIncomingOutputs(cachedTxs, addressEntry.getSubaddressIndex()).size(); + usage = subaddressIndex == 0 ? "Base address" : numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs); } public void cleanup() { @@ -114,6 +111,10 @@ class DepositListItem { return addressEntry.getAddressString(); } + public int getSubaddressIndex() { + return addressEntry.getSubaddressIndex(); + } + public String getUsage() { return usage; } @@ -130,8 +131,8 @@ class DepositListItem { return balanceAsBI; } - public int getNumTxOutputs() { - return numTxOutputs; + public int getNumTxsWithOutputs() { + return numTxsWithOutputs; } public long getNumConfirmationsSinceFirstUsed(List<MoneroTxWallet> incomingTxs) { @@ -139,14 +140,10 @@ class DepositListItem { return tx == null ? 0 : tx.getNumConfirmations(); } - private MoneroTxWallet getTxWithFewestConfirmations(List<MoneroTxWallet> incomingTxs) { + private MoneroTxWallet getTxWithFewestConfirmations(List<MoneroTxWallet> allIncomingTxs) { - // get txs with incoming transfers to subaddress - MoneroTxQuery query = new MoneroTxQuery() - .setTransferQuery(new MoneroTransferQuery() - .setIsIncoming(true) - .setSubaddressIndex(addressEntry.getSubaddressIndex())); - List<MoneroTxWallet> txs = incomingTxs == null ? xmrWalletService.getWallet().getTxs(query) : Filter.apply(query, incomingTxs); + // get txs with incoming outputs to subaddress index + List<MoneroTxWallet> txs = XmrWalletService.getTxsWithIncomingOutputs(allIncomingTxs, addressEntry.getSubaddressIndex()); // get tx with fewest confirmations MoneroTxWallet highestTx = null; diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index b9651b940c..c7f680561f 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -62,6 +62,7 @@ import javafx.scene.layout.VBox; import javafx.util.Callback; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; +import monero.wallet.model.MoneroWalletListener; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.bitcoinj.core.Coin; @@ -104,9 +105,11 @@ public class DepositView extends ActivatableView<VBox, Void> { private final ObservableList<DepositListItem> observableList = FXCollections.observableArrayList(); private final SortedList<DepositListItem> sortedList = new SortedList<>(observableList); private XmrBalanceListener balanceListener; + private MoneroWalletListener walletListener; private Subscription amountTextFieldSubscription; private ChangeListener<DepositListItem> tableViewSelectionListener; private int gridRow = 0; + List<MoneroTxWallet> txsWithIncomingOutputs; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -130,8 +133,11 @@ public class DepositView extends ActivatableView<VBox, Void> { confirmationsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations"))); usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage"))); - // trigger creation of at least 1 savings address - xmrWalletService.getFreshAddressEntry(); + // prefetch all incoming txs to avoid query per subaddress + txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs(); + + // trigger creation of at least 1 address + xmrWalletService.getFreshAddressEntry(txsWithIncomingOutputs); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses"))); @@ -147,13 +153,10 @@ public class DepositView extends ActivatableView<VBox, Void> { setUsageColumnCellFactory(); setConfidenceColumnCellFactory(); - // prefetch all incoming txs to avoid query per subaddress - List<MoneroTxWallet> incomingTxs = xmrWalletService.getIncomingTxs(); - addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); - confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(incomingTxs))); - usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxOutputs)); + confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(txsWithIncomingOutputs))); + usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxsWithOutputs)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); @@ -199,7 +202,7 @@ public class DepositView extends ActivatableView<VBox, Void> { generateNewAddressButton = buttonCheckBoxHBox.first; generateNewAddressButton.setOnAction(event -> { - boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getNumTxOutputs() == 0); + boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getSubaddressIndex() != 0 && xmrWalletService.getTxsWithIncomingOutputs(e.getSubaddressIndex()).isEmpty()); if (hasUnUsedAddress) { new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); } else { @@ -219,6 +222,13 @@ public class DepositView extends ActivatableView<VBox, Void> { } }; + walletListener = new MoneroWalletListener() { + @Override + public void onNewBlock(long height) { + updateList(); + } + }; + GUIUtil.focusWhenAddedToScene(amountTextField); } @@ -230,6 +240,8 @@ public class DepositView extends ActivatableView<VBox, Void> { updateList(); xmrWalletService.addBalanceListener(balanceListener); + xmrWalletService.addWalletListener(walletListener); + amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { addressTextField.setAmount(HavenoUtils.parseXmr(t)); updateQRCode(); @@ -245,6 +257,7 @@ public class DepositView extends ActivatableView<VBox, Void> { sortedList.comparatorProperty().unbind(); observableList.forEach(DepositListItem::cleanup); xmrWalletService.removeBalanceListener(balanceListener); + xmrWalletService.removeWalletListener(walletListener); amountTextFieldSubscription.unsubscribe(); } @@ -295,9 +308,14 @@ public class DepositView extends ActivatableView<VBox, Void> { observableList.forEach(DepositListItem::cleanup); observableList.clear(); - List<MoneroTxWallet> incomingTxs = xmrWalletService.getIncomingTxs(); // cache incoming txs for performance + // cache incoming txs + txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs(); + + // add available address entries and base address xmrWalletService.getAvailableAddressEntries() - .forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, incomingTxs))); + .forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs))); + xmrWalletService.getAddressEntries(XmrAddressEntry.Context.BASE_ADDRESS) + .forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs))); } private Coin getAmount() { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index b5bbf530bf..6cfa4c57bd 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1308,6 +1308,7 @@ message XmrAddressEntry { RESERVED_FOR_TRADE = 4; MULTI_SIG = 5; TRADE_PAYOUT = 6; + BASE_ADDRESS = 7; } int32 subaddress_index = 7;