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 38913e6a54..2404cb33a3 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -155,6 +155,10 @@ public class XmrWalletService { private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private Long syncStartHeight = null; private TaskLooper syncLooper = null; + private BigInteger cachedBalance = null; + private BigInteger cachedAvailableBalance = null; + private List cachedSubaddresses; + private List cachedTxs; @Inject XmrWalletService(User user, @@ -439,7 +443,6 @@ public class XmrWalletService { if (!unreservedFrozenKeyImages.isEmpty()) { log.warn("Thawing outputs which are not reserved for offer or trade: " + unreservedFrozenKeyImages); thawOutputs(unreservedFrozenKeyImages); - saveMainWallet(); } } } @@ -452,6 +455,8 @@ public class XmrWalletService { public void freezeOutputs(Collection keyImages) { synchronized (walletLock) { for (String keyImage : keyImages) wallet.freezeOutput(keyImage); + saveMainWallet(); + cacheWalletState(); } updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } @@ -464,6 +469,8 @@ public class XmrWalletService { public void thawOutputs(Collection keyImages) { synchronized (walletLock) { for (String keyImage : keyImages) wallet.thawOutput(keyImage); + saveMainWallet(); + cacheWalletState(); } updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } @@ -845,11 +852,12 @@ public class XmrWalletService { long time = System.currentTimeMillis(); syncWalletWithProgress(); // blocking log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); + cacheWalletState(); // log wallet balances if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { - BigInteger balance = wallet.getBalance(); - BigInteger unlockedBalance = wallet.getUnlockedBalance(); + BigInteger balance = getBalance(); + BigInteger unlockedBalance = getAvailableBalance(); log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); } @@ -1118,7 +1126,7 @@ public class XmrWalletService { // try to use available and not yet used entries try { - List unusedAddressEntries = getUnusedAddressEntries(getTxsWithIncomingOutputs(), wallet.getSubaddresses(0)); + List unusedAddressEntries = getUnusedAddressEntries(); if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId); } catch (Exception e) { log.warn("Error getting new address entry based on incoming transactions"); @@ -1131,6 +1139,7 @@ public class XmrWalletService { private XmrAddressEntry getNewAddressEntryAux(String offerId, XmrAddressEntry.Context context) { MoneroSubaddress subaddress = wallet.createSubaddress(0); + cacheWalletState(); XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); log.info("Add new XmrAddressEntry {}", entry); xmrAddressEntryList.addAddressEntry(entry); @@ -1143,12 +1152,6 @@ public class XmrWalletService { else return unusedAddressEntries.get(0); } - public synchronized XmrAddressEntry getFreshAddressEntry(List cachedTxs, List cachedSubaddresses) { - List unusedAddressEntries = getUnusedAddressEntries(cachedTxs, cachedSubaddresses); - 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; @@ -1228,15 +1231,13 @@ public class XmrWalletService { public List getFundedAvailableAddressEntries() { synchronized (walletLock) { - List subaddresses = wallet.getSubaddresses(0); - return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex(), subaddresses).compareTo(BigInteger.ZERO) > 0).collect(Collectors.toList()); + return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0).collect(Collectors.toList()); } } public List getAddressEntryListAsImmutableList() { synchronized (walletLock) { - List subaddresses = wallet.getSubaddresses(0); - for (MoneroSubaddress subaddress : subaddresses) { + for (MoneroSubaddress subaddress : cachedSubaddresses) { 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); @@ -1249,46 +1250,37 @@ public class XmrWalletService { public List getUnusedAddressEntries() { synchronized (walletLock) { - return getUnusedAddressEntries(getTxsWithIncomingOutputs(), wallet.getSubaddresses(0)); + return getAvailableAddressEntries().stream() + .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex())) + .collect(Collectors.toList()); } } - public List getUnusedAddressEntries(List cachedTxs, List cachedSubaddresses) { - return getAvailableAddressEntries().stream() - .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex(), cachedTxs, cachedSubaddresses)) - .collect(Collectors.toList()); - } - public boolean subaddressHasIncomingTransfers(int subaddressIndex) { - return subaddressHasIncomingTransfers(subaddressIndex, null, null); + return getNumOutputsForSubaddress(subaddressIndex) > 0; } - private boolean subaddressHasIncomingTransfers(int subaddressIndex, List incomingTxs, List subaddresses) { - return getNumOutputsForSubaddress(subaddressIndex, incomingTxs, subaddresses) > 0; - } - - public int getNumOutputsForSubaddress(int subaddressIndex, List incomingTxs, List subaddresses) { - incomingTxs = getTxsWithIncomingOutputs(subaddressIndex, incomingTxs); + public int getNumOutputsForSubaddress(int subaddressIndex) { int numUnspentOutputs = 0; - for (MoneroTxWallet tx : incomingTxs) { + for (MoneroTxWallet tx : cachedTxs) { //if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs } - boolean positiveBalance = getSubaddress(subaddresses, subaddressIndex).getBalance().compareTo(BigInteger.ZERO) > 0; + boolean positiveBalance = getSubaddress(subaddressIndex).getBalance().compareTo(BigInteger.ZERO) > 0; if (positiveBalance && numUnspentOutputs == 0) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance return numUnspentOutputs; } - private MoneroSubaddress getSubaddress(Collection subaddresses, int subaddressIndex) { - for (MoneroSubaddress subaddress : subaddresses) { + private MoneroSubaddress getSubaddress(int subaddressIndex) { + for (MoneroSubaddress subaddress : cachedSubaddresses) { if (subaddress.getIndex() == subaddressIndex) return subaddress; } return null; } - public int getNumTxsWithIncomingOutputs(int subaddressIndex, List txs, List subaddresses) { - List txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex, txs); - if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex, txsWithIncomingOutputs, subaddresses)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance + public int getNumTxsWithIncomingOutputs(int subaddressIndex) { + List txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex); + if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance return txsWithIncomingOutputs.size(); } @@ -1297,13 +1289,8 @@ public class XmrWalletService { } public List getTxsWithIncomingOutputs(Integer subaddressIndex) { - return getTxsWithIncomingOutputs(subaddressIndex, null); - } - - public List getTxsWithIncomingOutputs(Integer subaddressIndex, List txs) { - if (txs == null) txs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); List incomingTxs = new ArrayList<>(); - for (MoneroTxWallet tx : txs) { + for (MoneroTxWallet tx : cachedTxs) { boolean isIncoming = false; if (tx.getIncomingTransfers() != null) { for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { @@ -1331,35 +1318,23 @@ public class XmrWalletService { } public BigInteger getBalanceForSubaddress(int subaddressIndex) { - synchronized (walletLock) { - return wallet.getBalance(0, subaddressIndex); - } - } - - public BigInteger getBalanceForSubaddress(int subaddressIndex, Collection subaddresses) { - if (subaddresses == null) return getBalanceForSubaddress(subaddressIndex); - return getSubaddress(subaddresses, subaddressIndex).getBalance(); + return getSubaddress(subaddressIndex).getBalance(); } public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { - synchronized (walletLock) { - if (wallet == null) throw new IllegalStateException("Cannot get available balance for subaddress because main wallet is null"); - return wallet.getUnlockedBalance(0, subaddressIndex); - } + return getSubaddress(subaddressIndex).getUnlockedBalance(); } public BigInteger getBalance() { - synchronized (walletLock) { - if (wallet == null) throw new IllegalStateException("Cannot get balance because main wallet is null"); - return wallet.getBalance(0); - } + return cachedBalance; } public BigInteger getAvailableBalance() { - synchronized (walletLock) { - if (wallet == null) throw new IllegalStateException("Cannot get available balance because main wallet is null"); - return wallet.getUnlockedBalance(0); - } + return cachedAvailableBalance; + } + + public List getSubaddresses() { + return cachedSubaddresses; } public Stream getAddressEntriesForAvailableBalanceStream() { @@ -1368,8 +1343,7 @@ public class XmrWalletService { available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent())); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); - List subaddresses = wallet.getSubaddresses(0); - return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex(), subaddresses).compareTo(BigInteger.ZERO) > 0); + return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } } @@ -1417,7 +1391,8 @@ public class XmrWalletService { } public List getTransactions(boolean includeDead) { - return wallet.getTxs(new MoneroTxQuery().setIsFailed(includeDead ? null : false)); + if (includeDead) return cachedTxs; + return cachedTxs.stream().filter(tx -> !tx.isFailed()).collect(Collectors.toList()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1445,6 +1420,13 @@ public class XmrWalletService { // -------------------------------- HELPERS ------------------------------- + private void cacheWalletState() { + cachedBalance = wallet.getBalance(0); + cachedAvailableBalance = wallet.getUnlockedBalance(0); + cachedSubaddresses = wallet.getSubaddresses(0); + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + } + /** * Relays wallet notifications to external listeners. */ @@ -1457,6 +1439,7 @@ public class XmrWalletService { @Override public void onNewBlock(long height) { + cacheWalletState(); UserThread.execute(() -> { walletHeight.set(height); for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onNewBlock(height)); @@ -1465,6 +1448,7 @@ public class XmrWalletService { @Override public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + cacheWalletState(); updateBalanceListeners(); for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance)); } 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 89057fc016..180b883d61 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 @@ -32,7 +32,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.MoneroSubaddress; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @@ -57,14 +56,14 @@ class DepositListItem { return lazyFieldsSupplier.get(); } - DepositListItem(XmrAddressEntry addressEntry, XmrWalletService xmrWalletService, CoinFormatter formatter, List cachedTxs, List cachedSubaddresses) { + DepositListItem(XmrAddressEntry addressEntry, XmrWalletService xmrWalletService, CoinFormatter formatter) { this.xmrWalletService = xmrWalletService; this.addressEntry = addressEntry; - balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex(), cachedSubaddresses); + balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); balance.set(HavenoUtils.formatXmr(balanceAsBI)); - updateUsage(addressEntry.getSubaddressIndex(), cachedTxs, cachedSubaddresses); + updateUsage(addressEntry.getSubaddressIndex()); // confidence lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ @@ -73,7 +72,7 @@ class DepositListItem { tooltip = new Tooltip(Res.get("shared.notUsedYet")); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setTooltip(tooltip); - MoneroTx tx = getTxWithFewestConfirmations(cachedTxs); + MoneroTx tx = getTxWithFewestConfirmations(); if (tx == null) { txConfidenceIndicator.setVisible(false); } else { @@ -83,8 +82,8 @@ class DepositListItem { }}); } - private void updateUsage(int subaddressIndex, List cachedTxs, List cachedSubaddresses) { - numTxsWithOutputs = xmrWalletService.getNumTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs, cachedSubaddresses); + private void updateUsage(int subaddressIndex) { + numTxsWithOutputs = xmrWalletService.getNumTxsWithIncomingOutputs(addressEntry.getSubaddressIndex()); switch (addressEntry.getContext()) { case BASE_ADDRESS: usage = Res.get("funds.deposit.baseAddress"); @@ -138,15 +137,15 @@ class DepositListItem { return numTxsWithOutputs; } - public long getNumConfirmationsSinceFirstUsed(List incomingTxs) { - MoneroTx tx = getTxWithFewestConfirmations(incomingTxs); + public long getNumConfirmationsSinceFirstUsed() { + MoneroTx tx = getTxWithFewestConfirmations(); return tx == null ? 0 : tx.getNumConfirmations(); } - private MoneroTxWallet getTxWithFewestConfirmations(List allIncomingTxs) { + private MoneroTxWallet getTxWithFewestConfirmations() { // get txs with incoming outputs to subaddress index - List txs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), allIncomingTxs); + List txs = xmrWalletService.getTxsWithIncomingOutputs(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 2f1db7397e..b4a99627e8 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 @@ -76,9 +76,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.Callback; -import monero.wallet.model.MoneroSubaddress; 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; @@ -127,7 +125,6 @@ public class DepositView extends ActivatableView { private Subscription amountTextFieldSubscription; private ChangeListener tableViewSelectionListener; private int gridRow = 0; - List txsWithIncomingOutputs; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -151,15 +148,9 @@ public class DepositView extends ActivatableView { confirmationsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations"))); usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage"))); - // try to initialize with wallet txs + // trigger creation of at least 1 address try { - - // prefetch to avoid query per subaddress - txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs(); - List subaddresses = xmrWalletService.getWallet().getSubaddresses(0); - - // trigger creation of at least 1 address - xmrWalletService.getFreshAddressEntry(txsWithIncomingOutputs, subaddresses); + xmrWalletService.getFreshAddressEntry(); } catch (Exception e) { log.warn("Failed to get wallet txs to initialize DepositView"); e.printStackTrace(); @@ -181,7 +172,7 @@ public class DepositView extends ActivatableView { addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); - confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(txsWithIncomingOutputs))); + confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed())); usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); @@ -334,17 +325,13 @@ public class DepositView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { - - // cache incoming txs - txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs(); - List subaddresses = xmrWalletService.getWallet().getSubaddresses(0); // create deposit list items List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { if (addressEntry.isTrade()) continue; // skip reserved for trade - items.add(new DepositListItem(addressEntry, xmrWalletService, formatter, txsWithIncomingOutputs, subaddresses)); + items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } // update list diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 821cd34ef0..a3e27d3000 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -375,6 +375,8 @@ public abstract class MutableOfferView> exten advancedOptionsBox.setVisible(false); advancedOptionsBox.setManaged(false); + updateQrCode(); + model.onShowPayFundsScreen(() -> { if (!DevEnv.isDevMode()) { String key = "createOfferFundWalletInfo"; @@ -755,14 +757,7 @@ public abstract class MutableOfferView> exten missingCoinListener = (observable, oldValue, newValue) -> { if (!newValue.toString().equals("")) { - final byte[] imageBytes = QRCode - .from(getMoneroURI()) - .withSize(300, 300) - .to(ImageType.PNG) - .stream() - .toByteArray(); - Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); - qrCodeImageView.setImage(qrImage); + //updateQrCode(); // disabled to avoid wallet requests on key strokes } }; @@ -810,6 +805,17 @@ public abstract class MutableOfferView> exten }); } + private void updateQrCode() { + final byte[] imageBytes = QRCode + .from(getMoneroURI()) + .withSize(300, 300) + .to(ImageType.PNG) + .stream() + .toByteArray(); + Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); + qrCodeImageView.setImage(qrImage); + } + private void closeAndGoToOpenOffers() { //go to open offers UserThread.runAfter(() ->