mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-13 08:55:31 -04:00
improve stability on tor, refactor startup and shut down
refactor startup sequence to improve message reliability refactor shut down sequence to finish processing messages reduce monerod requests to improve slow tor connections refactor trade wallet polling monero node service uses default data directory unless local connections service checks connection by polling daemon connections service supports getRefreshPeriodMs and shutting down add make config: monerod-stagenet-custom fix bugs in key image polling force stop wallet on deletion trade manager initializes persisted trades on data received support hardcoding monero log level and request stack traces remove xmrAddress from Arbitrator model fix formatting of MoneroWalletRpcManager
This commit is contained in:
parent
5e364f7e7e
commit
2afa5d761d
51 changed files with 1058 additions and 644 deletions
|
@ -25,6 +25,7 @@ import haveno.common.crypto.KeyRing;
|
|||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.crypto.Sig;
|
||||
import haveno.common.util.Utilities;
|
||||
import haveno.core.app.HavenoSetup;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferPayload;
|
||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||
|
@ -35,6 +36,8 @@ import haveno.core.trade.messages.PaymentSentMessage;
|
|||
import haveno.core.util.JsonUtil;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -69,8 +72,9 @@ public class HavenoUtils {
|
|||
private static final int POOL_SIZE = 10;
|
||||
private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE);
|
||||
|
||||
public static ArbitrationManager arbitrationManager; // TODO: better way to share reference?
|
||||
|
||||
// TODO: better way to share refernces?
|
||||
public static ArbitrationManager arbitrationManager;
|
||||
public static HavenoSetup havenoSetup;
|
||||
|
||||
// ----------------------- CONVERSION UTILS -------------------------------
|
||||
|
||||
|
@ -502,4 +506,10 @@ public class HavenoUtils {
|
|||
public static String toCamelCase(String underscore) {
|
||||
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, underscore);
|
||||
}
|
||||
|
||||
public static boolean connectionConfigsEqual(MoneroRpcConnection c1, MoneroRpcConnection c2) {
|
||||
if (c1 == c2) return true;
|
||||
if (c1 == null) return false;
|
||||
return c1.equals(c2); // equality considers uri, username, and password
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package haveno.core.trade;
|
|||
import com.google.common.base.Preconditions;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Message;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.crypto.Encryption;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
|
@ -72,6 +73,7 @@ import monero.common.TaskLooper;
|
|||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.MoneroWalletRpc;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroMultisigSignResult;
|
||||
import monero.wallet.model.MoneroOutputWallet;
|
||||
|
@ -80,6 +82,8 @@ import monero.wallet.model.MoneroTxQuery;
|
|||
import monero.wallet.model.MoneroTxSet;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
@ -94,7 +98,6 @@ import java.util.Arrays;
|
|||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -385,6 +388,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
@Getter
|
||||
transient private boolean isInitialized;
|
||||
@Getter
|
||||
transient private boolean isShutDownStarted;
|
||||
@Getter
|
||||
transient private boolean isShutDown;
|
||||
|
||||
// Added in v1.2.0
|
||||
|
@ -572,12 +577,15 @@ public abstract class Trade implements Tradable, Model {
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void initialize(ProcessModelServiceProvider serviceProvider) {
|
||||
if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized");
|
||||
|
||||
// set arbitrator pub key ring once known
|
||||
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
|
||||
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
||||
});
|
||||
|
||||
// listen to daemon connection
|
||||
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
|
||||
xmrWalletService.getConnectionsService().addListener(newConnection -> onConnectionChanged(newConnection));
|
||||
|
||||
// check if done
|
||||
if (isPayoutUnlocked()) {
|
||||
|
@ -585,19 +593,19 @@ public abstract class Trade implements Tradable, Model {
|
|||
return;
|
||||
}
|
||||
|
||||
// reset payment sent state if no ack receive
|
||||
if (getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
|
||||
// reset buyer's payment sent state if no ack receive
|
||||
if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
|
||||
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
}
|
||||
|
||||
// reset payment received state if no ack receive
|
||||
if (getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||
// reset seller's payment received state if no ack receive
|
||||
if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
}
|
||||
|
||||
// handle trade state events
|
||||
// handle trade phase events
|
||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||
if (isCompleted()) {
|
||||
|
@ -652,18 +660,28 @@ public abstract class Trade implements Tradable, Model {
|
|||
xmrWalletService.addWalletListener(idlePayoutSyncer);
|
||||
}
|
||||
|
||||
if (isDepositRequested()) {
|
||||
|
||||
// start syncing and polling trade wallet
|
||||
updateSyncing();
|
||||
|
||||
// allow state notifications to process before returning
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
UserThread.execute(() -> latch.countDown());
|
||||
HavenoUtils.awaitLatch(latch);
|
||||
// send deposit confirmed message on startup or event
|
||||
if (isDepositsConfirmed()) {
|
||||
new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start();
|
||||
} else {
|
||||
EasyBind.subscribe(stateProperty(), state -> {
|
||||
if (isDepositsConfirmed()) {
|
||||
new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// reprocess pending payout messages
|
||||
this.getProtocol().maybeReprocessPaymentReceivedMessage(false);
|
||||
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
|
||||
|
||||
// trade is initialized but not synced
|
||||
isInitialized = true;
|
||||
|
||||
// sync wallet if applicable
|
||||
if (!isDepositRequested() || isPayoutUnlocked()) return;
|
||||
if (xmrWalletService.getConnectionsService().getConnection() == null || Boolean.FALSE.equals(xmrWalletService.getConnectionsService().isConnected())) return;
|
||||
updateSyncing();
|
||||
}
|
||||
|
||||
public void requestPersistence() {
|
||||
|
@ -710,8 +728,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
synchronized (walletLock) {
|
||||
if (wallet != null) return wallet;
|
||||
if (!walletExists()) return null;
|
||||
if (isShutDown) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because trade is shut down");
|
||||
if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName());
|
||||
if (isShutDownStarted) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because shut down is started");
|
||||
else wallet = xmrWalletService.openWallet(getWalletName());
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
@ -744,7 +762,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
|
||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
getWallet().sync();
|
||||
xmrWalletService.syncWallet(getWallet());
|
||||
pollWallet();
|
||||
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
}
|
||||
|
@ -753,7 +771,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
try {
|
||||
syncWallet();
|
||||
} catch (Exception e) {
|
||||
if (!isShutDown) {
|
||||
if (!isShutDown && walletExists()) {
|
||||
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -761,7 +779,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
||||
syncNormalStartTime = System.currentTimeMillis();
|
||||
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
||||
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getRefreshPeriodMs());
|
||||
UserThread.runAfter(() -> {
|
||||
if (!isShutDown && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
|
||||
}, syncNormalDuration);
|
||||
|
@ -772,7 +790,11 @@ public abstract class Trade implements Tradable, Model {
|
|||
if (getBuyer().getUpdatedMultisigHex() != null) multisigHexes.add(getBuyer().getUpdatedMultisigHex());
|
||||
if (getSeller().getUpdatedMultisigHex() != null) multisigHexes.add(getSeller().getUpdatedMultisigHex());
|
||||
if (getArbitrator().getUpdatedMultisigHex() != null) multisigHexes.add(getArbitrator().getUpdatedMultisigHex());
|
||||
if (!multisigHexes.isEmpty()) getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
|
||||
if (!multisigHexes.isEmpty()) {
|
||||
log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId());
|
||||
getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
|
||||
log.info("Done importing multisig hex for {} {}", getClass().getSimpleName(), getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void changeWalletPassword(String oldPassword, String newPassword) {
|
||||
|
@ -791,13 +813,22 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
private void closeWallet() {
|
||||
synchronized (walletLock) {
|
||||
if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId());
|
||||
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
|
||||
stopPolling();
|
||||
xmrWalletService.closeWallet(wallet, true);
|
||||
wallet = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopWallet() {
|
||||
synchronized (walletLock) {
|
||||
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
|
||||
stopPolling();
|
||||
xmrWalletService.stopWallet(wallet, wallet.getPath(), true);
|
||||
wallet = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteWallet() {
|
||||
synchronized (walletLock) {
|
||||
if (walletExists()) {
|
||||
|
@ -808,14 +839,10 @@ public abstract class Trade implements Tradable, Model {
|
|||
throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked");
|
||||
}
|
||||
|
||||
// check if wallet balance > dust
|
||||
BigInteger maxBalance = isDepositsPublished() ? getMakerDepositTx().getFee().min(getTakerDepositTx().getFee()) : BigInteger.ZERO;
|
||||
if (getWallet().getBalance().compareTo(maxBalance) > 0) {
|
||||
throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because its balance is more than dust");
|
||||
}
|
||||
// force stop the wallet
|
||||
if (wallet != null) stopWallet();
|
||||
|
||||
// close and delete trade wallet
|
||||
if (wallet != null) closeWallet();
|
||||
// delete wallet
|
||||
log.info("Deleting wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
xmrWalletService.deleteWallet(getWalletName());
|
||||
|
||||
|
@ -882,8 +909,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
// check connection to monero daemon
|
||||
checkWalletConnection();
|
||||
|
||||
// import multisig hex
|
||||
importMultisigHex();
|
||||
// check multisig import
|
||||
if (getWallet().isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
|
||||
|
||||
// gather info
|
||||
|
@ -979,8 +1005,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
||||
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
||||
|
||||
// check wallet's daemon connection
|
||||
checkWalletConnection();
|
||||
// check wallet connection
|
||||
if (sign || publish) checkWalletConnection();
|
||||
|
||||
// handle tx signing
|
||||
if (sign) {
|
||||
|
@ -1005,18 +1031,11 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// verify fee is within tolerance by recreating payout tx
|
||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||
MoneroTxWallet feeEstimateTx = null;
|
||||
try {
|
||||
feeEstimateTx = createPayoutTx();
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not recreate payout tx to verify fee: " + e.getMessage());
|
||||
}
|
||||
if (feeEstimateTx != null) {
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||
}
|
||||
MoneroTxWallet feeEstimateTx = createPayoutTx();;
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||
}
|
||||
|
||||
// update trade state
|
||||
|
@ -1072,7 +1091,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
if (depositId == null) return null;
|
||||
try {
|
||||
if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) {
|
||||
trader.setDepositTx(getTxFromWalletOrDaemon(depositId));
|
||||
trader.setDepositTx(getDepositTxFromWalletOrDaemon(depositId));
|
||||
}
|
||||
return trader.getDepositTx();
|
||||
} catch (MoneroError e) {
|
||||
|
@ -1081,12 +1100,18 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
}
|
||||
|
||||
private MoneroTx getTxFromWalletOrDaemon(String txId) {
|
||||
private MoneroTx getDepositTxFromWalletOrDaemon(String txId) {
|
||||
MoneroTx tx = null;
|
||||
|
||||
// first check wallet
|
||||
if (getWallet() != null) {
|
||||
try { tx = getWallet().getTx(txId); } // TODO monero-java: return null if tx not found
|
||||
catch (Exception e) { }
|
||||
List<MoneroTxWallet> filteredTxs = getWallet().getTxs(new MoneroTxQuery()
|
||||
.setHash(txId)
|
||||
.setIsConfirmed(isDepositsConfirmed() ? true : null)); // avoid checking pool if confirmed
|
||||
if (filteredTxs.size() == 1) tx = filteredTxs.get(0);
|
||||
}
|
||||
|
||||
// then check daemon
|
||||
if (tx == null) tx = getXmrWalletService().getTxWithCache(txId);
|
||||
return tx;
|
||||
}
|
||||
|
@ -1126,19 +1151,27 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
public void onShutDownStarted() {
|
||||
isShutDownStarted = true;
|
||||
if (wallet != null) log.info("{} {} onShutDownStarted()", getClass().getSimpleName(), getId());
|
||||
synchronized (this) {
|
||||
synchronized (walletLock) {
|
||||
stopPolling(); // allow locks to release before stopping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
if (wallet != null) log.info("{} {} onShutDown()", getClass().getSimpleName(), getId());
|
||||
synchronized (this) {
|
||||
log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
|
||||
isInitialized = false;
|
||||
isShutDown = true;
|
||||
if (wallet != null) closeWallet();
|
||||
synchronized (walletLock) {
|
||||
if (wallet != null) closeWallet();
|
||||
}
|
||||
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
||||
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
||||
if (idlePayoutSyncer != null) {
|
||||
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
||||
idlePayoutSyncer = null;
|
||||
}
|
||||
log.info("Done shutting down {} {}", getClass().getSimpleName(), getId());
|
||||
idlePayoutSyncer = null; // main wallet removes listener itself
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1280,6 +1313,18 @@ public abstract class Trade implements Tradable, Model {
|
|||
errorMessageProperty.set(errorMessage);
|
||||
}
|
||||
|
||||
public void prependErrorMessage(String errorMessage) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(errorMessage);
|
||||
if (this.errorMessage != null && !this.errorMessage.isEmpty()) {
|
||||
sb.append("\n\n---- Previous Error -----\n\n");
|
||||
sb.append(this.errorMessage);
|
||||
}
|
||||
String appendedErrorMessage = sb.toString();
|
||||
this.errorMessage = appendedErrorMessage;
|
||||
errorMessageProperty.set(appendedErrorMessage);
|
||||
}
|
||||
|
||||
public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) {
|
||||
this.assetTxProofResult = assetTxProofResult;
|
||||
assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1);
|
||||
|
@ -1434,6 +1479,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
final long tradeTime = getTakeOfferDate().getTime();
|
||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
|
||||
if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
|
||||
|
||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||
|
||||
|
@ -1465,7 +1512,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
public boolean isDepositsPublished() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal();
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
|
||||
}
|
||||
|
||||
public boolean isFundsLockedIn() {
|
||||
|
@ -1473,11 +1520,11 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
public boolean isDepositsConfirmed() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
||||
return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isDepositsUnlocked() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
||||
return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isPaymentSent() {
|
||||
|
@ -1616,7 +1663,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
*/
|
||||
public long getReprocessDelayInSeconds(int reprocessCount) {
|
||||
int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection)
|
||||
if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs() / 1000;
|
||||
if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getRefreshPeriodMs() / 1000;
|
||||
long delay = 60;
|
||||
for (int i = retryCycles; i < reprocessCount; i++) delay *= 2;
|
||||
return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay);
|
||||
|
@ -1642,15 +1689,27 @@ public abstract class Trade implements Tradable, Model {
|
|||
return tradeVolumeProperty;
|
||||
}
|
||||
|
||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
synchronized (walletLock) {
|
||||
if (isShutDown) return;
|
||||
MoneroWallet wallet = getWallet();
|
||||
if (wallet == null) return;
|
||||
|
||||
// check if ignored
|
||||
if (isShutDownStarted) return;
|
||||
if (getWallet() == null) return;
|
||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
||||
|
||||
// set daemon connection (must restart monero-wallet-rpc if proxy uri changed)
|
||||
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||
String newProxyUri = connection == null ? null : connection.getProxyUri();
|
||||
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||
wallet.setDaemonConnection(connection);
|
||||
if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) {
|
||||
log.info("Restarting monero-wallet-rpc for trade wallet to set proxy URI {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||
closeWallet();
|
||||
wallet = getWallet();
|
||||
} else {
|
||||
wallet.setDaemonConnection(connection);
|
||||
}
|
||||
updateWalletRefreshPeriod();
|
||||
|
||||
|
||||
// sync and reprocess messages on new thread
|
||||
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
||||
HavenoUtils.submitTask(() -> {
|
||||
|
@ -1665,14 +1724,14 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
private void updateSyncing() {
|
||||
if (isShutDown) return;
|
||||
if (isShutDownStarted) return;
|
||||
if (!isIdling()) {
|
||||
updateWalletRefreshPeriod();
|
||||
trySyncWallet();
|
||||
} else {
|
||||
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
||||
UserThread.runAfter(() -> {
|
||||
if (!isShutDown) {
|
||||
if (!isShutDownStarted) {
|
||||
updateWalletRefreshPeriod();
|
||||
trySyncWallet();
|
||||
}
|
||||
|
@ -1680,13 +1739,13 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateWalletRefreshPeriod() {
|
||||
public void updateWalletRefreshPeriod() {
|
||||
setWalletRefreshPeriod(getWalletRefreshPeriod());
|
||||
}
|
||||
|
||||
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
||||
synchronized (walletLock) {
|
||||
if (this.isShutDown) return;
|
||||
if (this.isShutDownStarted) return;
|
||||
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
||||
this.walletRefreshPeriod = walletRefreshPeriod;
|
||||
if (getWallet() != null) {
|
||||
|
@ -1702,7 +1761,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
synchronized (walletLock) {
|
||||
if (txPollLooper != null) return;
|
||||
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
txPollLooper = new TaskLooper(() -> { pollWallet(); });
|
||||
txPollLooper = new TaskLooper(() -> pollWallet());
|
||||
txPollLooper.start(walletRefreshPeriod);
|
||||
}
|
||||
}
|
||||
|
@ -1719,53 +1778,61 @@ public abstract class Trade implements Tradable, Model {
|
|||
private void pollWallet() {
|
||||
try {
|
||||
|
||||
// skip if either deposit tx id is unknown
|
||||
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) return;
|
||||
|
||||
// skip if payout unlocked
|
||||
if (isPayoutUnlocked()) return;
|
||||
|
||||
// rescan spent if deposits unlocked
|
||||
if (isDepositsUnlocked()) getWallet().rescanSpent();
|
||||
// rescan spent outputs to detect payout tx after deposits unlocked
|
||||
if (isDepositsUnlocked() && !isPayoutPublished()) getWallet().rescanSpent();
|
||||
|
||||
// get txs with outputs
|
||||
List<MoneroTxWallet> txs;
|
||||
try {
|
||||
txs = getWallet().getTxs(new MoneroTxQuery()
|
||||
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
|
||||
.setIncludeOutputs(true));
|
||||
} catch (Exception e) {
|
||||
if (!isShutDown) log.info("Could not fetch deposit txs from wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // expected at first
|
||||
return;
|
||||
// get txs from trade wallet
|
||||
boolean payoutExpected = isPaymentReceived() || processModel.getPaymentReceivedMessage() != null || disputeState.ordinal() > DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || processModel.getDisputeClosedMessage() != null;
|
||||
boolean checkPool = !isDepositsConfirmed() || (!isPayoutConfirmed() && payoutExpected);
|
||||
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
|
||||
if (!checkPool) query.setInTxPool(false); // avoid pool check if possible
|
||||
List<MoneroTxWallet> txs = wallet.getTxs(query);
|
||||
|
||||
// warn on double spend // TODO: other handling?
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId());
|
||||
}
|
||||
|
||||
// check deposit txs
|
||||
if (!isDepositsUnlocked()) {
|
||||
if (txs.size() == 2) {
|
||||
|
||||
// update trader state
|
||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
|
||||
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
|
||||
|
||||
// set security deposits
|
||||
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
|
||||
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
|
||||
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
|
||||
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
|
||||
getSeller().setSecurityDeposit(sellerSecurityDeposit);
|
||||
}
|
||||
|
||||
// set deposits published state
|
||||
setStateDepositsPublished();
|
||||
|
||||
// check if deposit txs confirmed
|
||||
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
|
||||
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
|
||||
|
||||
// update trader txs
|
||||
MoneroTxWallet makerDepositTx = null;
|
||||
MoneroTxWallet takerDepositTx = null;
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
if (tx.getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositTx = tx;
|
||||
if (tx.getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositTx = tx;
|
||||
}
|
||||
getMaker().setDepositTx(makerDepositTx);
|
||||
getTaker().setDepositTx(takerDepositTx);
|
||||
|
||||
// skip if deposit txs not seen
|
||||
if (makerDepositTx == null || takerDepositTx == null) return;
|
||||
|
||||
// set security deposits
|
||||
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
|
||||
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
|
||||
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
|
||||
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
|
||||
getSeller().setSecurityDeposit(sellerSecurityDeposit);
|
||||
}
|
||||
|
||||
// update state
|
||||
setStateDepositsPublished();
|
||||
if (makerDepositTx.isConfirmed() && takerDepositTx.isConfirmed()) setStateDepositsConfirmed();
|
||||
if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) setStateDepositsUnlocked();
|
||||
}
|
||||
|
||||
// check payout tx
|
||||
else {
|
||||
if (isDepositsUnlocked()) {
|
||||
|
||||
// check if deposit txs spent (appears on payout published)
|
||||
// check if any outputs spent (observed on payout published)
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
|
||||
if (Boolean.TRUE.equals(output.isSpent())) {
|
||||
|
@ -1775,19 +1842,18 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
|
||||
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
|
||||
if (!outgoingTxs.isEmpty()) {
|
||||
MoneroTxWallet payoutTx = outgoingTxs.get(0);
|
||||
setPayoutTx(payoutTx);
|
||||
setPayoutStatePublished();
|
||||
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
|
||||
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
if (tx.isOutgoing()) {
|
||||
setPayoutTx(tx);
|
||||
setPayoutStatePublished();
|
||||
if (tx.isConfirmed()) setPayoutStateConfirmed();
|
||||
if (!tx.isLocked()) setPayoutStateUnlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!isShutDown && getWallet() != null && isWalletConnected()) {
|
||||
log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1795,7 +1861,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
private long getWalletRefreshPeriod() {
|
||||
if (isIdling()) return IDLE_SYNC_PERIOD_MS;
|
||||
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
||||
return xmrWalletService.getConnectionsService().getRefreshPeriodMs();
|
||||
}
|
||||
|
||||
private void setStateDepositsPublished() {
|
||||
|
@ -1867,7 +1933,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
} catch (Exception e) {
|
||||
processing = false;
|
||||
e.printStackTrace();
|
||||
if (isInitialized && !isShutDown && !isWalletConnected()) throw e;
|
||||
if (isInitialized && !isShutDownStarted && !isWalletConnected()) throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package haveno.core.trade;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.ClockWatcher;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.crypto.KeyRing;
|
||||
|
@ -67,6 +69,7 @@ import haveno.core.user.User;
|
|||
import haveno.core.util.Validator;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.BootstrapListener;
|
||||
import haveno.network.p2p.DecryptedDirectMessageListener;
|
||||
import haveno.network.p2p.DecryptedMessageWithPubKey;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
|
@ -85,6 +88,7 @@ import monero.wallet.model.MoneroOutputQuery;
|
|||
import org.bitcoinj.core.Coin;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -198,6 +202,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
p2PService.addDecryptedDirectMessageListener(this);
|
||||
|
||||
failedTradesManager.setUnFailTradeCallback(this::unFailTrade);
|
||||
|
||||
// initialize trades when connected to p2p network
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
public void onUpdatedDataReceived() {
|
||||
initPersistedTrades();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -249,27 +260,24 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
|
||||
public void onAllServicesInitialized() {
|
||||
|
||||
// initialize
|
||||
initialize();
|
||||
|
||||
// listen for account updates
|
||||
accountService.addListener(new AccountServiceListener() {
|
||||
|
||||
@Override
|
||||
public void onAccountCreated() {
|
||||
log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()");
|
||||
initialize();
|
||||
log.info(TradeManager.class + ".accountService.onAccountCreated()");
|
||||
initPersistedTrades();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccountOpened() {
|
||||
log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()");
|
||||
initialize();
|
||||
log.info(TradeManager.class + ".accountService.onAccountOpened()");
|
||||
initPersistedTrades();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccountClosed() {
|
||||
log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()");
|
||||
log.info(TradeManager.class + ".accountService.onAccountClosed()");
|
||||
closeAllTrades();
|
||||
}
|
||||
|
||||
|
@ -280,21 +288,36 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
});
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
public void onShutDownStarted() {
|
||||
log.info("{}.onShutDownStarted()", getClass().getSimpleName());
|
||||
|
||||
// initialize trades off main thread
|
||||
new Thread(() -> initPersistedTrades()).start();
|
||||
// collect trades to prepare
|
||||
Set<Trade> trades = new HashSet<Trade>();
|
||||
trades.addAll(tradableList.getList());
|
||||
trades.addAll(closedTradableManager.getClosedTrades());
|
||||
trades.addAll(failedTradesManager.getObservableList());
|
||||
|
||||
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
|
||||
onTradesChanged();
|
||||
|
||||
xmrWalletService.setTradeManager(this);
|
||||
|
||||
// thaw unreserved outputs
|
||||
thawUnreservedOutputs();
|
||||
// prepare to shut down trades in parallel
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
for (Trade trade : trades) tasks.add(() -> {
|
||||
try {
|
||||
trade.onShutDownStarted();
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("Connection reset")) return; // expected if shut down with ctrl+c
|
||||
log.warn("Error notifying {} {} that shut down started {}", getClass().getSimpleName(), trade.getId());
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
try {
|
||||
HavenoUtils.executeTasks(tasks);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error notifying trades that shut down started: {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
log.info("Shutting down {}", getClass().getSimpleName());
|
||||
isShutDown = true;
|
||||
closeAllTrades();
|
||||
}
|
||||
|
@ -313,7 +336,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
try {
|
||||
trade.shutDown();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
if (e.getMessage() != null && (e.getMessage().contains("Connection reset") || e.getMessage().contains("Connection refused"))) return; // expected if shut down with ctrl+c
|
||||
log.warn("Error closing {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
try {
|
||||
|
@ -372,54 +397,80 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void initPersistedTrades() {
|
||||
log.info("Initializing persisted trades");
|
||||
|
||||
// get all trades
|
||||
List<Trade> trades = getAllTrades();
|
||||
// initialize off main thread
|
||||
new Thread(() -> {
|
||||
|
||||
// open trades in parallel since each may open a multisig wallet
|
||||
log.info("Initializing trades");
|
||||
int threadPoolSize = 10;
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
for (Trade trade : trades) {
|
||||
tasks.add(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// get all trades
|
||||
List<Trade> trades = getAllTrades();
|
||||
|
||||
// initialize trades in parallel
|
||||
int threadPoolSize = 10;
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
for (Trade trade : trades) {
|
||||
tasks.add(() -> {
|
||||
initPersistedTrade(trade);
|
||||
}
|
||||
});
|
||||
};
|
||||
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
||||
log.info("Done initializing trades");
|
||||
|
||||
// reset any available address entries
|
||||
if (isShutDown) return;
|
||||
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
||||
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
||||
.forEach(addressEntry -> {
|
||||
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||
// remove trade if protocol didn't initialize
|
||||
if (getOpenTrade(trade.getId()).isPresent() && !trade.isDepositRequested()) {
|
||||
log.warn("Removing persisted {} {} with uid={} because it did not finish initializing (state={})", trade.getClass().getSimpleName(), trade.getId(), trade.getUid(), trade.getState());
|
||||
removeTradeOnError(trade);
|
||||
}
|
||||
});
|
||||
};
|
||||
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
||||
log.info("Done initializing persisted trades");
|
||||
if (isShutDown) return;
|
||||
|
||||
// notify that persisted trades initialized
|
||||
persistedTradesInitialized.set(true);
|
||||
|
||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
||||
Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
||||
nonFailedTrades.addAll(tradableList.getList());
|
||||
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
||||
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
||||
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
|
||||
|
||||
// sync idle trades once in background after active trades
|
||||
for (Trade trade : trades) {
|
||||
if (trade.isIdling()) {
|
||||
HavenoUtils.submitTask(() -> trade.syncWallet());
|
||||
// sync idle trades once in background after active trades
|
||||
for (Trade trade : trades) {
|
||||
if (trade.isIdling()) {
|
||||
HavenoUtils.submitTask(() -> trade.syncWallet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
|
||||
onTradesChanged();
|
||||
|
||||
xmrWalletService.setTradeManager(this);
|
||||
|
||||
// process after all wallets initialized
|
||||
MonadicBinding<Boolean> walletsInitialized = EasyBind.combine(HavenoUtils.havenoSetup.getWalletInitialized(), persistedTradesInitialized, (a, b) -> a && b);
|
||||
walletsInitialized.subscribe((observable, oldValue, newValue) -> {
|
||||
if (!newValue) return;
|
||||
|
||||
// thaw unreserved outputs
|
||||
thawUnreservedOutputs();
|
||||
|
||||
// reset any available funded address entries
|
||||
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
||||
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
||||
.forEach(addressEntry -> {
|
||||
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||
});
|
||||
});
|
||||
|
||||
// notify that persisted trades initialized
|
||||
persistedTradesInitialized.set(true);
|
||||
|
||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
||||
// TODO: remove stats?
|
||||
Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
||||
nonFailedTrades.addAll(tradableList.getList());
|
||||
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
||||
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
||||
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
|
||||
}).start();
|
||||
|
||||
// allow execution to start
|
||||
GenUtils.waitFor(100);
|
||||
}
|
||||
|
||||
private void initPersistedTrade(Trade trade) {
|
||||
if (isShutDown) return;
|
||||
if (getTradeProtocol(trade) != null) return;
|
||||
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
||||
requestPersistence();
|
||||
scheduleDeletionIfUnfunded(trade);
|
||||
|
@ -1100,16 +1151,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
|
||||
private void addTrade(Trade trade) {
|
||||
synchronized(tradableList) {
|
||||
synchronized (tradableList) {
|
||||
if (tradableList.add(trade)) {
|
||||
requestPersistence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeTrade(Trade trade) {
|
||||
private void removeTrade(Trade trade) {
|
||||
log.info("TradeManager.removeTrade() " + trade.getId());
|
||||
synchronized(tradableList) {
|
||||
synchronized (tradableList) {
|
||||
if (!tradableList.contains(trade)) return;
|
||||
|
||||
// remove trade
|
||||
|
@ -1121,9 +1172,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
}
|
||||
|
||||
private synchronized void removeTradeOnError(Trade trade) {
|
||||
private void removeTradeOnError(Trade trade) {
|
||||
log.info("TradeManager.removeTradeOnError() " + trade.getId());
|
||||
synchronized(tradableList) {
|
||||
synchronized (tradableList) {
|
||||
if (!tradableList.contains(trade)) return;
|
||||
|
||||
// unreserve taker key images
|
||||
|
@ -1150,18 +1201,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
|
||||
private void scheduleDeletionIfUnfunded(Trade trade) {
|
||||
if (trade.isDepositRequested() && !trade.isDepositsPublished()) {
|
||||
log.warn("Scheduling to delete trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
if (getOpenTrade(trade.getId()).isPresent() && trade.isDepositRequested() && !trade.isDepositsPublished()) {
|
||||
log.warn("Scheduling to delete open trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
UserThread.runAfter(() -> {
|
||||
if (isShutDown) return;
|
||||
|
||||
// get trade's deposit txs from daemon
|
||||
MoneroTx makerDepositTx = xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());
|
||||
MoneroTx takerDepositTx = xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash());
|
||||
MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());
|
||||
MoneroTx takerDepositTx = trade.getTaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash());
|
||||
|
||||
// delete multisig trade wallet if neither deposit tx published
|
||||
if (makerDepositTx == null && takerDepositTx == null) {
|
||||
log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId());
|
||||
log.warn("Deleting {} {} after protocol error", trade.getClass().getSimpleName(), trade.getId());
|
||||
removeTrade(trade);
|
||||
failedTradesManager.removeTrade(trade);
|
||||
if (trade.walletExists()) trade.deleteWallet();
|
||||
|
|
|
@ -182,7 +182,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
|||
",\n reserveTxHex=" + reserveTxHex +
|
||||
",\n reserveTxKey=" + reserveTxKey +
|
||||
",\n payoutAddress=" + payoutAddress +
|
||||
",\n makerSignature=" + Utilities.byteArrayToInteger(makerSignature) +
|
||||
",\n makerSignature=" + (makerSignature == null ? null : Utilities.byteArrayToInteger(makerSignature)) +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,10 +141,11 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
|||
@Override
|
||||
public String toString() {
|
||||
return "PaymentSentMessage{" +
|
||||
",\n tradeId=" + tradeId +
|
||||
",\n uid='" + uid + '\'' +
|
||||
",\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n counterCurrencyTxId=" + counterCurrencyTxId +
|
||||
",\n counterCurrencyExtraData=" + counterCurrencyExtraData +
|
||||
",\n uid='" + uid + '\'' +
|
||||
",\n payoutTxHex=" + payoutTxHex +
|
||||
",\n updatedMultisigHex=" + updatedMultisigHex +
|
||||
",\n paymentAccountKey=" + paymentAccountKey +
|
||||
|
|
|
@ -324,7 +324,7 @@ public class FluentProtocol {
|
|||
log.info(info);
|
||||
return Result.VALID.info(info);
|
||||
} else {
|
||||
String info = MessageFormat.format("We received a {0} but we are are not in the expected state. " +
|
||||
String info = MessageFormat.format("We received a {0} but we are not in the expected state. " +
|
||||
"Expected states={1}, Trade state= {2}, tradeId={3}",
|
||||
trigger,
|
||||
expectedStates,
|
||||
|
|
|
@ -52,7 +52,7 @@ import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage;
|
|||
import haveno.core.trade.protocol.tasks.ProcessSignContractRequest;
|
||||
import haveno.core.trade.protocol.tasks.ProcessSignContractResponse;
|
||||
import haveno.core.trade.protocol.tasks.RemoveOffer;
|
||||
import haveno.core.trade.protocol.tasks.ResendDisputeClosedMessageWithPayout;
|
||||
import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout;
|
||||
import haveno.core.trade.protocol.tasks.TradeTask;
|
||||
import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
|
||||
import haveno.core.util.Validator;
|
||||
|
@ -242,32 +242,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
}
|
||||
|
||||
protected void onInitialized() {
|
||||
if (!trade.isCompleted()) {
|
||||
processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||
}
|
||||
|
||||
// initialize trade
|
||||
trade.initialize(processModel.getProvider());
|
||||
// listen for direct messages unless completed
|
||||
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||
|
||||
// initialize trade with lock
|
||||
synchronized (trade) {
|
||||
trade.initialize(processModel.getProvider());
|
||||
}
|
||||
|
||||
// process mailbox messages
|
||||
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
||||
mailboxMessageService.addDecryptedMailboxListener(this);
|
||||
if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this);
|
||||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||
|
||||
// send deposit confirmed message on startup or event
|
||||
if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) {
|
||||
new Thread(() -> sendDepositsConfirmedMessages()).start();
|
||||
} else {
|
||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) {
|
||||
new Thread(() -> sendDepositsConfirmedMessages()).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// reprocess payout messages if pending
|
||||
maybeReprocessPaymentReceivedMessage(true);
|
||||
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(trade, true);
|
||||
}
|
||||
|
||||
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
||||
|
@ -448,7 +435,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
.setup(tasks(
|
||||
ProcessDepositsConfirmedMessage.class,
|
||||
VerifyPeersAccountAgeWitness.class,
|
||||
ResendDisputeClosedMessageWithPayout.class)
|
||||
MaybeResendDisputeClosedMessageWithPayout.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(sender, response);
|
||||
|
@ -475,7 +462,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
// the mailbox msg once wallet is ready and trade state set.
|
||||
synchronized (trade) {
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||
log.warn("Ignoring PaymentSentMessage which was already processed");
|
||||
log.warn("Received another PaymentSentMessage which was already processed, ACKing");
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
|
@ -518,6 +506,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
return;
|
||||
}
|
||||
synchronized (trade) {
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) {
|
||||
log.warn("Received another PaymentReceivedMessage which was already processed, ACKing");
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
|
@ -844,7 +837,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
}
|
||||
}
|
||||
|
||||
private void sendDepositsConfirmedMessages() {
|
||||
public void maybeSendDepositsConfirmedMessages() {
|
||||
synchronized (trade) {
|
||||
if (!trade.isInitialized()) return; // skip if shutting down
|
||||
if (trade.getProcessModel().isDepositsConfirmedMessagesDelivered()) return; // skip if already delivered
|
||||
|
@ -860,7 +853,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
|
||||
// retry in 15 minutes
|
||||
UserThread.runAfter(() -> {
|
||||
sendDepositsConfirmedMessages();
|
||||
maybeSendDepositsConfirmedMessages();
|
||||
}, 15, TimeUnit.MINUTES);
|
||||
handleTaskRunnerFault(null, null, "SendDepositsConfirmedMessages", errorMessage);
|
||||
})))
|
||||
|
|
|
@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
|
|||
trade.getSelf().getReserveTxHash(),
|
||||
trade.getSelf().getReserveTxHex(),
|
||||
trade.getSelf().getReserveTxKey(),
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString(),
|
||||
null);
|
||||
|
||||
// send request to arbitrator
|
||||
|
|
|
@ -32,10 +32,10 @@ import java.util.List;
|
|||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
public class ResendDisputeClosedMessageWithPayout extends TradeTask {
|
||||
public class MaybeResendDisputeClosedMessageWithPayout extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) {
|
||||
public MaybeResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||
trade.getSelf().setDepositTx(depositTx);
|
||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
|
||||
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address?
|
||||
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
|
||||
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
|
||||
|
||||
// maker signs deposit hash nonce to avoid challenge protocol
|
||||
|
|
|
@ -53,14 +53,21 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
|
|||
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
|
||||
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
|
||||
|
||||
// update multisig hex
|
||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
|
||||
// decrypt seller payment account payload if key given
|
||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
||||
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
|
||||
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
||||
}
|
||||
processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails
|
||||
|
||||
// update multisig hex
|
||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// persist and complete
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
|
|
@ -111,6 +111,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||
processModel.setMultisigAddress(result.getAddress());
|
||||
trade.saveWallet(); // save multisig wallet on completion
|
||||
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED);
|
||||
trade.updateWalletRefreshPeriod(); // starts syncing
|
||||
}
|
||||
|
||||
// update multisig participants if new state to communicate
|
||||
|
|
|
@ -113,7 +113,8 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||
|
||||
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
|
||||
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
||||
boolean deferSignAndPublish = trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout();
|
||||
if (deferSignAndPublish) {
|
||||
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||
|
@ -135,6 +136,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||
trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
trade.syncWallet();
|
||||
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
||||
else throw e;
|
||||
}
|
||||
|
|
|
@ -47,10 +47,7 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||
// update latest peer address
|
||||
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
|
||||
|
||||
// if seller, decrypt buyer's payment account payload
|
||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
|
||||
// update state
|
||||
// update state from message
|
||||
processModel.setPaymentSentMessage(message);
|
||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
|
@ -59,6 +56,20 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
||||
|
||||
// if seller, decrypt buyer's payment account payload
|
||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
trade.requestPersistence();
|
||||
|
||||
// import multisig hex
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// update state
|
||||
trade.advanceState(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||
trade.requestPersistence();
|
||||
complete();
|
||||
|
|
|
@ -58,8 +58,8 @@ public abstract class SendMailboxMessageTask extends TradeTask {
|
|||
TradeMailboxMessage message = getTradeMailboxMessage(id);
|
||||
setStateSent();
|
||||
NodeAddress peersNodeAddress = getReceiverNodeAddress();
|
||||
log.info("Send {} to peer {} for {} {}", trade.getClass().getSimpleName(), trade.getId(),
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
log.info("Send {} to peer {} for {} {}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, trade.getClass().getSimpleName(), trade.getId(), message.getUid());
|
||||
|
||||
TradeTask task = this;
|
||||
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
|
||||
|
|
|
@ -43,7 +43,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||
BigInteger takerFee = trade.getTakerFee();
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
||||
String returnAddress = model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
||||
|
||||
// collect reserved key images
|
||||
|
|
|
@ -78,7 +78,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
||||
}
|
||||
|
||||
NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress());
|
||||
NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress(), "Arbitrator address is null", trade.getClass().getSimpleName(), trade.getId());
|
||||
|
||||
// The first 4 chars are sufficient to identify an arbitrator.
|
||||
// For testing with regtest/localhost we use the full address as its localhost and would result in
|
||||
|
|
|
@ -170,7 +170,13 @@ public class TradeStatisticsManager {
|
|||
return;
|
||||
}
|
||||
|
||||
TradeStatistics3 tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
||||
TradeStatistics3 tradeStatistics3 = null;
|
||||
try {
|
||||
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
|
||||
return;
|
||||
}
|
||||
boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash()));
|
||||
if (hasTradeStatistics3) {
|
||||
log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue