mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
refactor trade protocol error handling and wallet deletion
This commit is contained in:
parent
b034ac8c13
commit
6fb846d783
10 changed files with 233 additions and 205 deletions
|
@ -50,6 +50,7 @@ import haveno.core.monetary.Volume;
|
||||||
import haveno.core.network.MessageState;
|
import haveno.core.network.MessageState;
|
||||||
import haveno.core.offer.Offer;
|
import haveno.core.offer.Offer;
|
||||||
import haveno.core.offer.OfferDirection;
|
import haveno.core.offer.OfferDirection;
|
||||||
|
import haveno.core.offer.OpenOffer;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.proto.CoreProtoResolver;
|
import haveno.core.proto.CoreProtoResolver;
|
||||||
import haveno.core.proto.network.CoreNetworkProtoResolver;
|
import haveno.core.proto.network.CoreNetworkProtoResolver;
|
||||||
|
@ -73,12 +74,14 @@ import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
@ -133,14 +136,18 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
|
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
|
||||||
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
||||||
private static final long DELETE_BACKUPS_AFTER_NUM_BLOCKS = 3600; // ~5 days
|
|
||||||
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
|
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
|
||||||
|
private static final long DELETE_AFTER_NUM_BLOCKS = 1; // if deposit requested but not published
|
||||||
|
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
|
||||||
private final Object walletLock = new Object();
|
private final Object walletLock = new Object();
|
||||||
private final Object pollLock = new Object();
|
private final Object pollLock = new Object();
|
||||||
|
private final LongProperty walletHeight = new SimpleLongProperty(0);
|
||||||
private MoneroWallet wallet;
|
private MoneroWallet wallet;
|
||||||
boolean wasWalletSynced;
|
boolean wasWalletSynced;
|
||||||
boolean pollInProgress;
|
boolean pollInProgress;
|
||||||
boolean restartInProgress;
|
boolean restartInProgress;
|
||||||
|
private Subscription protocolErrorStateSubscription;
|
||||||
|
private Subscription protocolErrorHeightSubscription;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Enums
|
// Enums
|
||||||
|
@ -643,6 +650,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (!isInitialized || isShutDownStarted) return;
|
if (!isInitialized || isShutDownStarted) return;
|
||||||
ThreadUtils.submitToPool(() -> {
|
ThreadUtils.submitToPool(() -> {
|
||||||
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
|
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
|
||||||
|
if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages());
|
||||||
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
|
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
|
||||||
if (isPaymentReceived()) {
|
if (isPaymentReceived()) {
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
|
@ -785,6 +793,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getHeight() {
|
||||||
|
return walletHeight.get();
|
||||||
|
}
|
||||||
|
|
||||||
private String getWalletName() {
|
private String getWalletName() {
|
||||||
return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid();
|
return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid();
|
||||||
}
|
}
|
||||||
|
@ -814,7 +826,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (!xmrConnectionService.isSyncedWithinTolerance()) return false;
|
if (!xmrConnectionService.isSyncedWithinTolerance()) return false;
|
||||||
Long targetHeight = xmrConnectionService.getTargetHeight();
|
Long targetHeight = xmrConnectionService.getTargetHeight();
|
||||||
if (targetHeight == null) return false;
|
if (targetHeight == null) return false;
|
||||||
if (targetHeight - wallet.getHeight() <= 3) return true; // synced if within 3 blocks of target height
|
if (targetHeight - walletHeight.get() <= 3) return true; // synced if within 3 blocks of target height
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -941,7 +953,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wallet must be synced
|
// wallet must be synced
|
||||||
if (isDepositRequested() && !isSyncedWithinTolerance()) {
|
if (isDepositRequested() && isWalletBehind()) {
|
||||||
log.warn("Wallet is not synced for {} {}, syncing", getClass().getSimpleName(), getId());
|
log.warn("Wallet is not synced for {} {}, syncing", getClass().getSimpleName(), getId());
|
||||||
syncWallet(true);
|
syncWallet(true);
|
||||||
}
|
}
|
||||||
|
@ -966,19 +978,9 @@ public abstract class Trade implements Tradable, Model {
|
||||||
forceCloseWallet();
|
forceCloseWallet();
|
||||||
|
|
||||||
// delete wallet
|
// delete wallet
|
||||||
log.info("Deleting wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Deleting wallet and backups for {} {}", getClass().getSimpleName(), getId());
|
||||||
xmrWalletService.deleteWallet(getWalletName());
|
xmrWalletService.deleteWallet(getWalletName());
|
||||||
|
xmrWalletService.deleteWalletBackups(getWalletName());
|
||||||
// delete trade wallet backups if empty and payout unlocked, else schedule
|
|
||||||
if (isPayoutUnlocked() || !isDepositRequested() || isDepositFailed()) {
|
|
||||||
xmrWalletService.deleteWalletBackups(getWalletName());
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// schedule backup deletion by recording delete height
|
|
||||||
log.warn("Scheduling to delete backup wallet for " + getClass().getSimpleName() + " " + getId() + " in the small chance it becomes funded");
|
|
||||||
processModel.setDeleteBackupsHeight(xmrConnectionService.getLastInfo().getHeight() + DELETE_BACKUPS_AFTER_NUM_BLOCKS);
|
|
||||||
maybeScheduleDeleteBackups();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(e.getMessage());
|
log.warn(e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -1290,9 +1292,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
private void clearProcessData() {
|
private void clearProcessData() {
|
||||||
|
|
||||||
// delete backup wallets after main wallet + blocks
|
|
||||||
maybeScheduleDeleteBackups();
|
|
||||||
|
|
||||||
// delete trade wallet
|
// delete trade wallet
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (!walletExists()) return; // done if already cleared
|
if (!walletExists()) return; // done if already cleared
|
||||||
|
@ -1310,27 +1309,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeScheduleDeleteBackups() {
|
|
||||||
if (processModel.getDeleteBackupsHeight() == 0) return;
|
|
||||||
if (xmrConnectionService.getLastInfo().getHeight() >= processModel.getDeleteBackupsHeight()) {
|
|
||||||
xmrWalletService.deleteWalletBackups(getWalletName());
|
|
||||||
processModel.setDeleteBackupsHeight(0); // reset delete height
|
|
||||||
} else {
|
|
||||||
MoneroWalletListener deleteBackupsListener = new MoneroWalletListener() {
|
|
||||||
@Override
|
|
||||||
public synchronized void onNewBlock(long height) { // prevent concurrent deletion
|
|
||||||
if (processModel.getDeleteBackupsHeight() == 0) return;
|
|
||||||
if (xmrConnectionService.getLastInfo().getHeight() >= processModel.getDeleteBackupsHeight()) {
|
|
||||||
xmrWalletService.deleteWalletBackups(getWalletName());
|
|
||||||
processModel.setDeleteBackupsHeight(0); // reset delete height
|
|
||||||
xmrWalletService.removeWalletListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xmrWalletService.addWalletListener(deleteBackupsListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void maybeClearSensitiveData() {
|
public void maybeClearSensitiveData() {
|
||||||
String change = "";
|
String change = "";
|
||||||
if (removeAllChatMessages()) {
|
if (removeAllChatMessages()) {
|
||||||
|
@ -1409,6 +1387,127 @@ public abstract class Trade implements Tradable, Model {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Trade error cleanup
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onProtocolError() {
|
||||||
|
|
||||||
|
// check if deposit published
|
||||||
|
if (isDepositsPublished()) {
|
||||||
|
restorePublishedTrade();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unreserve taker key images
|
||||||
|
if (this instanceof TakerTrade) {
|
||||||
|
ThreadUtils.submitToPool(() -> {
|
||||||
|
xmrWalletService.thawOutputs(getSelf().getReserveTxKeyImages());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// unreserve open offer
|
||||||
|
Optional<OpenOffer> openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId());
|
||||||
|
if (this instanceof MakerTrade && openOffer.isPresent()) {
|
||||||
|
processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove if deposit not requested or is failed
|
||||||
|
if (!isDepositRequested() || isDepositFailed()) {
|
||||||
|
removeTradeOnError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// done if wallet already deleted
|
||||||
|
if (!walletExists()) return;
|
||||||
|
|
||||||
|
// move to failed trades
|
||||||
|
processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this);
|
||||||
|
|
||||||
|
// set error height
|
||||||
|
if (processModel.getTradeProtocolErrorHeight() == 0) {
|
||||||
|
log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getLastInfo().getHeight());
|
||||||
|
processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for deposits published to restore trade
|
||||||
|
protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> {
|
||||||
|
if (isDepositsPublished()) {
|
||||||
|
restorePublishedTrade();
|
||||||
|
if (protocolErrorStateSubscription != null) { // unsubscribe
|
||||||
|
protocolErrorStateSubscription.unsubscribe();
|
||||||
|
protocolErrorStateSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// listen for block confirmations to remove trade
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
protocolErrorHeightSubscription = EasyBind.subscribe(walletHeight, lastWalletHeight -> {
|
||||||
|
if (isShutDown || isDepositsPublished()) return;
|
||||||
|
if (lastWalletHeight.longValue() < processModel.getTradeProtocolErrorHeight() + DELETE_AFTER_NUM_BLOCKS) return;
|
||||||
|
if (System.currentTimeMillis() - startTime < DELETE_AFTER_MS) return;
|
||||||
|
|
||||||
|
// remove trade off thread
|
||||||
|
ThreadUtils.submitToPool(() -> {
|
||||||
|
|
||||||
|
// get trade's deposit txs from daemon
|
||||||
|
MoneroTx makerDepositTx = getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(getMaker().getDepositTxHash());
|
||||||
|
MoneroTx takerDepositTx = getTaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(getTaker().getDepositTxHash());
|
||||||
|
|
||||||
|
// remove trade and wallet if neither deposit tx published
|
||||||
|
if (makerDepositTx == null && takerDepositTx == null) {
|
||||||
|
log.warn("Deleting {} {} after protocol error", getClass().getSimpleName(), getId());
|
||||||
|
if (this instanceof ArbitratorTrade && (getMaker().getReserveTxHash() != null || getTaker().getReserveTxHash() != null)) {
|
||||||
|
processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); // arbitrator retains trades with reserved funds for analysis and penalty
|
||||||
|
deleteWallet();
|
||||||
|
onShutDownStarted();
|
||||||
|
ThreadUtils.submitToPool(() -> shutDown()); // run off thread
|
||||||
|
} else {
|
||||||
|
removeTradeOnError();
|
||||||
|
}
|
||||||
|
} else if (!isPayoutPublished()) {
|
||||||
|
|
||||||
|
// set error if wallet may be partially funded
|
||||||
|
String errorMessage = "Refusing to delete " + getClass().getSimpleName() + " " + getId() + " after protocol error because its wallet might be funded";
|
||||||
|
prependErrorMessage(errorMessage);
|
||||||
|
log.warn(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
if (protocolErrorHeightSubscription != null) {
|
||||||
|
protocolErrorHeightSubscription.unsubscribe();
|
||||||
|
protocolErrorHeightSubscription = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restorePublishedTrade() {
|
||||||
|
|
||||||
|
// close open offer
|
||||||
|
if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) {
|
||||||
|
log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId());
|
||||||
|
processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-freeze outputs
|
||||||
|
xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages());
|
||||||
|
|
||||||
|
// restore trade from failed trades
|
||||||
|
processModel.getTradeManager().onMoveFailedTradeToPendingTrades(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeTradeOnError() {
|
||||||
|
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
|
||||||
|
|
||||||
|
// clear and shut down trade
|
||||||
|
clearAndShutDown();
|
||||||
|
|
||||||
|
// unregister trade
|
||||||
|
processModel.getTradeManager().unregisterTrade(this);
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Model implementation
|
// Model implementation
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1815,6 +1914,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositsPublished() {
|
public boolean isDepositsPublished() {
|
||||||
|
if (isDepositFailed()) return false;
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
|
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1935,9 +2035,11 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
public BigInteger getFrozenAmount() {
|
public BigInteger getFrozenAmount() {
|
||||||
BigInteger sum = BigInteger.ZERO;
|
BigInteger sum = BigInteger.ZERO;
|
||||||
for (String keyImage : getSelf().getReserveTxKeyImages()) {
|
if (getSelf().getReserveTxKeyImages() != null) {
|
||||||
List<MoneroOutputWallet> outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
|
for (String keyImage : getSelf().getReserveTxKeyImages()) {
|
||||||
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
|
List<MoneroOutputWallet> outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
|
||||||
|
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
@ -2169,12 +2271,12 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// skip if payout unlocked
|
// skip if payout unlocked
|
||||||
if (isPayoutUnlocked()) return;
|
if (isPayoutUnlocked()) return;
|
||||||
|
|
||||||
// skip if either deposit tx id is unknown
|
// skip if deposit txs unknown or not requested
|
||||||
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) return;
|
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return;
|
||||||
|
|
||||||
// sync if wallet too far behind daemon
|
// sync if wallet too far behind daemon
|
||||||
if (xmrConnectionService.getTargetHeight() == null) return;
|
if (xmrConnectionService.getTargetHeight() == null) return;
|
||||||
if (wallet.getHeight() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false);
|
if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false);
|
||||||
|
|
||||||
// update deposit txs
|
// update deposit txs
|
||||||
if (!isDepositsUnlocked()) {
|
if (!isDepositsUnlocked()) {
|
||||||
|
@ -2286,12 +2388,13 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (isWalletBehind()) {
|
if (isWalletBehind()) {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
xmrWalletService.syncWallet(wallet);
|
xmrWalletService.syncWallet(wallet);
|
||||||
|
walletHeight.set(wallet.getHeight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWalletBehind() {
|
private boolean isWalletBehind() {
|
||||||
return wallet.getHeight() < xmrConnectionService.getTargetHeight();
|
return walletHeight.get() < xmrConnectionService.getTargetHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDepositTxs(List<? extends MoneroTx> txs) {
|
private void setDepositTxs(List<? extends MoneroTx> txs) {
|
||||||
|
|
|
@ -133,7 +133,6 @@ import monero.daemon.model.MoneroTx;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
import org.fxmisc.easybind.EasyBind;
|
import org.fxmisc.easybind.EasyBind;
|
||||||
import org.fxmisc.easybind.Subscription;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -434,7 +433,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
Set<String> uids = new HashSet<String>();
|
Set<String> uids = new HashSet<String>();
|
||||||
Set<Trade> tradesToSkip = new HashSet<Trade>();
|
Set<Trade> tradesToSkip = new HashSet<Trade>();
|
||||||
Set<Trade> tradesToMaybeRemoveOnError = new HashSet<Trade>();
|
Set<Trade> uninitializedTrades = new HashSet<Trade>();
|
||||||
for (Trade trade : trades) {
|
for (Trade trade : trades) {
|
||||||
tasks.add(() -> {
|
tasks.add(() -> {
|
||||||
try {
|
try {
|
||||||
|
@ -451,7 +450,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
// remove trade if protocol didn't initialize
|
// remove trade if protocol didn't initialize
|
||||||
if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) {
|
if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) {
|
||||||
tradesToMaybeRemoveOnError.add(trade);
|
uninitializedTrades.add(trade);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!isShutDownStarted) {
|
if (!isShutDownStarted) {
|
||||||
|
@ -477,9 +476,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
// process after all wallets initialized
|
// process after all wallets initialized
|
||||||
if (!HavenoUtils.isSeedNode()) {
|
if (!HavenoUtils.isSeedNode()) {
|
||||||
|
|
||||||
// maybe remove trades on error
|
// handle uninitialized trades
|
||||||
for (Trade trade : tradesToMaybeRemoveOnError) {
|
for (Trade trade : uninitializedTrades) {
|
||||||
maybeRemoveTradeOnError(trade);
|
trade.onProtocolError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// freeze or thaw outputs
|
// freeze or thaw outputs
|
||||||
|
@ -623,7 +622,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
// process with protocol
|
// process with protocol
|
||||||
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||||
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
||||||
maybeRemoveTradeOnError(trade);
|
trade.onProtocolError();
|
||||||
});
|
});
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
@ -704,7 +703,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
// process with protocol
|
// process with protocol
|
||||||
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||||
log.warn("Maker error during trade initialization: " + errorMessage);
|
log.warn("Maker error during trade initialization: " + errorMessage);
|
||||||
maybeRemoveTradeOnError(trade);
|
trade.onProtocolError();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -797,8 +796,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getOpenTrade(response.getOfferId());
|
Optional<Trade> tradeOptional = getOpenTrade(response.getOfferId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + response.getOfferId());
|
tradeOptional = getFailedTrade(response.getOfferId());
|
||||||
return;
|
if (!tradeOptional.isPresent()) {
|
||||||
|
log.warn("No trade with id " + response.getOfferId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer);
|
((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer);
|
||||||
|
@ -885,8 +887,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}, errorMessage -> {
|
}, errorMessage -> {
|
||||||
log.warn("Taker error during trade initialization: " + errorMessage);
|
log.warn("Taker error during trade initialization: " + errorMessage);
|
||||||
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId());
|
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error
|
||||||
maybeRemoveTradeOnError(trade);
|
trade.onProtocolError();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
@ -945,13 +947,32 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed");
|
if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed");
|
||||||
closedTradableManager.add(trade);
|
closedTradableManager.add(trade);
|
||||||
trade.setCompleted(true);
|
trade.setCompleted(true);
|
||||||
removeTrade(trade);
|
removeTrade(trade, true);
|
||||||
|
|
||||||
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||||
xmrWalletService.resetAddressEntriesForTrade(trade.getId());
|
xmrWalletService.resetAddressEntriesForTrade(trade.getId());
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void unregisterTrade(Trade trade) {
|
||||||
|
removeTrade(trade, true);
|
||||||
|
removeFailedTrade(trade);
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeTrade(Trade trade, boolean removeDirectMessageListener) {
|
||||||
|
log.info("TradeManager.removeTrade() " + trade.getId());
|
||||||
|
|
||||||
|
// remove trade
|
||||||
|
synchronized (tradableList) {
|
||||||
|
if (!tradableList.remove(trade)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unregister message listener and persist
|
||||||
|
if (removeDirectMessageListener) p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Dispute
|
// Dispute
|
||||||
|
@ -1014,8 +1035,17 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
// If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published)
|
// If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published)
|
||||||
// we move the trade to FailedTradesManager
|
// we move the trade to FailedTradesManager
|
||||||
public void onMoveInvalidTradeToFailedTrades(Trade trade) {
|
public void onMoveInvalidTradeToFailedTrades(Trade trade) {
|
||||||
removeTrade(trade);
|
|
||||||
failedTradesManager.add(trade);
|
failedTradesManager.add(trade);
|
||||||
|
removeTrade(trade, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMoveFailedTradeToPendingTrades(Trade trade) {
|
||||||
|
addFailedTradeToPendingTrades(trade);
|
||||||
|
failedTradesManager.removeTrade(trade);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFailedTrade(Trade trade) {
|
||||||
|
failedTradesManager.removeTrade(trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addFailedTradeToPendingTrades(Trade trade) {
|
public void addFailedTradeToPendingTrades(Trade trade) {
|
||||||
|
@ -1255,132 +1285,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeTrade(Trade trade) {
|
|
||||||
log.info("TradeManager.removeTrade() " + trade.getId());
|
|
||||||
|
|
||||||
// remove trade
|
|
||||||
synchronized (tradableList) {
|
|
||||||
if (!tradableList.remove(trade)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unregister and persist
|
|
||||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
|
||||||
requestPersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeRemoveTradeOnError(Trade trade) {
|
|
||||||
if (trade.isDepositRequested() && !trade.isDepositFailed()) {
|
|
||||||
listenForCleanup(trade);
|
|
||||||
} else {
|
|
||||||
removeTradeOnError(trade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeTradeOnError(Trade trade) {
|
|
||||||
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
|
|
||||||
|
|
||||||
// unreserve taker key images
|
|
||||||
if (trade instanceof TakerTrade) {
|
|
||||||
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
|
||||||
trade.getSelf().setReserveTxKeyImages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// unreserve open offer
|
|
||||||
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(trade.getId());
|
|
||||||
if (trade instanceof MakerTrade && openOffer.isPresent()) {
|
|
||||||
openOfferManager.unreserveOpenOffer(openOffer.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear and shut down trade
|
|
||||||
trade.clearAndShutDown();
|
|
||||||
|
|
||||||
// remove trade from list
|
|
||||||
removeTrade(trade);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void listenForCleanup(Trade trade) {
|
|
||||||
if (getOpenTrade(trade.getId()).isPresent() && trade.isDepositRequested()) {
|
|
||||||
if (trade.isDepositsPublished()) {
|
|
||||||
cleanupPublishedTrade(trade);
|
|
||||||
} else {
|
|
||||||
log.warn("Scheduling to delete open trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
|
||||||
new TradeCleanupListener(trade); // TODO: better way than creating listener?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupPublishedTrade(Trade trade) {
|
|
||||||
if (trade instanceof MakerTrade && openOfferManager.getOpenOfferById(trade.getId()).isPresent()) {
|
|
||||||
log.warn("Closing open offer as cleanup step");
|
|
||||||
openOfferManager.closeOpenOffer(checkNotNull(trade.getOffer()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TradeCleanupListener {
|
|
||||||
|
|
||||||
private static final long REMOVE_AFTER_MS = 60000;
|
|
||||||
private static final int REMOVE_AFTER_NUM_CONFIRMATIONS = 1;
|
|
||||||
private Long startHeight;
|
|
||||||
private Subscription stateSubscription;
|
|
||||||
private Subscription heightSubscription;
|
|
||||||
|
|
||||||
public TradeCleanupListener(Trade trade) {
|
|
||||||
|
|
||||||
// listen for deposits published to close open offer
|
|
||||||
stateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
|
|
||||||
if (trade.isDepositsPublished()) {
|
|
||||||
cleanupPublishedTrade(trade);
|
|
||||||
if (stateSubscription != null) {
|
|
||||||
stateSubscription.unsubscribe();
|
|
||||||
stateSubscription = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen for block confirmation to remove trade
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
heightSubscription = EasyBind.subscribe(xmrWalletService.getConnectionService().chainHeightProperty(), lastBlockHeight -> {
|
|
||||||
if (isShutDown) return;
|
|
||||||
if (startHeight == null) startHeight = lastBlockHeight.longValue();
|
|
||||||
if (lastBlockHeight.longValue() >= startHeight + REMOVE_AFTER_NUM_CONFIRMATIONS) {
|
|
||||||
new Thread(() -> {
|
|
||||||
|
|
||||||
// wait minimum time
|
|
||||||
HavenoUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime)));
|
|
||||||
|
|
||||||
// get trade's deposit txs from daemon
|
|
||||||
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());
|
|
||||||
|
|
||||||
// remove trade and wallet if neither deposit tx published
|
|
||||||
if (makerDepositTx == null && takerDepositTx == null) {
|
|
||||||
log.warn("Deleting {} {} after protocol error", trade.getClass().getSimpleName(), trade.getId());
|
|
||||||
if (trade instanceof ArbitratorTrade && (trade.getMaker().getReserveTxHash() != null || trade.getTaker().getReserveTxHash() != null)) {
|
|
||||||
onMoveInvalidTradeToFailedTrades(trade); // arbitrator retains trades with reserved funds for analysis and penalty
|
|
||||||
} else {
|
|
||||||
removeTradeOnError(trade);
|
|
||||||
failedTradesManager.removeTrade(trade);
|
|
||||||
}
|
|
||||||
} else if (!trade.isPayoutPublished()) {
|
|
||||||
|
|
||||||
// set error that wallet may be partially funded
|
|
||||||
String errorMessage = "Refusing to delete " + trade.getClass().getSimpleName() + " " + trade.getId() + " after protocol timeout because its wallet might be funded";
|
|
||||||
trade.prependErrorMessage(errorMessage);
|
|
||||||
log.warn(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsubscribe
|
|
||||||
if (heightSubscription != null) {
|
|
||||||
heightSubscription.unsubscribe();
|
|
||||||
heightSubscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove once tradableList is refactored to a final field
|
// TODO Remove once tradableList is refactored to a final field
|
||||||
// (part of the persistence refactor PR)
|
// (part of the persistence refactor PR)
|
||||||
private void onTradesChanged() {
|
private void onTradesChanged() {
|
||||||
|
|
|
@ -157,7 +157,7 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||||
private String multisigAddress;
|
private String multisigAddress;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private long deleteBackupsHeight;
|
private long tradeProtocolErrorHeight;
|
||||||
|
|
||||||
// We want to indicate the user the state of the message delivery of the
|
// We want to indicate the user the state of the message delivery of the
|
||||||
// PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet.
|
// PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet.
|
||||||
|
@ -207,7 +207,7 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||||
.setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name())
|
.setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name())
|
||||||
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
||||||
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
|
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
|
||||||
.setDeleteBackupsHeight(deleteBackupsHeight);
|
.setTradeProtocolErrorHeight(tradeProtocolErrorHeight);
|
||||||
Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage()));
|
Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage()));
|
||||||
Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage()));
|
Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage()));
|
||||||
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage()));
|
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage()));
|
||||||
|
@ -230,7 +230,7 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||||
processModel.setFundsNeededForTrade(proto.getFundsNeededForTrade());
|
processModel.setFundsNeededForTrade(proto.getFundsNeededForTrade());
|
||||||
processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation());
|
processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation());
|
||||||
processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation());
|
processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation());
|
||||||
processModel.setDeleteBackupsHeight(proto.getDeleteBackupsHeight());
|
processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight());
|
||||||
|
|
||||||
// nullable
|
// nullable
|
||||||
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
|
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
|
||||||
|
|
|
@ -427,15 +427,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
System.out.println(getClass().getSimpleName() + ".handleDepositResponse()");
|
System.out.println(getClass().getSimpleName() + ".handleDepositResponse()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
|
|
||||||
// check trade
|
|
||||||
if (trade.hasFailed()) {
|
|
||||||
log.warn("{} {} ignoring {} from {} because trade failed with previous error: {}", trade.getClass().getSimpleName(), trade.getId(), response.getClass().getSimpleName(), sender, trade.getErrorMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
|
|
||||||
// process message
|
|
||||||
latchTrade();
|
latchTrade();
|
||||||
processModel.setTradeMessage(response);
|
processModel.setTradeMessage(response);
|
||||||
expect(anyState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST, Trade.State.SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST, Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS, Trade.State.DEPOSIT_TXS_SEEN_IN_NETWORK)
|
expect(anyState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST, Trade.State.SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST, Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS, Trade.State.DEPOSIT_TXS_SEEN_IN_NETWORK)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.messages.DepositRequest;
|
import haveno.core.trade.messages.DepositRequest;
|
||||||
import haveno.core.trade.messages.DepositResponse;
|
import haveno.core.trade.messages.DepositResponse;
|
||||||
import haveno.core.trade.protocol.TradePeer;
|
import haveno.core.trade.protocol.TradePeer;
|
||||||
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.p2p.SendDirectMessageListener;
|
import haveno.network.p2p.SendDirectMessageListener;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -101,6 +102,10 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extend timeout
|
||||||
|
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while verifying deposit tx for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
|
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
// set deposit info
|
// set deposit info
|
||||||
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||||
trader.setDepositTxFee(verifiedTx.getFee());
|
trader.setDepositTxFee(verifiedTx.getFee());
|
||||||
|
@ -109,7 +114,6 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
|
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
|
||||||
|
|
||||||
// relay deposit txs when both available
|
// relay deposit txs when both available
|
||||||
// TODO (woodser): add small delay so tx has head start against double spend attempts?
|
|
||||||
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
||||||
|
|
||||||
// update trade state
|
// update trade state
|
||||||
|
@ -147,8 +151,8 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
|
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
complete();
|
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|
||||||
// handle error before deposits relayed
|
// handle error before deposits relayed
|
||||||
|
@ -192,4 +196,8 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isTimedOut() {
|
||||||
|
return !processModel.getTradeManager().hasOpenTrade(trade);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,6 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// reset protocol timeout
|
// reset protocol timeout
|
||||||
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.math.BigInteger;
|
||||||
import haveno.common.taskrunner.TaskRunner;
|
import haveno.common.taskrunner.TaskRunner;
|
||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.messages.DepositResponse;
|
import haveno.core.trade.messages.DepositResponse;
|
||||||
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -42,9 +43,13 @@ public class ProcessDepositResponse extends TradeTask {
|
||||||
DepositResponse message = (DepositResponse) processModel.getTradeMessage();
|
DepositResponse message = (DepositResponse) processModel.getTradeMessage();
|
||||||
if (message.getErrorMessage() != null) {
|
if (message.getErrorMessage() != null) {
|
||||||
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
|
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
|
||||||
|
processModel.getTradeManager().unregisterTrade(trade);
|
||||||
throw new RuntimeException(message.getErrorMessage());
|
throw new RuntimeException(message.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset protocol timeout
|
||||||
|
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
// record security deposits
|
// record security deposits
|
||||||
trade.getBuyer().setSecurityDeposit(BigInteger.valueOf(message.getBuyerSecurityDeposit()));
|
trade.getBuyer().setSecurityDeposit(BigInteger.valueOf(message.getBuyerSecurityDeposit()));
|
||||||
trade.getSeller().setSecurityDeposit(BigInteger.valueOf(message.getSellerSecurityDeposit()));
|
trade.getSeller().setSecurityDeposit(BigInteger.valueOf(message.getSellerSecurityDeposit()));
|
||||||
|
|
|
@ -549,11 +549,20 @@ public class XmrWalletService {
|
||||||
/**
|
/**
|
||||||
* Freeze the given outputs with a lock on the wallet.
|
* Freeze the given outputs with a lock on the wallet.
|
||||||
*
|
*
|
||||||
* @param keyImages the key images to freeze
|
* @param keyImages the key images to freeze (ignored if null or empty)
|
||||||
*/
|
*/
|
||||||
public void freezeOutputs(Collection<String> keyImages) {
|
public void freezeOutputs(Collection<String> keyImages) {
|
||||||
|
if (keyImages == null || keyImages.isEmpty()) return;
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (WALLET_LOCK) {
|
||||||
for (String keyImage : keyImages) wallet.freezeOutput(keyImage);
|
|
||||||
|
// collect outputs to freeze
|
||||||
|
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
|
||||||
|
.map(output -> output.getKeyImage().getHex())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
unfrozenKeyImages.retainAll(keyImages);
|
||||||
|
|
||||||
|
// freeze outputs
|
||||||
|
for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage);
|
||||||
cacheWalletInfo();
|
cacheWalletInfo();
|
||||||
requestSaveMainWallet();
|
requestSaveMainWallet();
|
||||||
}
|
}
|
||||||
|
@ -567,7 +576,15 @@ public class XmrWalletService {
|
||||||
public void thawOutputs(Collection<String> keyImages) {
|
public void thawOutputs(Collection<String> keyImages) {
|
||||||
if (keyImages == null || keyImages.isEmpty()) return;
|
if (keyImages == null || keyImages.isEmpty()) return;
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (WALLET_LOCK) {
|
||||||
for (String keyImage : keyImages) wallet.thawOutput(keyImage);
|
|
||||||
|
// collect outputs to thaw
|
||||||
|
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
|
||||||
|
.map(output -> output.getKeyImage().getHex())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
frozenKeyImages.retainAll(keyImages);
|
||||||
|
|
||||||
|
// thaw outputs
|
||||||
|
for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage);
|
||||||
cacheWalletInfo();
|
cacheWalletInfo();
|
||||||
requestSaveMainWallet();
|
requestSaveMainWallet();
|
||||||
}
|
}
|
||||||
|
|
|
@ -556,7 +556,7 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(FailedTradesListItem newItem, boolean empty) {
|
public void updateItem(FailedTradesListItem newItem, boolean empty) {
|
||||||
super.updateItem(newItem, empty);
|
super.updateItem(newItem, empty);
|
||||||
if (!empty && newItem != null) {
|
if (!empty && newItem != null && newItem.getTrade().isDepositsPublished()) {
|
||||||
Label icon = FormBuilder.getIcon(AwesomeIcon.UNDO);
|
Label icon = FormBuilder.getIcon(AwesomeIcon.UNDO);
|
||||||
JFXButton iconButton = new JFXButton("", icon);
|
JFXButton iconButton = new JFXButton("", icon);
|
||||||
iconButton.setStyle("-fx-cursor: hand;");
|
iconButton.setStyle("-fx-cursor: hand;");
|
||||||
|
|
|
@ -1558,7 +1558,7 @@ message ProcessModel {
|
||||||
bytes mediated_payout_tx_signature = 15; // placeholder if mediation used in future
|
bytes mediated_payout_tx_signature = 15; // placeholder if mediation used in future
|
||||||
int64 buyer_payout_amount_from_mediation = 16;
|
int64 buyer_payout_amount_from_mediation = 16;
|
||||||
int64 seller_payout_amount_from_mediation = 17;
|
int64 seller_payout_amount_from_mediation = 17;
|
||||||
int64 delete_backups_height = 18;
|
int64 trade_protocol_error_height = 18;
|
||||||
string trade_fee_address = 19;
|
string trade_fee_address = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue