implement batch key image polling for offer book in background

This commit is contained in:
woodser 2022-12-18 18:37:44 +00:00
parent 03b26fa423
commit 38864d71ff
8 changed files with 272 additions and 138 deletions

View file

@ -140,7 +140,6 @@ public final class CoreMoneroConnectionsService {
public void addListener(MoneroConnectionManagerListener listener) {
synchronized (lock) {
accountService.checkAccountOpen();
connectionManager.addListener(listener);
}
}
@ -236,11 +235,14 @@ public final class CoreMoneroConnectionsService {
}
}
public boolean isConnectionLocal() {
return getConnection() != null && HavenoUtils.isLocalHost(getConnection().getUri());
}
public long getDefaultRefreshPeriodMs() {
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
else {
boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
if (isLocal) {
if (isConnectionLocal()) {
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
} else {

View file

@ -17,7 +17,6 @@
package bisq.core.api;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService;
@ -52,7 +51,6 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import static bisq.common.util.MathUtils.exactMultiply;
import static bisq.common.util.MathUtils.roundDoubleToLong;
@ -81,8 +79,7 @@ public class CoreOffersService {
private final OfferFilterService offerFilter;
private final OpenOfferManager openOfferManager;
private final User user;
private final XmrWalletService xmrWalletService;
@Inject
public CoreOffersService(CoreContext coreContext,
KeyRing keyRing,
@ -92,8 +89,7 @@ public class CoreOffersService {
OfferFilterService offerFilter,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
User user,
XmrWalletService xmrWalletService) {
User user) {
this.coreContext = coreContext;
this.keyRing = keyRing;
this.coreWalletsService = coreWalletsService;
@ -102,116 +98,66 @@ public class CoreOffersService {
this.offerFilter = offerFilter;
this.openOfferManager = openOfferManager;
this.user = user;
this.xmrWalletService = xmrWalletService;
}
Offer getOffer(String id) {
return new ArrayList<>(offerBookService.getOffers()).stream()
.filter(o -> o.getId().equals(id))
.filter(o -> !o.isMyOffer(keyRing))
.filter(o -> {
Result result = offerFilter.canTakeOffer(o, coreContext.isApiUser());
boolean valid = result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
if (!valid) log.warn("Cannot take offer " + o.getId() + " with invalid state : " + result);
return valid;
})
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
}
Offer getMyOffer(String id) {
return new ArrayList<>(openOfferManager.getObservableList()).stream()
.map(OpenOffer::getOffer)
.filter(o -> o.getId().equals(id))
.filter(o -> o.isMyOffer(keyRing))
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
}
List<Offer> getOffers(String direction, String currencyCode) {
// excludes my offers
List<Offer> getOffers() {
List<Offer> offers = new ArrayList<>(offerBookService.getOffers()).stream()
.filter(o -> !o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.filter(o -> {
Result result = offerFilter.canTakeOffer(o, coreContext.isApiUser());
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
})
.sorted(priceComparator(direction))
.collect(Collectors.toList());
offers.removeAll(getUnreservedOffers(offers));
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers;
}
List<Offer> getMyOffers(String direction, String currencyCode) {
// get my open offers
List<Offer> offers = new ArrayList<>(openOfferManager.getObservableList()).stream()
.map(OpenOffer::getOffer)
.filter(o -> o.isMyOffer(keyRing))
List<Offer> getOffers(String direction, String currencyCode) {
return getOffers().stream()
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.sorted(priceComparator(direction))
.collect(Collectors.toList());
// remove unreserved offers
Set<Offer> unreservedOffers = getUnreservedOffers(offers); // TODO (woodser): optimize performance, probably don't call here
offers.removeAll(unreservedOffers);
// remove my unreserved offers from offer manager
List<OpenOffer> unreservedOpenOffers = new ArrayList<OpenOffer>();
for (Offer unreservedOffer : unreservedOffers) {
unreservedOpenOffers.add(openOfferManager.getOpenOfferById(unreservedOffer.getId()).get());
}
openOfferManager.removeOpenOffers(unreservedOpenOffers, null);
return offers;
}
private Set<Offer> getUnreservedOffers(List<Offer> offers) {
Set<Offer> unreservedOffers = new HashSet<Offer>();
// collect reserved key images and check for duplicate funds
List<String> allKeyImages = new ArrayList<String>();
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!allKeyImages.add(keyImage)) {
log.warn("Key image {} belongs to another offer, removing offer {}", keyImage, offer.getId()); // TODO (woodser): this is list, not set, so not checking for duplicates
unreservedOffers.add(offer);
}
}
}
// get spent key images
// TODO (woodser): paginate offers and only check key images of current page
List<String> spentKeyImages = new ArrayList<String>();
List<MoneroKeyImageSpentStatus> spentStatuses = allKeyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : xmrWalletService.getDaemon().getKeyImageSpentStatuses(allKeyImages);
for (int i = 0; i < spentStatuses.size(); i++) {
if (spentStatuses.get(i) != MoneroKeyImageSpentStatus.NOT_SPENT) spentKeyImages.add(allKeyImages.get(i));
}
// check for offers with spent key images
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
if (unreservedOffers.contains(offer)) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (spentKeyImages.contains(keyImage)) {
log.warn("Offer {} reserved funds have already been spent with key image {}", offer.getId(), keyImage);
unreservedOffers.add(offer);
}
}
}
return unreservedOffers;
Offer getOffer(String id) {
return getOffers().stream()
.filter(o -> o.getId().equals(id))
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
}
List<Offer> getMyOffers() {
List<Offer> offers = new ArrayList<>(openOfferManager.getObservableList()).stream()
.map(OpenOffer::getOffer)
.filter(o -> o.isMyOffer(keyRing))
.collect(Collectors.toList());
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers;
};
List<Offer> getMyOffers(String direction, String currencyCode) {
return getMyOffers().stream()
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.sorted(priceComparator(direction))
.collect(Collectors.toList());
}
Offer getMyOffer(String id) {
return getMyOffers().stream()
.filter(o -> o.getId().equals(id))
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
}
OpenOffer getMyOpenOffer(String id) {
getMyOffer(id); // ensure offer is valid
return openOfferManager.getOpenOfferById(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
}
// Create and place new offer.
void postOffer(String currencyCode,
String directionAsString,
String priceAsString,
@ -262,7 +208,6 @@ public class CoreOffersService {
errorMessageHandler);
}
// Edit a placed offer.
Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
@ -297,6 +242,27 @@ public class CoreOffersService {
});
}
// -------------------------- PRIVATE HELPERS -----------------------------
private Set<Offer> getOffersWithDuplicateKeyImages(List<Offer> offers) {
Set<Offer> duplicateFundedOffers = new HashSet<Offer>();
Set<String> seenKeyImages = new HashSet<String>();
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) {
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, removing offer {}", keyImage, offer2.getId());
duplicateFundedOffers.add(offer2);
}
}
}
}
}
return duplicateFundedOffers;
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",

View file

@ -9,6 +9,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon;
@ -17,6 +18,7 @@ import monero.daemon.model.MoneroKeyImageSpentStatus;
/**
* Poll for changes to the spent status of key images.
*/
@Slf4j
public class MoneroKeyImagePoller {
private MoneroDaemon daemon;
@ -25,6 +27,17 @@ public class MoneroKeyImagePoller {
private Set<MoneroKeyImageListener> listeners = new HashSet<MoneroKeyImageListener>();
private TaskLooper looper;
private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
private boolean isPolling = false;
/**
* Construct the listener.
*
* @param refreshPeriodMs - refresh period in milliseconds
* @param keyImages - key images to listen to
*/
public MoneroKeyImagePoller() {
looper = new TaskLooper(() -> poll());
}
/**
* Construct the listener.
@ -111,10 +124,9 @@ public class MoneroKeyImagePoller {
* @return the key images to listen to
*/
public void setKeyImages(String... keyImages) {
synchronized (keyImages) {
synchronized (this.keyImages) {
this.keyImages.clear();
this.keyImages.addAll(Arrays.asList(keyImages));
refreshPolling();
addKeyImages(keyImages);
}
}
@ -124,10 +136,7 @@ public class MoneroKeyImagePoller {
* @param keyImage - the key image to listen to
*/
public void addKeyImage(String keyImage) {
synchronized (keyImages) {
addKeyImages(keyImage);
refreshPolling();
}
addKeyImages(keyImage);
}
/**
@ -136,7 +145,16 @@ public class MoneroKeyImagePoller {
* @param keyImages - key images to listen to
*/
public void addKeyImages(String... keyImages) {
synchronized (keyImages) {
addKeyImages(Arrays.asList(keyImages));
}
/**
* Add key images to listen to.
*
* @param keyImages - key images to listen to
*/
public void addKeyImages(Collection<String> keyImages) {
synchronized (this.keyImages) {
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage);
refreshPolling();
}
@ -148,10 +166,7 @@ public class MoneroKeyImagePoller {
* @param keyImage - the key image to unlisten to
*/
public void removeKeyImage(String keyImage) {
synchronized (keyImages) {
removeKeyImages(keyImage);
refreshPolling();
}
removeKeyImages(keyImage);
}
/**
@ -160,38 +175,81 @@ public class MoneroKeyImagePoller {
* @param keyImages - key images to unlisten to
*/
public void removeKeyImages(String... keyImages) {
synchronized (keyImages) {
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) throw new MoneroError("Key image not registered with poller: " + keyImage);
this.keyImages.removeAll(Arrays.asList(keyImages));
removeKeyImages(Arrays.asList(keyImages));
}
/**
* Remove key images to listen to.
*
* @param keyImages - key images to unlisten to
*/
public void removeKeyImages(Collection<String> keyImages) {
synchronized (this.keyImages) {
Set<String> containedKeyImages = new HashSet<String>(keyImages);
containedKeyImages.retainAll(this.keyImages);
this.keyImages.removeAll(containedKeyImages);
for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage);
refreshPolling();
}
}
/**
* Indicates if the given key image is spent.
*
* @param keyImage - the key image to check
* @return true if the key is spent, false if unspent, null if unknown
*/
public Boolean isSpent(String keyImage) {
if (!lastStatuses.containsKey(keyImage)) return null;
return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT;
}
public void poll() {
synchronized (keyImages) {
// fetch spent statuses
List<MoneroKeyImageSpentStatus> spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages);
// collect changed statuses
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
for (int i = 0; i < keyImages.size(); i++) {
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
}
if (daemon == null) {
log.warn("Cannot poll key images because daemon is null");
return;
}
try {
// fetch spent statuses
List<MoneroKeyImageSpentStatus> spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages);
// announce changes
for (MoneroKeyImageListener listener : new ArrayList<MoneroKeyImageListener>(listeners)) listener.onSpentStatusChanged(changedStatuses);
// collect changed statuses
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
for (int i = 0; i < keyImages.size(); i++) {
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
}
}
// announce changes
if (!changedStatuses.isEmpty()) {
for (MoneroKeyImageListener listener : new ArrayList<MoneroKeyImageListener>(listeners)) {
listener.onSpentStatusChanged(changedStatuses);
}
}
} catch (Exception e) {
log.warn("Error polling key images: " + e.getMessage());
e.printStackTrace();
}
}
}
private void refreshPolling() {
setIsPolling(listeners.size() > 0);
setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
}
private void setIsPolling(boolean isPolling) {
if (isPolling) looper.start(refreshPeriodMs);
else looper.stop();
private void setIsPolling(boolean enabled) {
if (enabled) {
if (!isPolling) {
isPolling = true; // TODO monero-java: looper.isPolling()
looper.start(refreshPeriodMs);
}
} else {
isPolling = false;
looper.stop();
}
}
}

View file

@ -112,6 +112,11 @@ public class Offer implements NetworkPayload, PersistablePayload {
@JsonExclude
transient private String currencyCode;
@JsonExclude
@Getter
@Setter
transient private boolean isReservedFundsSpent;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor

View file

@ -17,6 +17,9 @@
package bisq.core.offer;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.wallet.MoneroKeyImageListener;
import bisq.core.btc.wallet.MoneroKeyImagePoller;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res;
import bisq.core.provider.price.PriceFeedService;
@ -25,13 +28,15 @@ import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.HashMapChangedListener;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import common.utils.GenUtils;
import monero.common.MoneroConnectionManagerListener;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.file.JsonFileManager;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Named;
@ -41,6 +46,7 @@ import java.io.File;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@ -56,18 +62,22 @@ import javax.annotation.Nullable;
public class OfferBookService {
private static final Logger log = LoggerFactory.getLogger(OfferBookService.class);
public interface OfferBookChangedListener {
void onAdded(Offer offer);
void onRemoved(Offer offer);
}
private final P2PService p2PService;
private final PriceFeedService priceFeedService;
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
private final FilterManager filterManager;
private final JsonFileManager jsonFileManager;
private final CoreMoneroConnectionsService connectionsService;
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
public interface OfferBookChangedListener {
void onAdded(Offer offer);
void onRemoved(Offer offer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -77,21 +87,36 @@ public class OfferBookService {
public OfferBookService(P2PService p2PService,
PriceFeedService priceFeedService,
FilterManager filterManager,
CoreMoneroConnectionsService connectionsService,
@Named(Config.STORAGE_DIR) File storageDir,
@Named(Config.DUMP_STATISTICS) boolean dumpStatistics) {
this.p2PService = p2PService;
this.priceFeedService = priceFeedService;
this.filterManager = filterManager;
this.connectionsService = connectionsService;
jsonFileManager = new JsonFileManager(storageDir);
// listen for monero connection changes
connectionsService.addListener(new MoneroConnectionManagerListener() {
@Override
public void onConnectionChanged(MoneroRpcConnection connection) {
keyImagePoller.setDaemon(connectionsService.getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
}
});
// listen for offers
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
maybeInitializeKeyImagePoller();
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onAdded(offer);
}
}));
@ -101,9 +126,12 @@ public class OfferBookService {
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
maybeInitializeKeyImagePoller();
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onRemoved(offer);
}
}));
@ -197,6 +225,7 @@ public class OfferBookService {
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
return offer;
})
.collect(Collectors.toList());
@ -225,6 +254,52 @@ 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<String, MoneroKeyImageSpentStatus> spentStatuses) {
for (String keyImage : spentStatuses.keySet()) {
updateAffectedOffers(keyImage);
}
}
});
// first poll after 5s
new Thread(() -> {
GenUtils.waitFor(5000);
keyImagePoller.poll();
});
}
}
private long getKeyImageRefreshPeriodMs() {
return connectionsService.isConnectionLocal() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
private void updateAffectedOffers(String keyImage) {
for (Offer offer : getOffers()) {
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
offerBookChangedListeners.forEach(listener -> {
listener.onRemoved(offer);
listener.onAdded(offer);
});
}
}
}
private void setReservedFundsSpent(Offer offer) {
if (keyImagePoller == null) return;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
log.warn("Reserved funds spent for offer {}", offer.getId());
offer.setReservedFundsSpent(true);
}
}
}
private void doDumpStatistics() {
// We filter the case that it is a MarketBasedPrice but the price is not available
// That should only be possible if the price feed provider is not available

View file

@ -83,7 +83,8 @@ public class OfferFilterService {
IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT,
IS_MY_INSUFFICIENT_TRADE_LIMIT,
ARBITRATOR_NOT_VALIDATED,
SIGNATURE_NOT_VALIDATED;
SIGNATURE_NOT_VALIDATED,
RESERVE_FUNDS_SPENT;
@Getter
private final boolean isValid;
@ -134,6 +135,9 @@ public class OfferFilterService {
if (!hasValidSignature(offer)) {
return Result.SIGNATURE_NOT_VALIDATED;
}
if (isReservedFundsSpent(offer)) {
return Result.RESERVE_FUNDS_SPENT;
}
if (!isAnyPaymentAccountValidForOffer(offer)) {
return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
}
@ -226,4 +230,8 @@ public class OfferFilterService {
if (arbitrator == null) return false; // invalid arbitrator
return HavenoUtils.isArbitratorSignatureValid(offer, arbitrator);
}
public boolean isReservedFundsSpent(Offer offer) {
return offer.isReservedFundsSpent();
}
}

View file

@ -25,6 +25,7 @@ import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.exceptions.TradePriceOutOfToleranceException;
import bisq.core.filter.FilterManager;
import bisq.core.offer.OfferBookService.OfferBookChangedListener;
import bisq.core.offer.messages.OfferAvailabilityRequest;
import bisq.core.offer.messages.OfferAvailabilityResponse;
import bisq.core.offer.messages.SignOfferRequest;
@ -189,6 +190,21 @@ 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
// remove open offer if reserved funds spent
offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() {
@Override
public void onAdded(Offer offer) {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
if (openOfferOptional.isPresent() && offer.isReservedFundsSpent()) {
removeOpenOffer(openOfferOptional.get(), null);
}
}
@Override
public void onRemoved(Offer offer) {
// nothing to do
}
});
}
@Override
@ -301,6 +317,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
removeOpenOffers(getObservableList(), completeHandler);
}
public void removeOpenOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
removeOpenOffers(List.of(openOffer), completeHandler);
}
public void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
int size = openOffers.size();
// Copy list as we remove in the loop

View file

@ -1065,8 +1065,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
// delete trade wallet
trade.deleteWallet();
// delete trade wallet if exists
if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet();
// unreserve key images
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {