diff --git a/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java index af49708acb..96918bc8e7 100644 --- a/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java +++ b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java @@ -17,6 +17,8 @@ import monero.daemon.model.MoneroKeyImageSpentStatus; /** * Poll for changes to the spent status of key images. + * + * TODO: move to monero-java? */ @Slf4j public class MoneroKeyImagePoller { @@ -240,7 +242,7 @@ public class MoneroKeyImagePoller { setIsPolling(keyImages.size() > 0 && listeners.size() > 0); } - private void setIsPolling(boolean enabled) { + private synchronized void setIsPolling(boolean enabled) { if (enabled) { if (!isPolling) { isPolling = true; // TODO monero-java: looper.isPolling() diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index e519be1bc9..abb26968ff 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -69,6 +69,7 @@ public class OfferBookService { private final JsonFileManager jsonFileManager; private final CoreMoneroConnectionsService connectionsService; + // poll key images of offers private MoneroKeyImagePoller keyImagePoller; private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes @@ -96,7 +97,7 @@ public class OfferBookService { this.connectionsService = connectionsService; jsonFileManager = new JsonFileManager(storageDir); - // listen for monero connection changes + // listen for connection changes to monerod connectionsService.addListener(new MoneroConnectionManagerListener() { @Override public void onConnectionChanged(MoneroRpcConnection connection) { @@ -255,25 +256,25 @@ public class OfferBookService { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void maybeInitializeKeyImagePoller() { - synchronized (this) { - if (keyImagePoller != null) return; - keyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs()); - keyImagePoller.addListener(new MoneroKeyImageListener() { - @Override - public void onSpentStatusChanged(Map spentStatuses) { - for (String keyImage : spentStatuses.keySet()) { - updateAffectedOffers(keyImage); - } + private synchronized void maybeInitializeKeyImagePoller() { + if (keyImagePoller != null) return; + keyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs()); + + // handle when key images spent + keyImagePoller.addListener(new MoneroKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (String keyImage : spentStatuses.keySet()) { + updateAffectedOffers(keyImage); } - }); - - // first poll after 5s - new Thread(() -> { - GenUtils.waitFor(5000); - keyImagePoller.poll(); - }); - } + } + }); + + // first poll after 5s + new Thread(() -> { + GenUtils.waitFor(5000); + keyImagePoller.poll(); + }); } private long getKeyImageRefreshPeriodMs() { diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 30999be8fd..9e059e2884 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -21,6 +21,8 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.api.CoreContext; import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.MoneroKeyImageListener; +import bisq.core.btc.wallet.MoneroKeyImagePoller; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.exceptions.TradePriceOutOfToleranceException; @@ -56,6 +58,7 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.peers.PeerManager; +import common.utils.GenUtils; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.Capabilities; @@ -84,6 +87,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.Map.Entry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -92,6 +96,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import lombok.Getter; +import monero.common.MoneroConnectionManagerListener; +import monero.common.MoneroRpcConnection; +import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; @@ -114,7 +121,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private final KeyRing keyRing; private final User user; private final P2PService p2PService; - private final CoreMoneroConnectionsService connectionService; + private final CoreMoneroConnectionsService connectionsService; private final BtcWalletService btcWalletService; private final XmrWalletService xmrWalletService; private final TradeWalletService tradeWalletService; @@ -141,6 +148,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Getter private final AccountAgeWitnessService accountAgeWitnessService; + // poll key images of signed offers + private MoneroKeyImagePoller signedOfferKeyImagePoller; + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialization @@ -151,7 +163,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe KeyRing keyRing, User user, P2PService p2PService, - CoreMoneroConnectionsService connectionService, + CoreMoneroConnectionsService connectionsService, BtcWalletService btcWalletService, XmrWalletService xmrWalletService, TradeWalletService tradeWalletService, @@ -171,7 +183,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe this.keyRing = keyRing; this.user = user; this.p2PService = p2PService; - this.connectionService = connectionService; + this.connectionsService = connectionsService; this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.tradeWalletService = tradeWalletService; @@ -191,6 +203,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers + // listen for connection changes to monerod + connectionsService.addListener(new MoneroConnectionManagerListener() { + @Override + public void onConnectionChanged(MoneroRpcConnection connection) { + maybeInitializeKeyImagePoller(); + signedOfferKeyImagePoller.setDaemon(connectionsService.getDaemon()); + signedOfferKeyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); + } + }); + // remove open offer if reserved funds spent offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() { @Override @@ -214,7 +236,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe persistenceManager.readPersisted(persisted -> { openOffers.setAll(persisted.getList()); openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); - + // read signed offers signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { signedOffers.setAll(signedOfferPersisted.getList()); @@ -225,6 +247,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe completeHandler); } + private synchronized void maybeInitializeKeyImagePoller() { + if (signedOfferKeyImagePoller != null) return; + signedOfferKeyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs()); + + // handle when key images confirmed spent + signedOfferKeyImagePoller.addListener(new MoneroKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (Entry entry : spentStatuses.entrySet()) { + if (entry.getValue() == MoneroKeyImageSpentStatus.CONFIRMED) { + removeSignedOffers(entry.getKey()); + } + } + } + }); + + // first poll in 5s + new Thread(() -> { + GenUtils.waitFor(5000); + signedOfferKeyImagePoller.poll(); + }); + } + + private long getKeyImageRefreshPeriodMs() { + return connectionsService.isConnectionLocal() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; + } + public void onAllServicesInitialized() { p2PService.addDecryptedDirectMessageListener(this); @@ -264,6 +313,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe lastUnlockedBalance = newUnlockedBalance; } }); + + // initialize key image poller for signed offers + maybeInitializeKeyImagePoller(); + + // poll spent status of key images + for (SignedOffer signedOffer : signedOffers.getList()) { + signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + } } private void cleanUpAddressEntries() { @@ -672,9 +729,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void addSignedOffer(SignedOffer openOffer) { + private void addSignedOffer(SignedOffer signedOffer) { + log.info("Adding SignedOffer offer for offer {}", signedOffer.getOfferId()); synchronized (signedOffers) { - signedOffers.add(openOffer); + signedOffers.add(signedOffer); + signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + } + } + + private void removeSignedOffer(SignedOffer signedOffer) { + log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId()); + synchronized (signedOffers) { + signedOffers.remove(signedOffer); + signedOfferKeyImagePoller.removeKeyImages(signedOffer.getReserveTxKeyImages()); + } + } + + private void removeSignedOffers(String keyImage) { + for (SignedOffer signedOffer : new ArrayList(signedOffers.getList())) { + if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { + removeSignedOffer(signedOffer); + } } } @@ -1008,7 +1083,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } // Don't allow trade start if Monero node is not fully synced - if (!connectionService.isSyncedWithinTolerance()) { + if (!connectionsService.isSyncedWithinTolerance()) { errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced."; log.info(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index 416546eaac..86b44c2285 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -1,6 +1,7 @@ package bisq.core.offer; import bisq.core.api.CoreContext; +import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.trade.TradableList; import bisq.network.p2p.P2PService; @@ -52,6 +53,7 @@ public class OpenOfferManagerTest { public void testStartEditOfferForActiveOffer() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); + CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); @@ -59,7 +61,7 @@ public class OpenOfferManagerTest { null, null, p2PService, - null, + connectionsService, null, null, null, @@ -100,13 +102,14 @@ public class OpenOfferManagerTest { public void testStartEditOfferForDeactivatedOffer() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); + CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); final OpenOfferManager manager = new OpenOfferManager(coreContext, null, null, p2PService, - null, + connectionsService, null, null, null, @@ -139,6 +142,7 @@ public class OpenOfferManagerTest { public void testStartEditOfferForOfferThatIsCurrentlyEdited() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); + CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); @@ -147,7 +151,7 @@ public class OpenOfferManagerTest { null, null, p2PService, - null, + connectionsService, null, null, null,