diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index fc656a19d5..b53142edcc 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -154,7 +154,7 @@ public class TradeInfo implements Payload { .withPhase(trade.getPhase().name()) .withPeriodState(trade.getPeriodState().name()) .withIsDepositPublished(trade.isDepositPublished()) - .withIsDepositUnlocked(trade.isDepositConfirmed()) + .withIsDepositUnlocked(trade.isDepositUnlocked()) .withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentReceived(trade.isPaymentReceived()) .withIsPayoutPublished(trade.isPayoutPublished()) diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 2f4bbc5a26..efa0c5b711 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -39,7 +39,7 @@ import bisq.core.util.ParsingUtils; import bisq.core.util.VolumeUtil; import bisq.network.p2p.AckMessage; import bisq.network.p2p.NodeAddress; - +import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.taskrunner.Model; @@ -876,20 +876,19 @@ public abstract class Trade implements Tradable, Model { // handle deposit txs seen if (txs.size() == 2) { - - // update state - setState(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK); boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); makerDepositTx = makerFirst ? txs.get(0) : txs.get(1); takerDepositTx = makerFirst ? txs.get(1) : txs.get(0); - + // check if deposit txs unlocked if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) { - long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK - 1; + long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK; if (havenoWallet.getHeight() >= unlockHeight) { - setConfirmedState(); + setUnlockedState(); return; } + } else { + setStateIfValidTransitionTo(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK); } } @@ -900,40 +899,37 @@ public abstract class Trade implements Tradable, Model { @Override public void onNewBlock(long height) { - + // ignore if no longer listening if (depositTxListener == null) return; - + // ignore if before unlock height if (unlockHeight != null && height < unlockHeight) return; - + // fetch txs from daemon List txs = daemon.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()), true); - + // ignore if deposit txs not seen if (txs.size() != 2) return; - + // update deposit txs boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); makerDepositTx = makerFirst ? txs.get(0) : txs.get(1); takerDepositTx = makerFirst ? txs.get(1) : txs.get(0); - - // update state when deposit txs seen - if (txs.size() == 2) { - setState(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK); - } - + // compute unlock height if (unlockHeight == null && txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) { - unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK - 1; + unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK; } - - // check if txs unlocked + + // check if deposit txs unlocked if (unlockHeight != null && height == unlockHeight) { log.info("Multisig deposits unlocked for trade {}", getId()); - setConfirmedState(); // TODO (woodser): bisq "confirmed" = xmr unlocked after 10 confirmations + setUnlockedState(); xmrWalletService.removeWalletListener(depositTxListener); // remove listener when notified depositTxListener = null; // prevent re-applying trade state in subsequent requests + } else if (txs.size() == 2) { + setStateIfValidTransitionTo(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK); } } }; @@ -1079,8 +1075,10 @@ public abstract class Trade implements Tradable, Model { } this.state = state; - stateProperty.set(state); - statePhaseProperty.set(state.getPhase()); + UserThread.execute(() -> { + stateProperty.set(state); + statePhaseProperty.set(state.getPhase()); + }); } public void setDisputeState(DisputeState disputeState) { @@ -1242,7 +1240,7 @@ public abstract class Trade implements Tradable, Model { final MoneroTx takerDepositTx = getTakerDepositTx(); final MoneroTx makerDepositTx = getMakerDepositTx(); if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) { - if (isDepositConfirmed()) { + if (isDepositUnlocked()) { final long tradeTime = getTakeOfferDate().getTime(); long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight()); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); @@ -1318,7 +1316,7 @@ public abstract class Trade implements Tradable, Model { disputeState != DisputeState.REFUND_REQUEST_CLOSED; } - public boolean isDepositConfirmed() { + public boolean isDepositUnlocked() { return getState().getPhase().ordinal() >= Phase.DEPOSIT_UNLOCKED.ordinal(); } @@ -1479,13 +1477,13 @@ public abstract class Trade implements Tradable, Model { // } // } - private void setConfirmedState() { + private void setUnlockedState() { // we only apply the state if we are not already further in the process - if (!isDepositConfirmed()) { + if (!isDepositUnlocked()) { // As setState is called here from the trade itself we cannot trigger a requestPersistence call. // But as we get setupConfidenceListener called at startup anyway there is no issue if it would not be // persisted in case the shutdown routine did not persist the trade. - setState(State.DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN); // TODO (woodser): for xmr this means deposit txs have unlocked after 10 confirmations + setStateIfValidTransitionTo(State.DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN); // TODO (woodser): for xmr this means deposit txs have unlocked after 10 confirmations } } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index edbc81a8ea..6cadaaaaed 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -884,8 +884,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void updateTradePeriodState() { - getObservableList().forEach(trade -> { - UserThread.execute(() -> { // prevent concurrent modification error + UserThread.execute(() -> { // prevent concurrent modification error + getObservableList().forEach(trade -> { if (!trade.isPayoutPublished()) { Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); diff --git a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java index 793749dfa2..60163dcd80 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java @@ -53,7 +53,7 @@ public class ArbitratorProtocol extends DisputeProtocol { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -79,7 +79,7 @@ public class ArbitratorProtocol extends DisputeProtocol { handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -106,7 +106,7 @@ public class ArbitratorProtocol extends DisputeProtocol { handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -134,7 +134,7 @@ public class ArbitratorProtocol extends DisputeProtocol { handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index 5e57637959..a8637a9655 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -1,5 +1,5 @@ /* - * This file is part of Haveno. +e * This file is part of Haveno. * * Haveno is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by @@ -95,7 +95,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -122,40 +122,13 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); - synchronized (trade) { - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(TRADE_TIMEOUT); - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); - awaitTradeLatch(); - } - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); @@ -164,6 +137,41 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(TRADE_TIMEOUT); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + handleTaskRunnerFault(sender, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) // extend timeout + .executeTasks(true); + awaitTradeLatch(); + } else { + // process sign contract request after contract signature requested + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractRequest(message, sender)).start(); // process notification without trade lock + }); + } + } + } + + @Override + public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == Trade.State.CONTRACT_SIGNED) { + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + expect(state(Trade.State.CONTRACT_SIGNED) .with(message) .from(sender)) .setup(tasks( @@ -178,11 +186,12 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) // extend timeout - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { + // process sign contract response after contract signed EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock + if (state == Trade.State.CONTRACT_SIGNED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock }); } } @@ -210,7 +219,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol handleTaskRunnerFault(sender, response, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -241,7 +250,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { @@ -295,7 +304,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol BuyerFinalizesDelayedPayoutTx.class, BuyerSendsDelayedPayoutTxSignatureResponse.class) .withTimeout(60)) - .executeTasks(); + .executeTasks(true); } // We keep the handler here in as well to make it more transparent which messages we expect diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index e6054e6fd8..9597bbe537 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -112,7 +112,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol handleError(errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -139,35 +139,43 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); synchronized (trade) { - latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(TRADE_TIMEOUT); - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); - awaitTradeLatch(); + if (trade.getState() == Trade.State.CONTRACT_SIGNATURE_REQUESTED) { + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(TRADE_TIMEOUT); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + handleTaskRunnerFault(sender, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) // extend timeout + .executeTasks(true); + awaitTradeLatch(); + } else { + // process sign contract request after contract signature requested + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractRequest(message, sender)).start(); // process notification without trade lock + }); + } } } @@ -176,11 +184,11 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); - if (trade.getState() == Trade.State.CONTRACT_SIGNATURE_REQUESTED) { + if (trade.getState() == Trade.State.CONTRACT_SIGNED) { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); - expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + expect(state(Trade.State.CONTRACT_SIGNED) .with(message) .from(sender)) .setup(tasks( @@ -195,11 +203,11 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) // extend timeout - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock + if (state == Trade.State.CONTRACT_SIGNED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock }); } } @@ -227,7 +235,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol handleTaskRunnerFault(sender, response, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -258,7 +266,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { @@ -325,7 +333,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol BuyerSetupDepositTxListener.class, BuyerAsTakerSendsDepositTxMessage.class) .withTimeout(60)) - .executeTasks(); + .executeTasks(true); } @Override @@ -340,7 +348,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol BuyerFinalizesDelayedPayoutTx.class, BuyerSendsDelayedPayoutTxSignatureResponse.class) .withTimeout(60)) - .executeTasks(); + .executeTasks(true); } // We keep the handler here in as well to make it more transparent which messages we expect diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java index 254bebd50e..1c0d1f8073 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -161,7 +161,7 @@ public abstract class BuyerProtocol extends DisputeProtocol { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks( @@ -177,7 +177,7 @@ public abstract class BuyerProtocol extends DisputeProtocol { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -191,9 +191,6 @@ public abstract class BuyerProtocol extends DisputeProtocol { protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); - log.info("Received {} from {} with tradeId {} and uid {}", - message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); - if (message instanceof DelayedPayoutTxSignatureRequest) { handle((DelayedPayoutTxSignatureRequest) message, peer); } else if (message instanceof DepositTxAndDelayedPayoutTxMessage) { diff --git a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java index cc24fcbfb7..5150196b3a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java @@ -83,6 +83,17 @@ public class FluentProtocol { return this; } + public FluentProtocol executeTasks(boolean newThread) { + if (newThread) { + new Thread(() -> { + executeTasks(); + }).start(); + } else { + executeTasks(); + } + return this; + } + public FluentProtocol executeTasks() { Condition.Result result = condition.getResult(); if (!result.isValid) { @@ -92,30 +103,27 @@ public class FluentProtocol { return this; } - synchronized (tradeProtocol.trade) { - if (setup.getTimeoutSec() > 0) { - tradeProtocol.startTimeout(setup.getTimeoutSec()); - } - - NodeAddress peer = condition.getPeer(); - if (peer != null) { - tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer); // TODO (woodser): node has multiple peers (arbitrator and maker or taker), but fluent protocol assumes only one - tradeProtocol.processModel.getTradeManager().requestPersistence(); - } - - TradeMessage message = condition.getMessage(); - if (message != null) { - tradeProtocol.processModel.setTradeMessage(message); - tradeProtocol.processModel.getTradeManager().requestPersistence(); - } - - TradeTaskRunner taskRunner = setup.getTaskRunner(peer, message, condition.getEvent()); - taskRunner.addTasks(setup.getTasks()); - taskRunner.run(); - return this; + if (setup.getTimeoutSec() > 0) { + tradeProtocol.startTimeout(setup.getTimeoutSec()); } - } + NodeAddress peer = condition.getPeer(); + if (peer != null) { + tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer); // TODO (woodser): node has multiple peers (arbitrator and maker or taker), but fluent protocol assumes only one + tradeProtocol.processModel.getTradeManager().requestPersistence(); + } + + TradeMessage message = condition.getMessage(); + if (message != null) { + tradeProtocol.processModel.setTradeMessage(message); + tradeProtocol.processModel.getTradeManager().requestPersistence(); + } + + TradeTaskRunner taskRunner = setup.getTaskRunner(peer, message, condition.getEvent()); + taskRunner.addTasks(setup.getTasks()); + taskRunner.run(); + return this; + } /////////////////////////////////////////////////////////////////////////////////////////// // Condition class diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index fa75aec285..4cfb4f13e8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -95,7 +95,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -122,35 +122,43 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); synchronized (trade) { - latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(TRADE_TIMEOUT); - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); - awaitTradeLatch(); + if (trade.getState() == Trade.State.CONTRACT_SIGNATURE_REQUESTED) { + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(TRADE_TIMEOUT); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + handleTaskRunnerFault(sender, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) // extend timeout + .executeTasks(true); + awaitTradeLatch(); + } else { + // process sign contract request after contract signature requested + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractRequest(message, sender)).start(); // process notification without trade lock + }); + } } } @@ -159,11 +167,11 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); - if (trade.getState() == Trade.State.CONTRACT_SIGNATURE_REQUESTED) { + if (trade.getState() == Trade.State.CONTRACT_SIGNED) { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); - expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + expect(state(Trade.State.CONTRACT_SIGNED) .with(message) .from(sender)) .setup(tasks( @@ -178,11 +186,11 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) // extend timeout - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock + if (state == Trade.State.CONTRACT_SIGNED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock }); } } @@ -210,7 +218,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc handleTaskRunnerFault(sender, response, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -241,7 +249,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { @@ -300,7 +308,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc SellerSignsDelayedPayoutTx.class, SellerSendDelayedPayoutTxSignatureRequest.class) .withTimeout(60)) - .executeTasks(); + .executeTasks(true); } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index cb8f86298b..caa6be17dc 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -105,7 +105,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc handleError(errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -132,40 +132,13 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); - synchronized (trade) { - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(TRADE_TIMEOUT); - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); - awaitTradeLatch(); - } - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); @@ -174,6 +147,41 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(TRADE_TIMEOUT); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + handleTaskRunnerFault(sender, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) // extend timeout + .executeTasks(true); + awaitTradeLatch(); + } else { + // process sign contract request after contract signature requested + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractRequest(message, sender)).start(); // process notification without trade lock + }); + } + } + } + + @Override + public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == Trade.State.CONTRACT_SIGNED) { + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + expect(state(Trade.State.CONTRACT_SIGNED) .with(message) .from(sender)) .setup(tasks( @@ -188,11 +196,11 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) // extend timeout - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.CONTRACT_SIGNATURE_REQUESTED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock + if (state == Trade.State.CONTRACT_SIGNED) new Thread(() -> handleSignContractResponse(message, sender)).start(); // process notification without trade lock }); } } @@ -220,7 +228,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc handleTaskRunnerFault(sender, response, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -251,7 +259,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } else { EasyBind.subscribe(trade.stateProperty(), state -> { @@ -324,6 +332,6 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc SellerSignsDelayedPayoutTx.class, SellerSendDelayedPayoutTxSignatureRequest.class) .withTimeout(60)) - .executeTasks(); + .executeTasks(true); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java index 89d2c425c6..ab361513d7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -113,7 +113,7 @@ public abstract class SellerProtocol extends DisputeProtocol { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_TIMEOUT)) - .executeTasks(); + .executeTasks(true); awaitTradeLatch(); } } @@ -126,7 +126,7 @@ public abstract class SellerProtocol extends DisputeProtocol { log.info("SellerProtocol.onPaymentReceived()"); synchronized (trade) { SellerEvent event = SellerEvent.PAYMENT_RECEIVED; - expect(anyPhase(Trade.Phase.PAYMENT_SENT) + expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks( diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 2028a967b7..a3d0568485 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -461,6 +461,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleError(errorMessage); } + // these are not thread safe, so they must be used within a lock on the trade protected void handleError(String errorMessage) { stopTimeout(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java index e249227453..6e10528039 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -134,7 +134,7 @@ public class ProcessSignContractRequest extends TradeTask { } private void completeAux() { - trade.setState(State.CONTRACT_SIGNATURE_REQUESTED); // TODO: rename to contract_signature_request_received + trade.setState(State.CONTRACT_SIGNED); processModel.getTradeManager().requestPersistence(); complete(); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java index 72f3a96d31..477b73e6a4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java @@ -21,6 +21,7 @@ import bisq.common.app.Version; import bisq.common.taskrunner.TaskRunner; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.messages.SignContractRequest; import bisq.network.p2p.SendDirectMessageListener; import java.util.Date; @@ -124,6 +125,7 @@ public class SendSignContractRequestAfterMultisig extends TradeTask { } private void completeAux() { + trade.setState(State.CONTRACT_SIGNATURE_REQUESTED); processModel.getTradeManager().requestPersistence(); processModel.getXmrWalletService().saveWallet(processModel.getXmrWalletService().getWallet()); complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java index b939435bea..4c757c6a27 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java @@ -97,6 +97,7 @@ public class BuyerSendsPaymentSentMessage extends SendMailboxMessageTask { @Override protected void setStateArrived() { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); // the message has arrived but we're ultimately waiting for an AckMessage response if (!trade.isPayoutPublished()) { tryToSendAgainLater(); diff --git a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java index 73b99b56bd..277fa6cce4 100644 --- a/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/TxIdTextField.java @@ -40,7 +40,7 @@ import javafx.scene.layout.AnchorPane; import lombok.Getter; import lombok.Setter; -import monero.wallet.model.MoneroTxWallet; +import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroWalletListener; import javax.annotation.Nullable; @@ -176,9 +176,10 @@ public class TxIdTextField extends AnchorPane { } private void updateConfidence(String txId) { - MoneroTxWallet tx = null; + MoneroTx tx = null; try { - tx = xmrWalletService.getWallet().getTx(txId); + tx = xmrWalletService.getDaemon().getTx(txId); // TODO: cache results and don't re-fetch + tx.setNumConfirmations(tx.isConfirmed() ? xmrWalletService.getConnectionsService().getLastInfo().getHeight() - tx.getHeight() : 0l); // TODO: use tx.getNumConfirmations() when MoneroDaemonRpc supports it } catch (Exception e) { // do nothing } @@ -188,6 +189,10 @@ public class TxIdTextField extends AnchorPane { txConfidenceIndicator.setVisible(true); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); } + if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) { + xmrWalletService.removeWalletListener(txUpdater); // unregister listener + txUpdater = null; + } } else { //TODO we should show some placeholder in case of a tx which we are not aware of but which can be // confirmed already. This is for instance the case of the other peers trade fee tx, as it is not related diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index af95e605ba..65582c0979 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -640,11 +640,7 @@ public class PendingTradesView extends ActivatableViewAndModel Platform.runLater(new Runnable() { - @Override public void run() { - update(); - } - }); + listener = (observable, oldValue, newValue) -> UserThread.execute(() -> update()); trade.stateProperty().addListener(listener); update(); } else { @@ -805,7 +801,6 @@ public class PendingTradesView extends ActivatableViewAndModel errorMessageListener; protected Label infoLabel; @@ -175,15 +175,11 @@ public abstract class TradeStepView extends AnchorPane { } public void activate() { - UserThread.execute(() -> { activateAux(); }); - } - - private void activateAux() { if (selfTxIdTextField != null) { - if (makerTxIdSubscription != null) - makerTxIdSubscription.unsubscribe(); + if (selfTxIdSubscription != null) + selfTxIdSubscription.unsubscribe(); - makerTxIdSubscription = EasyBind.subscribe(model.dataModel.makerTxId, id -> { + selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.makerTxId : model.dataModel.takerTxId, id -> { if (!id.isEmpty()) selfTxIdTextField.setup(id); else @@ -191,10 +187,10 @@ public abstract class TradeStepView extends AnchorPane { }); } if (peerTxIdTextField != null) { - if (takerTxIdSubscription != null) - takerTxIdSubscription.unsubscribe(); + if (peerTxIdSubscription != null) + peerTxIdSubscription.unsubscribe(); - takerTxIdSubscription = EasyBind.subscribe(model.dataModel.takerTxId, id -> { + selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.takerTxId : model.dataModel.makerTxId, id -> { if (!id.isEmpty()) peerTxIdTextField.setup(id); else @@ -288,10 +284,10 @@ public abstract class TradeStepView extends AnchorPane { } public void deactivate() { - if (makerTxIdSubscription != null) - makerTxIdSubscription.unsubscribe(); - if (takerTxIdSubscription != null) - takerTxIdSubscription.unsubscribe(); + if (selfTxIdSubscription != null) + selfTxIdSubscription.unsubscribe(); + if (peerTxIdSubscription != null) + peerTxIdSubscription.unsubscribe(); if (selfTxIdTextField != null) selfTxIdTextField.cleanup(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 428ff685a3..2a6435f77c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -155,7 +155,7 @@ public class BuyerStep2View extends TradeStepView { if (timeoutTimer != null) timeoutTimer.stop(); - if (trade.isDepositConfirmed() && !trade.isPaymentSent()) { + if (trade.isDepositUnlocked() && !trade.isPaymentSent()) { showPopup(); } else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) { if (!trade.hasFailed()) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index b079b96d96..3ed7b7e68d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -136,6 +136,7 @@ public class SellerStep3View extends TradeStepView { busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageArrived")); break; + case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageStoredInMailbox")); diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index bf5ad29604..359b8f4785 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -65,7 +65,6 @@ import bisq.common.util.Utilities; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.uri.BitcoinURI; import com.googlecode.jcsv.CSVStrategy; @@ -135,7 +134,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; -import monero.wallet.model.MoneroTxWallet; +import monero.daemon.model.MoneroTx; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; @@ -567,7 +566,7 @@ public class GUIUtil { }; } - public static void updateConfidence(MoneroTxWallet tx, + public static void updateConfidence(MoneroTx tx, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { if (tx != null) {