mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-06 05:34:50 -04:00
Fix missing sequence nr and signature updates at refresh offers
This commit is contained in:
parent
ca20de64d9
commit
bb6334f6a0
24 changed files with 720 additions and 506 deletions
|
@ -17,15 +17,14 @@
|
|||
|
||||
package io.bitsquare.arbitration;
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.app.ProgramArguments;
|
||||
import io.bitsquare.common.Timer;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.handlers.ErrorMessageHandler;
|
||||
import io.bitsquare.common.handlers.ResultHandler;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.BootstrapListener;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
|
@ -47,7 +46,6 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -57,10 +55,14 @@ import static org.bitcoinj.core.Utils.HEX;
|
|||
public class ArbitratorManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class);
|
||||
|
||||
private final KeyRing keyRing;
|
||||
private final ArbitratorService arbitratorService;
|
||||
private final User user;
|
||||
private final ObservableMap<NodeAddress, Arbitrator> arbitratorsObservableMap = FXCollections.observableHashMap();
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Static
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final long REPUBLISH_MILLIS = Arbitrator.TTL / 2;
|
||||
private static final long RETRY_REPUBLISH_SEC = 5;
|
||||
|
||||
private static final String publicKeyForTesting = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee";
|
||||
|
||||
// Keys for invited arbitrators in bootstrapping phase (before registration is open to anyone and security payment is implemented)
|
||||
// For testing purpose here is a private key so anyone can setup an arbitrator for now.
|
||||
|
@ -87,10 +89,24 @@ public class ArbitratorManager {
|
|||
"0274f772a98d23e7a0251ab30d7121897b5aebd11a2f1e45ab654aa57503173245",
|
||||
"036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f"
|
||||
));
|
||||
private static final String publicKeyForTesting = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee";
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Instance fields
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private final KeyRing keyRing;
|
||||
private final ArbitratorService arbitratorService;
|
||||
private final User user;
|
||||
private final ObservableMap<NodeAddress, Arbitrator> arbitratorsObservableMap = FXCollections.observableHashMap();
|
||||
private final boolean isDevTest;
|
||||
private BootstrapListener bootstrapListener;
|
||||
private ScheduledThreadPoolExecutor republishArbitratorExecutor;
|
||||
private Timer republishArbitratorTimer, retryRepublishArbitratorTimer;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public ArbitratorManager(@Named(ProgramArguments.DEV_TEST) boolean isDevTest, KeyRing keyRing, ArbitratorService arbitratorService, User user) {
|
||||
|
@ -113,19 +129,26 @@ public class ArbitratorManager {
|
|||
}
|
||||
|
||||
public void shutDown() {
|
||||
if (republishArbitratorExecutor != null)
|
||||
MoreExecutors.shutdownAndAwaitTermination(republishArbitratorExecutor, 500, TimeUnit.MILLISECONDS);
|
||||
stopRepublishArbitratorTimer();
|
||||
stopRetryRepublishArbitratorTimer();
|
||||
if (bootstrapListener != null)
|
||||
arbitratorService.getP2PService().removeP2PServiceListener(bootstrapListener);
|
||||
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
if (user.getRegisteredArbitrator() != null) {
|
||||
|
||||
P2PService p2PService = arbitratorService.getP2PService();
|
||||
if (!p2PService.isBootstrapped()) {
|
||||
bootstrapListener = new BootstrapListener() {
|
||||
@Override
|
||||
public void onBootstrapComplete() {
|
||||
republishArbitrator();
|
||||
ArbitratorManager.this.onBootstrapComplete();
|
||||
}
|
||||
};
|
||||
p2PService.addP2PServiceListener(bootstrapListener);
|
||||
|
@ -133,29 +156,13 @@ public class ArbitratorManager {
|
|||
} else {
|
||||
republishArbitrator();
|
||||
}
|
||||
|
||||
// re-publish periodically
|
||||
republishArbitratorExecutor = Utilities.getScheduledThreadPoolExecutor("republishArbitrator", 1, 5, 5);
|
||||
long delay = Arbitrator.TTL / 2;
|
||||
republishArbitratorExecutor.scheduleAtFixedRate(this::republishArbitrator, delay, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
republishArbitratorTimer = UserThread.runPeriodically(this::republishArbitrator, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS);
|
||||
|
||||
applyArbitrators();
|
||||
}
|
||||
|
||||
private void republishArbitrator() {
|
||||
if (bootstrapListener != null)
|
||||
arbitratorService.getP2PService().removeP2PServiceListener(bootstrapListener);
|
||||
|
||||
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
|
||||
if (registeredArbitrator != null) {
|
||||
addArbitrator(registeredArbitrator,
|
||||
this::applyArbitrators,
|
||||
log::error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void applyArbitrators() {
|
||||
Map<NodeAddress, Arbitrator> map = arbitratorService.getArbitrators();
|
||||
log.trace("Arbitrators . size=" + map.values().size());
|
||||
|
@ -222,18 +229,6 @@ public class ArbitratorManager {
|
|||
return key.signMessage(keyToSignAsHex);
|
||||
}
|
||||
|
||||
private boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) {
|
||||
String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded());
|
||||
try {
|
||||
ECKey key = ECKey.fromPublicOnly(registrationPubKey);
|
||||
key.verifyMessage(keyToSignAsHex, signature);
|
||||
return true;
|
||||
} catch (SignatureException e) {
|
||||
log.warn("verifySignature failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ECKey getRegistrationKey(String privKeyBigIntString) {
|
||||
try {
|
||||
|
@ -246,4 +241,61 @@ public class ArbitratorManager {
|
|||
public boolean isPublicKeyInList(String pubKeyAsHex) {
|
||||
return isDevTest && pubKeyAsHex.equals(publicKeyForTesting) || publicKeys.contains(pubKeyAsHex);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onBootstrapComplete() {
|
||||
if (bootstrapListener != null) {
|
||||
arbitratorService.getP2PService().removeP2PServiceListener(bootstrapListener);
|
||||
bootstrapListener = null;
|
||||
}
|
||||
|
||||
republishArbitrator();
|
||||
}
|
||||
|
||||
private void republishArbitrator() {
|
||||
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
|
||||
if (registeredArbitrator != null) {
|
||||
addArbitrator(registeredArbitrator,
|
||||
this::applyArbitrators,
|
||||
errorMessage -> {
|
||||
if (retryRepublishArbitratorTimer == null)
|
||||
retryRepublishArbitratorTimer = UserThread.runPeriodically(() -> {
|
||||
stopRetryRepublishArbitratorTimer();
|
||||
republishArbitrator();
|
||||
}, RETRY_REPUBLISH_SEC);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) {
|
||||
String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded());
|
||||
try {
|
||||
ECKey key = ECKey.fromPublicOnly(registrationPubKey);
|
||||
key.verifyMessage(keyToSignAsHex, signature);
|
||||
return true;
|
||||
} catch (SignatureException e) {
|
||||
log.warn("verifySignature failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void stopRetryRepublishArbitratorTimer() {
|
||||
if (retryRepublishArbitratorTimer != null) {
|
||||
retryRepublishArbitratorTimer.stop();
|
||||
retryRepublishArbitratorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopRepublishArbitratorTimer() {
|
||||
if (republishArbitratorTimer != null) {
|
||||
republishArbitratorTimer.stop();
|
||||
republishArbitratorTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,8 +54,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
|
|||
@JsonExclude
|
||||
private static final Logger log = LoggerFactory.getLogger(Offer.class);
|
||||
|
||||
public static final long TTL = TimeUnit.SECONDS.toMillis(60);
|
||||
// public static final long TTL = TimeUnit.SECONDS.toMillis(10); //TODO
|
||||
// public static final long TTL = TimeUnit.SECONDS.toMillis(60);
|
||||
public static final long TTL = TimeUnit.SECONDS.toMillis(10); //TODO
|
||||
|
||||
public final static String TAC_OFFERER = "When placing that offer I accept that anyone who fulfills my conditions can " +
|
||||
"take that offer.";
|
||||
|
|
|
@ -60,7 +60,10 @@ import static io.bitsquare.util.Validator.nonEmptyStringOf;
|
|||
public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMessageListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenOfferManager.class);
|
||||
|
||||
private static final long RETRY_DELAY_AFTER_ALL_CON_LOST_SEC = 5;
|
||||
private static final long RETRY_REPUBLISH_DELAY_SEC = 5;
|
||||
private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 10;
|
||||
private static final long REPUBLISH_INTERVAL_MILLIS = 10 * Offer.TTL;
|
||||
private static final long REFRESH_INTERVAL_MILLIS = (long) (Offer.TTL * 0.5);
|
||||
|
||||
private final KeyRing keyRing;
|
||||
private final User user;
|
||||
|
@ -73,7 +76,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
private final TradableList<OpenOffer> openOffers;
|
||||
private final Storage<TradableList<OpenOffer>> openOffersStorage;
|
||||
private boolean stopped;
|
||||
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, republishOffersTimer;
|
||||
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer;
|
||||
private BootstrapListener bootstrapListener;
|
||||
|
||||
|
||||
|
@ -131,6 +134,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
|
||||
stopPeriodicRefreshOffersTimer();
|
||||
stopPeriodicRepublishOffersTimer();
|
||||
stopRetryRepublishOffersTimer();
|
||||
|
||||
log.info("remove all open offers at shutDown");
|
||||
// we remove own offers from offerbook when we go offline
|
||||
|
@ -167,10 +171,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
|
||||
// Republish means we send the complete offer object
|
||||
republishOffers();
|
||||
startRepublishOffersThread();
|
||||
startPeriodicRepublishOffersTimer();
|
||||
|
||||
// Refresh is started once we get a success from republish
|
||||
|
||||
// We republish after a bit as it might be that our connected node still has the offer in the data map
|
||||
// but other peers have it already removed because of expired TTL.
|
||||
// Those other not directly connected peers would not get the broadcast of the new offer, as the first
|
||||
// connected peer (seed node) does nto broadcast if it has the data in the map.
|
||||
// To update quickly to the whole network we repeat the republishOffers call after a few seconds when we
|
||||
// are better connected to the network. There is no guarantee that all peers will receive it but we have
|
||||
// also our periodic timer, so after that longer interval the offer should be available to all peers.
|
||||
if (retryRepublishOffersTimer == null)
|
||||
retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
|
||||
REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC);
|
||||
|
||||
p2PService.getPeerManager().addListener(this);
|
||||
}
|
||||
|
||||
|
@ -184,90 +199,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
stopped = true;
|
||||
stopPeriodicRefreshOffersTimer();
|
||||
stopPeriodicRepublishOffersTimer();
|
||||
stopRetryRepublishOffersTimer();
|
||||
|
||||
restart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewConnectionAfterAllConnectionsLost() {
|
||||
stopped = false;
|
||||
restart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAwakeFromStandby() {
|
||||
stopped = false;
|
||||
if (!p2PService.getNetworkNode().getAllConnections().isEmpty())
|
||||
restart();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RepublishOffers, refreshOffers
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void startRepublishOffersThread() {
|
||||
stopped = false;
|
||||
if (periodicRepublishOffersTimer == null)
|
||||
periodicRepublishOffersTimer = UserThread.runPeriodically(OpenOfferManager.this::republishOffers,
|
||||
Offer.TTL * 10,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void republishOffers() {
|
||||
Log.traceCall("Number of offer for republish: " + openOffers.size());
|
||||
if (!stopped) {
|
||||
stopPeriodicRefreshOffersTimer();
|
||||
|
||||
for (OpenOffer openOffer : openOffers) {
|
||||
offerBookService.republishOffers(openOffer.getOffer(),
|
||||
() -> {
|
||||
log.debug("Successful added offer to P2P network");
|
||||
// Refresh means we send only the dat needed to refresh the TTL (hash, signature and sequence nr.)
|
||||
startRefreshOffersThread();
|
||||
},
|
||||
errorMessage -> {
|
||||
//TODO handle with retry
|
||||
log.error("Add offer to P2P network failed. " + errorMessage);
|
||||
stopRepublishOffersTimer();
|
||||
republishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
|
||||
RETRY_DELAY_AFTER_ALL_CON_LOST_SEC);
|
||||
});
|
||||
openOffer.setStorage(openOffersStorage);
|
||||
}
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that republishOffers call.");
|
||||
}
|
||||
}
|
||||
|
||||
private void startRefreshOffersThread() {
|
||||
stopped = false;
|
||||
// refresh sufficiently before offer would expire
|
||||
if (periodicRefreshOffersTimer == null)
|
||||
periodicRefreshOffersTimer = UserThread.runPeriodically(OpenOfferManager.this::refreshOffers,
|
||||
(long) (Offer.TTL * 0.5),
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void refreshOffers() {
|
||||
if (!stopped) {
|
||||
Log.traceCall("Number of offer for refresh: " + openOffers.size());
|
||||
for (OpenOffer openOffer : openOffers) {
|
||||
offerBookService.refreshOffer(openOffer.getOffer(),
|
||||
() -> log.debug("Successful refreshed TTL for offer"),
|
||||
errorMessage -> log.error("Refresh TTL for offer failed. " + errorMessage));
|
||||
}
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that refreshOffers call.");
|
||||
}
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
startRepublishOffersThread();
|
||||
startRefreshOffersThread();
|
||||
if (republishOffersTimer == null) {
|
||||
stopped = false;
|
||||
republishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, RETRY_DELAY_AFTER_ALL_CON_LOST_SEC);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -282,6 +232,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
openOffers.add(openOffer);
|
||||
openOffersStorage.queueUpForSave();
|
||||
resultHandler.handleResult(transaction);
|
||||
if (!stopped) {
|
||||
startPeriodicRepublishOffersTimer();
|
||||
startPeriodicRefreshOffersTimer();
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
|
||||
}
|
||||
}
|
||||
);
|
||||
placeOfferProtocol.placeOffer();
|
||||
|
@ -395,6 +351,97 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RepublishOffers, refreshOffers
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void republishOffers() {
|
||||
Log.traceCall("Number of offer for republish: " + openOffers.size());
|
||||
if (!stopped) {
|
||||
stopPeriodicRefreshOffersTimer();
|
||||
|
||||
openOffers.stream().forEach(openOffer -> {
|
||||
offerBookService.republishOffers(openOffer.getOffer(),
|
||||
() -> {
|
||||
if (!stopped) {
|
||||
log.debug("Successful added offer to P2P network");
|
||||
// Refresh means we send only the dat needed to refresh the TTL (hash, signature and sequence nr.)
|
||||
if (periodicRefreshOffersTimer == null)
|
||||
startPeriodicRefreshOffersTimer();
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that offerBookService.republishOffers.onSuccess call.");
|
||||
}
|
||||
},
|
||||
errorMessage -> {
|
||||
if (!stopped) {
|
||||
log.error("Add offer to P2P network failed. " + errorMessage);
|
||||
stopRetryRepublishOffersTimer();
|
||||
retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
|
||||
RETRY_REPUBLISH_DELAY_SEC);
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that offerBookService.republishOffers.onFault call.");
|
||||
}
|
||||
});
|
||||
openOffer.setStorage(openOffersStorage);
|
||||
});
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that republishOffers call.");
|
||||
}
|
||||
}
|
||||
|
||||
private void startPeriodicRepublishOffersTimer() {
|
||||
Log.traceCall();
|
||||
stopped = false;
|
||||
if (periodicRepublishOffersTimer == null)
|
||||
periodicRepublishOffersTimer = UserThread.runPeriodically(() -> {
|
||||
if (!stopped) {
|
||||
republishOffers();
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that periodicRepublishOffersTimer.run call.");
|
||||
}
|
||||
},
|
||||
REPUBLISH_INTERVAL_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
else
|
||||
log.warn("periodicRepublishOffersTimer already stated");
|
||||
}
|
||||
|
||||
private void startPeriodicRefreshOffersTimer() {
|
||||
Log.traceCall();
|
||||
stopped = false;
|
||||
// refresh sufficiently before offer would expire
|
||||
if (periodicRefreshOffersTimer == null)
|
||||
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
|
||||
if (!stopped) {
|
||||
Log.traceCall("Number of offer for refresh: " + openOffers.size());
|
||||
openOffers.stream().forEach(openOffer -> {
|
||||
offerBookService.refreshOffer(openOffer.getOffer(),
|
||||
() -> log.debug("Successful refreshed TTL for offer"),
|
||||
errorMessage -> log.error("Refresh TTL for offer failed. " + errorMessage));
|
||||
});
|
||||
} else {
|
||||
log.warn("We have stopped already. We ignore that periodicRefreshOffersTimer.run call.");
|
||||
}
|
||||
},
|
||||
REFRESH_INTERVAL_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
else
|
||||
log.warn("periodicRefreshOffersTimer already stated");
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
Log.traceCall();
|
||||
if (retryRepublishOffersTimer == null)
|
||||
retryRepublishOffersTimer = UserThread.runAfter(() -> {
|
||||
stopped = false;
|
||||
stopRetryRepublishOffersTimer();
|
||||
republishOffers();
|
||||
}, RETRY_REPUBLISH_DELAY_SEC);
|
||||
|
||||
startPeriodicRepublishOffersTimer();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -413,10 +460,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
}
|
||||
}
|
||||
|
||||
private void stopRepublishOffersTimer() {
|
||||
if (republishOffersTimer != null) {
|
||||
republishOffersTimer.stop();
|
||||
republishOffersTimer = null;
|
||||
private void stopRetryRepublishOffersTimer() {
|
||||
if (retryRepublishOffersTimer != null) {
|
||||
retryRepublishOffersTimer.stop();
|
||||
retryRepublishOffersTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue