From bb95b4b1d6fc51c8e309e251ae81f45c12d600cd Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 31 Mar 2022 08:17:58 -0400 Subject: [PATCH] support multithreading in api and protocols close trade wallets while unused for scalability verify txs do not use unlock height increase trade init timeout to 60s --- Makefile | 9 +- .../proto/persistable/PersistableList.java | 44 +- .../java/bisq/common/taskrunner/Task.java | 13 +- core/src/main/java/bisq/core/api/CoreApi.java | 6 +- .../bisq/core/api/CoreDisputesService.java | 226 +++++----- .../java/bisq/core/api/CoreOffersService.java | 26 +- .../core/api/CorePaymentAccountsService.java | 2 +- .../java/bisq/core/api/CoreTradesService.java | 34 +- .../bisq/core/api/CoreWalletsService.java | 4 +- .../src/main/java/bisq/core/btc/Balances.java | 2 +- .../btc/setup/MoneroWalletRpcManager.java | 147 ++++--- .../core/btc/wallet/XmrWalletService.java | 128 +++--- .../bisq/core/offer/OpenOfferManager.java | 25 +- .../OfferAvailabilityProtocol.java | 2 +- .../tasks/MakerReservesTradeFunds.java | 39 +- .../tasks/MakerSendsSignOfferRequest.java | 1 + .../core/offer/takeoffer/TakeOfferModel.java | 2 +- .../core/presentation/TradePresentation.java | 9 +- .../bisq/core/provider/fee/FeeService.java | 5 - .../bisq/core/support/SupportManager.java | 82 ++-- .../bisq/core/support/dispute/Dispute.java | 4 +- .../core/support/dispute/DisputeManager.java | 359 ++++++++-------- .../arbitration/ArbitrationManager.java | 388 ++++++++++-------- .../dispute/mediation/MediationManager.java | 4 +- .../support/dispute/refund/RefundManager.java | 4 +- .../support/traderchat/TraderChatManager.java | 8 +- .../java/bisq/core/trade/TradableList.java | 10 +- core/src/main/java/bisq/core/trade/Trade.java | 142 ++++--- .../java/bisq/core/trade/TradeManager.java | 110 +++-- .../main/java/bisq/core/trade/TradeUtils.java | 20 +- .../trade/protocol/ArbitratorProtocol.java | 176 ++++---- .../trade/protocol/BuyerAsMakerProtocol.java | 285 +++++++------ .../trade/protocol/BuyerAsTakerProtocol.java | 307 ++++++++------ .../core/trade/protocol/BuyerProtocol.java | 87 ++-- .../core/trade/protocol/FluentProtocol.java | 40 +- .../core/trade/protocol/ProcessModel.java | 13 +- .../trade/protocol/SellerAsMakerProtocol.java | 289 +++++++------ .../trade/protocol/SellerAsTakerProtocol.java | 290 +++++++------ .../core/trade/protocol/SellerProtocol.java | 93 +++-- .../core/trade/protocol/TradeProtocol.java | 65 ++- ...=> ArbitratorProcessesDepositRequest.java} | 55 ++- ...atorSendsInitTradeAndMultisigRequests.java | 19 +- .../tasks/ProcessDepositResponse.java | 3 +- .../tasks/ProcessInitMultisigRequest.java | 235 +++++------ .../ProcessPaymentAccountPayloadRequest.java | 16 +- .../tasks/ProcessSignContractRequest.java | 106 +++-- .../tasks/ProcessSignContractResponse.java | 33 +- .../tasks/ProcessUpdateMultisigRequest.java | 6 +- .../SendSignContractRequestAfterMultisig.java | 166 ++++---- .../tasks/SetupDepositTxsListener.java | 2 +- .../tasks/UpdateMultisigWithTradingPeer.java | 3 +- .../buyer/BuyerCreateAndSignPayoutTx.java | 22 +- .../BuyerProcessPayoutTxPublishedMessage.java | 1 + .../seller/SellerSignAndPublishPayoutTx.java | 96 +---- .../tasks/taker/TakerReservesTradeFunds.java | 43 +- ...akerSendsInitTradeRequestToArbitrator.java | 5 +- .../daemon/grpc/GrpcErrorMessageHandler.java | 2 +- .../daemon/grpc/GrpcExceptionHandler.java | 4 +- .../daemon/grpc/GrpcNotificationsService.java | 34 +- .../bisq/daemon/grpc/GrpcOffersService.java | 9 + .../java/bisq/daemon/grpc/GrpcServer.java | 1 - .../bisq/daemon/grpc/GrpcTradesService.java | 34 +- .../bisq/daemon/grpc/GrpcWalletsService.java | 23 +- .../grpc/interceptor/GrpcCallRateMeter.java | 56 +-- .../main/java/bisq/desktop/main/MainView.java | 6 +- .../desktop/main/funds/locked/LockedView.java | 2 +- .../main/funds/reserved/ReservedView.java | 2 +- .../transactions/TransactionAwareTrade.java | 8 +- .../market/offerbook/OfferBookChartView.java | 18 +- .../main/offer/offerbook/OfferBookView.java | 4 +- .../offer/takeoffer/TakeOfferViewModel.java | 32 +- .../bisq/desktop/main/overlays/Overlay.java | 78 ++-- .../overlays/notifications/Notification.java | 4 +- .../pendingtrades/PendingTradesDataModel.java | 90 ++-- .../pendingtrades/PendingTradesView.java | 2 +- .../steps/buyer/BuyerStep2View.java | 6 +- .../bisq/desktop/main/shared/ChatView.java | 74 ++-- .../support/dispute/DisputeChatPopup.java | 134 +++--- .../main/support/dispute/DisputeView.java | 90 ++-- .../java/bisq/desktop/util/Transitions.java | 4 +- .../bisq/network/p2p/network/Connection.java | 29 +- proto/src/main/proto/pb.proto | 67 +-- 82 files changed, 2786 insertions(+), 2338 deletions(-) rename core/src/main/java/bisq/core/trade/protocol/tasks/{ProcessDepositRequest.java => ArbitratorProcessesDepositRequest.java} (74%) diff --git a/Makefile b/Makefile index d4b0216285..3dc04c0308 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,13 @@ localnet: haveno: ./gradlew build -haveno-apps: # quick build desktop and daemon apps without tests, etc - ./gradlew :core:compileJava :desktop:build +# build haveno without tests +no-tests: + ./gradlew build -x test + +# quick build desktop and daemon apps without tests +haveno-apps: + ./gradlew :core:compileJava :desktop:build -x test deploy: # create a new screen session named 'localnet' diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistableList.java b/common/src/main/java/bisq/common/proto/persistable/PersistableList.java index c6a9f337ad..ef88337865 100644 --- a/common/src/main/java/bisq/common/proto/persistable/PersistableList.java +++ b/common/src/main/java/bisq/common/proto/persistable/PersistableList.java @@ -42,43 +42,61 @@ public abstract class PersistableList implements P } public void setAll(Collection collection) { - this.list.clear(); - this.list.addAll(collection); + synchronized (this.list) { + this.list.clear(); + this.list.addAll(collection); + } } public boolean add(T item) { - if (!list.contains(item)) { - list.add(item); - return true; + synchronized (list) { + if (!list.contains(item)) { + list.add(item); + return true; + } + return false; } - return false; } public boolean remove(T item) { - return list.remove(item); + synchronized (list) { + return list.remove(item); + } } public Stream stream() { - return list.stream(); + synchronized (list) { + return list.stream(); + } } public int size() { - return list.size(); + synchronized (list) { + return list.size(); + } } public boolean contains(T item) { - return list.contains(item); + synchronized (list) { + return list.contains(item); + } } public boolean isEmpty() { - return list.isEmpty(); + synchronized (list) { + return list.isEmpty(); + } } public void forEach(Consumer action) { - list.forEach(action); + synchronized (list) { + list.forEach(action); + } } public void clear() { - list.clear(); + synchronized (list) { + list.clear(); + } } } diff --git a/common/src/main/java/bisq/common/taskrunner/Task.java b/common/src/main/java/bisq/common/taskrunner/Task.java index fc399c8498..c8c8a1be0c 100644 --- a/common/src/main/java/bisq/common/taskrunner/Task.java +++ b/common/src/main/java/bisq/common/taskrunner/Task.java @@ -66,11 +66,14 @@ public abstract class Task { } protected void failed(Throwable t) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - errorMessage = sw.toString(); - log.error(t.getMessage(), t); +// // append stacktrace to error message (only for development) +// StringWriter sw = new StringWriter(); +// PrintWriter pw = new PrintWriter(sw); +// t.printStackTrace(pw); +// errorMessage = sw.toString(); + + errorMessage = t.getMessage() + " (task " + getClass().getSimpleName() + ")"; + log.error(errorMessage, t); taskHandler.handleErrorMessage(errorMessage); } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index da646183cf..7086f3a31c 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -434,7 +434,8 @@ public class CoreApi { double buyerSecurityDeposit, long triggerPrice, String paymentAccountId, - Consumer resultHandler) { + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { coreOffersService.createAndPlaceOffer(currencyCode, directionAsString, priceAsString, @@ -445,7 +446,8 @@ public class CoreApi { buyerSecurityDeposit, triggerPrice, paymentAccountId, - resultHandler); + resultHandler, + errorMessageHandler); } public Offer editOffer(String offerId, diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index 1995085623..8f07b06962 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -85,113 +85,122 @@ public class CoreDisputesService { } public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) { - Trade trade = tradeManager.getTradeById(tradeId).orElseThrow(() -> + Trade trade = tradeManager.getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); - Offer offer = trade.getOffer(); - if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId)); + synchronized (trade) { + Offer offer = trade.getOffer(); + if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId)); - // Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded - // to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored. - var disputeManager = arbitrationManager; - var isSupportTicket = false; - var isMaker = tradeManager.isMyOffer(offer); - var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket); + // Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded + // to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored. + var disputeManager = arbitrationManager; + var isSupportTicket = false; + var isMaker = tradeManager.isMyOffer(offer); + var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket); - // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes - // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); - String updatedMultisigHex = multisigWallet.getMultisigHex(); - disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); - tradeManager.requestPersistence(); + // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes + // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); + String updatedMultisigHex = multisigWallet.getMultisigHex(); + disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); + tradeManager.requestPersistence(); + + // close multisig wallet + xmrWalletService.closeMultisigWallet(trade.getId()); + } } public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) { - byte[] payoutTxSerialized = null; - String payoutTxHashAsString = null; + synchronized (trade) { + byte[] payoutTxSerialized = null; + String payoutTxHashAsString = null; - PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); - checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); - byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser) - String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser) - Dispute dispute = new Dispute(new Date().getTime(), - trade.getId(), - pubKey.hashCode(), // trader id, - true, - (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, - isMaker, - pubKey, - trade.getDate().getTime(), - trade.getMaxTradePeriodDate().getTime(), - trade.getContract(), - trade.getContractHash(), - depositTxSerialized, - payoutTxSerialized, - depositTxHashAsString, - payoutTxHashAsString, - trade.getContractAsJson(), - trade.getMaker().getContractSignature(), - trade.getTaker().getContractSignature(), - trade.getMaker().getPaymentAccountPayload(), - trade.getTaker().getPaymentAccountPayload(), - arbitratorPubKeyRing, - isSupportTicket, - SupportType.ARBITRATION); + PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); + checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); + byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser) + String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser) + Dispute dispute = new Dispute(new Date().getTime(), + trade.getId(), + pubKey.hashCode(), // trader id, + true, + (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, + isMaker, + pubKey, + trade.getDate().getTime(), + trade.getMaxTradePeriodDate().getTime(), + trade.getContract(), + trade.getContractHash(), + depositTxSerialized, + payoutTxSerialized, + depositTxHashAsString, + payoutTxHashAsString, + trade.getContractAsJson(), + trade.getMaker().getContractSignature(), + trade.getTaker().getContractSignature(), + trade.getMaker().getPaymentAccountPayload(), + trade.getTaker().getPaymentAccountPayload(), + arbitratorPubKeyRing, + isSupportTicket, + SupportType.ARBITRATION); - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - return dispute; + return dispute; + } } public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { try { - var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() + var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() .filter(d -> tradeId.equals(d.getTradeId())) .findFirst(); Dispute dispute; if (disputeOptional.isPresent()) dispute = disputeOptional.get(); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); + + synchronized (tradeManager.getTrade(tradeId)) { + var closeDate = new Date(); + var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); + var contract = dispute.getContract(); - var closeDate = new Date(); - var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); - var contract = dispute.getContract(); + DisputePayout payout; + if (customWinnerAmount > 0) { + payout = DisputePayout.CUSTOM; + } else if (winner == DisputeResult.Winner.BUYER) { + payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; + } else if (winner == DisputeResult.Winner.SELLER) { + payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; + } else { + throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); + } + applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); - DisputePayout payout; - if (customWinnerAmount > 0) { - payout = DisputePayout.CUSTOM; - } else if (winner == DisputeResult.Winner.BUYER) { - payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; - } else if (winner == DisputeResult.Winner.SELLER) { - payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; - } else { - throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); + // resolve the payout + resolveDisputePayout(dispute, disputeResult, contract); + + // close dispute ticket + closeDispute(arbitrationManager, dispute, disputeResult, false); + + // close dispute ticket for peer + var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() + .filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) + .findFirst(); + + if (peersDisputeOptional.isPresent()) { + var peerDispute = peersDisputeOptional.get(); + var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); + peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); + peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); + peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); + resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); + closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); + } else { + throw new IllegalStateException("could not find peer dispute"); + } + + arbitrationManager.requestPersistence(); } - applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); - - // resolve the payout - resolveDisputePayout(dispute, disputeResult, contract); - - // close dispute ticket - closeDispute(arbitrationManager, dispute, disputeResult, false); - - // close dispute ticket for peer - var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() - .filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) - .findFirst(); - - if (peersDisputeOptional.isPresent()) { - var peerDispute = peersDisputeOptional.get(); - var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); - peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); - peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); - peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); - resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); - closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); - } else { - throw new IllegalStateException("could not find peer dispute"); - } - - arbitrationManager.requestPersistence(); } catch (Exception e) { throw new IllegalStateException(e); } @@ -245,29 +254,36 @@ public class CoreDisputesService { // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) if (!dispute.isMediationDispute()) { try { - System.out.println(disputeResult); - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? - //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); + synchronized (tradeManager.getTrade(dispute.getTradeId())) { + System.out.println(disputeResult); + //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? + //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); - // TODO (woodser): don't send signed tx if opener is not co-signer? - // // determine if opener is co-signer - // boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); - // boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); - // if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx"); + // TODO (woodser): don't send signed tx if opener is not co-signer? + // // determine if opener is co-signer + // boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); + // boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); + // if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx"); - // arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex - boolean isOpener = dispute.isOpener(); - System.out.println("Is dispute opener: " + isOpener); - if (isOpener) { - MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); - System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); - if (arbitratorPayoutTx != null) - disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); + // open multisig wallet + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); + + // arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex + boolean isOpener = dispute.isOpener(); + System.out.println("Is dispute opener: " + isOpener); + if (isOpener) { + MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); + System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); + if (arbitratorPayoutTx != null) + disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); + } + + // send arbitrator's updated multisig hex with dispute result + disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex()); + + // close multisig wallet + xmrWalletService.closeMultisigWallet(dispute.getTradeId()); } - - // send arbitrator's updated multisig hex with dispute result - disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex()); } catch (AddressFormatException e2) { log.error("Error at close dispute", e2); return; diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 2c59402d7b..acde40380a 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -32,7 +32,7 @@ import bisq.core.payment.PaymentAccount; import bisq.core.user.User; import bisq.common.crypto.KeyRing; - +import bisq.common.handlers.ErrorMessageHandler; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.utils.Fiat; @@ -175,7 +175,10 @@ class CoreOffersService { List allKeyImages = new ArrayList(); for (Offer offer : offers) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (!allKeyImages.add(keyImage)) unreservedOffers.add(offer); + if (!allKeyImages.add(keyImage)) { + log.warn("Key image {} belongs to another offer, removing offer {}", keyImage, offer.getId()); + unreservedOffers.add(offer); + } } } @@ -191,7 +194,10 @@ class CoreOffersService { for (Offer offer : offers) { if (unreservedOffers.contains(offer)) continue; for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (spentKeyImages.contains(keyImage)) unreservedOffers.add(offer); + if (spentKeyImages.contains(keyImage)) { + log.warn("Offer {} reserved funds have already been spent with key image {}", offer.getId(), keyImage); + unreservedOffers.add(offer); + } } } @@ -216,7 +222,8 @@ class CoreOffersService { double buyerSecurityDeposit, long triggerPrice, String paymentAccountId, - Consumer resultHandler) { + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); @@ -252,7 +259,8 @@ class CoreOffersService { buyerSecurityDeposit, triggerPrice, useSavingsWallet, - transaction -> resultHandler.accept(offer)); + transaction -> resultHandler.accept(offer), + errorMessageHandler); } // Edit a placed offer. @@ -303,16 +311,14 @@ class CoreOffersService { double buyerSecurityDeposit, long triggerPrice, boolean useSavingsWallet, - Consumer resultHandler) { + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { openOfferManager.placeOffer(offer, buyerSecurityDeposit, useSavingsWallet, triggerPrice, resultHandler::accept, - log::error); - - if (offer.getErrorMessage() != null) - throw new IllegalStateException(offer.getErrorMessage()); + errorMessageHandler); } private boolean offerMatchesDirectionAndCurrency(Offer offer, diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 7cbdcc0341..f06c2aef65 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -100,7 +100,7 @@ class CorePaymentAccountsService { // Crypto Currency Accounts - PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + synchronized PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 8e81e2aee3..1726329884 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -103,16 +103,24 @@ class CoreTradesService { throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId)); var useSavingsWallet = true; - //noinspection ConstantConditions - takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet); - log.info("Initiating take {} offer, {}", - offer.isBuyOffer() ? "buy" : "sell", - takeOfferModel); - //noinspection ConstantConditions + + // synchronize access to take offer model // TODO (woodser): to avoid synchronizing, don't use stateful model + Coin txFeeFromFeeService; // TODO (woodser): remove this and other unused fields + Coin takerFee; + Coin fundsNeededForTrade; + synchronized (takeOfferModel) { + takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet); + txFeeFromFeeService = takeOfferModel.getTxFeeFromFeeService(); + takerFee = takeOfferModel.getTakerFee(); + fundsNeededForTrade = takeOfferModel.getFundsNeededForTrade(); + log.info("Initiating take {} offer, {}", offer.isBuyOffer() ? "buy" : "sell", takeOfferModel); + } + + // take offer tradeManager.onTakeOffer(offer.getAmount(), - takeOfferModel.getTxFeeFromFeeService(), - takeOfferModel.getTakerFee(), - takeOfferModel.getFundsNeededForTrade(), + txFeeFromFeeService, + takerFee, + fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, @@ -225,7 +233,7 @@ class CoreTradesService { } private Optional getOpenTrade(String tradeId) { - return tradeManager.getTradeById(tradeId); + return tradeManager.getOpenTrade(tradeId); } private Optional getClosedTrade(String tradeId) { @@ -236,14 +244,14 @@ class CoreTradesService { List getTrades() { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); - List trades = new ArrayList(tradeManager.getTrades()); + List trades = new ArrayList(tradeManager.getOpenTrades()); trades.addAll(closedTradableManager.getClosedTrades()); return trades; } List getChatMessages(String tradeId) { Trade trade; - var tradeOptional = tradeManager.getTradeById(tradeId); + var tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) trade = tradeOptional.get(); else throw new IllegalStateException(format("trade with id '%s' not found", tradeId)); boolean isMaker = tradeManager.isMyOffer(trade.getOffer()); @@ -253,7 +261,7 @@ class CoreTradesService { void sendChatMessage(String tradeId, String message) { Trade trade; - var tradeOptional = tradeManager.getTradeById(tradeId); + var tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) trade = tradeOptional.get(); else throw new IllegalStateException(format("trade with id '%s' not found", tradeId)); boolean isMaker = tradeManager.isMyOffer(trade.getOffer()); diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index c929e9666a..6394e7f726 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -509,12 +509,12 @@ class CoreWalletsService { } - // Throws a RuntimeException if wallet currency code is not BTC. + // Throws a RuntimeException if wallet currency code is not BTC or XMR. private void verifyWalletCurrencyCodeIsValid(String currencyCode) { if (currencyCode == null || currencyCode.isEmpty()) return; - if (!currencyCode.equalsIgnoreCase("BTC")) + if (!currencyCode.equalsIgnoreCase("BTC") && !currencyCode.equalsIgnoreCase("XMR")) throw new IllegalStateException(format("wallet does not support %s", currencyCode)); } diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index 2eda918631..f067928628 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -94,7 +94,7 @@ public class Balances { private void updatedBalances() { // Need to delay a bit to get the balances correct - UserThread.execute(() -> { + UserThread.execute(() -> { // TODO (woodser): running on user thread because JFX properties updated for legacy app updateAvailableBalance(); updateLockedBalance(); updateReservedOfferBalance(); diff --git a/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java b/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java index 5e9ee0bacc..6d207d48d1 100644 --- a/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java @@ -46,44 +46,51 @@ public class MoneroWalletRpcManager { * @return a client connected to the monero-wallet-rpc instance */ public MoneroWalletRpc startInstance(List cmd) { + try { - try { - - // register given port - if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) { - int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1; - int port = Integer.parseInt(cmd.get(portArgumentPosition)); - MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process - registeredPorts.put(port, walletRpc); - return walletRpc; - } - - // register assigned ports up to maximum attempts - else { - int numAttempts = 0; - while (numAttempts < NUM_ALLOWED_ATTEMPTS) { - int port = -1; - try { - numAttempts++; - port = registerPort(); - List cmdCopy = new ArrayList<>(cmd); // preserve original cmd - cmdCopy.add(RPC_BIND_PORT_ARGUMENT); - cmdCopy.add("" + port); - MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process - registeredPorts.put(port, walletRpc); - return walletRpc; - } catch (Exception e) { - if (numAttempts >= NUM_ALLOWED_ATTEMPTS) { - log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); - throw e; - } + // register given port + if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) { + int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1; + int port = Integer.parseInt(cmd.get(portArgumentPosition)); + synchronized (registeredPorts) { + if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered"); + registeredPorts.put(port, null); + } + MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process + synchronized (registeredPorts) { + registeredPorts.put(port, walletRpc); + } + return walletRpc; } - } - throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here + + // register assigned ports up to maximum attempts + else { + int numAttempts = 0; + while (numAttempts < NUM_ALLOWED_ATTEMPTS) { + int port = -1; + try { + numAttempts++; + port = registerPort(); + List cmdCopy = new ArrayList<>(cmd); // preserve original cmd + cmdCopy.add(RPC_BIND_PORT_ARGUMENT); + cmdCopy.add("" + port); + MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process + synchronized (registeredPorts) { + registeredPorts.put(port, walletRpc); + } + return walletRpc; + } catch (Exception e) { + if (numAttempts >= NUM_ALLOWED_ATTEMPTS) { + log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); + throw e; + } + } + } + throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here + } + } catch (IOException e) { + throw new MoneroError(e); } - } catch (IOException e) { - throw new MoneroError(e); - } } /** @@ -93,41 +100,59 @@ public class MoneroWalletRpcManager { * @param save specifies if the wallet should be saved before closing */ public void stopInstance(MoneroWalletRpc walletRpc, boolean save) { - boolean found = false; - for (Map.Entry entry : registeredPorts.entrySet()) { - if (walletRpc == entry.getValue()) { - walletRpc.close(save); - walletRpc.stopProcess(); - found = true; - try { unregisterPort(entry.getKey()); } - catch (Exception e) { throw new MoneroError(e); } - break; + + // unregister port + synchronized (registeredPorts) { + boolean found = false; + for (Map.Entry entry : registeredPorts.entrySet()) { + if (walletRpc == entry.getValue()) { + found = true; + try { + unregisterPort(entry.getKey()); + } catch (Exception e) { + throw new MoneroError(e); + } + break; + } + } + if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port"); } - } - if (!found) throw new RuntimeException("MoneroWalletRpc instance not associated with port"); + + // close wallet and stop process + walletRpc.close(save); + walletRpc.stopProcess(); } private int registerPort() throws IOException { + synchronized (registeredPorts) { - // register next consecutive port - if (startPort != null) { - int port = startPort; - while (registeredPorts.containsKey(port)) port++; - registeredPorts.put(port, null); - return port; - } + // register next consecutive port + if (startPort != null) { + int port = startPort; + while (registeredPorts.containsKey(port)) port++; + registeredPorts.put(port, null); + return port; + } - // register auto-assigned port - else { + // register auto-assigned port + else { + int port = getLocalPort(); + registeredPorts.put(port, null); + return port; + } + } + } + + private void unregisterPort(int port) { + synchronized (registeredPorts) { + registeredPorts.remove(port); + } + } + + private int getLocalPort() throws IOException { ServerSocket socket = new ServerSocket(0); // use socket to get available port int port = socket.getLocalPort(); socket.close(); - registeredPorts.put(port, null); return port; - } - } - - private void unregisterPort(int port) { - registeredPorts.remove(port); } } diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index 7c65661ea7..71ec8a45fd 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -63,7 +63,7 @@ public class XmrWalletService { private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user"; private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_NAME = "haveno_XMR"; - private static final long MONERO_WALLET_SYNC_RATE = 5000l; + private static final long MONERO_WALLET_SYNC_PERIOD = 5000l; private final CoreAccountService accountService; private final CoreMoneroConnectionsService connectionsService; @@ -154,61 +154,57 @@ public class XmrWalletService { return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); } - public boolean walletExists(String walletName) { - String path = walletDir.toString() + File.separator + walletName; - return new File(path + ".keys").exists(); - } - - public void closeWallet(MoneroWallet walletRpc, boolean save) { - log.info("{}.closeWallet({}, {})", getClass(), walletRpc.getPath(), save); - MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save); - } - - public void deleteWallet(String walletName) { - log.info("{}.deleteWallet({})", getClass(), walletName); - if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); - String path = walletDir.toString() + File.separator + walletName; - if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - // WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup? + public boolean multisigWalletExists(String tradeId) { + return walletExists("xmr_multisig_trade_" + tradeId); } // TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse - public synchronized MoneroWallet createMultisigWallet(String tradeId) { - log.info("{}.createMultisigWallet({})", getClass(), tradeId); - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = "xmr_multisig_trade_" + tradeId; - MoneroWallet multisigWallet = null; - multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port - multisigWallets.put(tradeId, multisigWallet); - multisigWallet.startSyncing(5000l); - return multisigWallet; - } - - public MoneroWallet getMultisigWallet(String tradeId) { // TODO (woodser): synchronize per wallet id - log.info("{}.getMultisigWallet({})", getClass(), tradeId); - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = "xmr_multisig_trade_" + tradeId; - if (!walletExists(path)) return null; - MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); - multisigWallets.put(tradeId, multisigWallet); - multisigWallet.startSyncing(5000l); // TODO (woodser): use sync period from config. apps stall if too many multisig wallets and too short sync period - return multisigWallet; - } - - public synchronized boolean deleteMultisigWallet(String tradeId) { - log.info("{}.deleteMultisigWallet({})", getClass(), tradeId); - String walletName = "xmr_multisig_trade_" + tradeId; - if (!walletExists(walletName)) return false; - try { - closeWallet(getMultisigWallet(tradeId), false); - } catch (Exception err) { - // multisig wallet may not be open + public MoneroWallet createMultisigWallet(String tradeId) { + log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); + Trade trade = tradeManager.getOpenTrade(tradeId).get(); + synchronized (trade) { + if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId()); + String path = "xmr_multisig_trade_" + trade.getId(); + MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port + multisigWallets.put(trade.getId(), multisigWallet); + return multisigWallet; + } + } + + // TODO (woodser): provide progress notifications during open? + public MoneroWallet getMultisigWallet(String tradeId) { + log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId); + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { + if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId()); + String path = "xmr_multisig_trade_" + trade.getId(); + if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + trade.getId()); + MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); + multisigWallets.put(trade.getId(), multisigWallet); + return multisigWallet; + } + } + + public void closeMultisigWallet(String tradeId) { + log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { + if (!multisigWallets.containsKey(trade.getId())) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + trade.getId()); + MoneroWallet wallet = multisigWallets.remove(trade.getId()); + closeWallet(wallet, true); + } + } + + public boolean deleteMultisigWallet(String tradeId) { + log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { + String walletName = "xmr_multisig_trade_" + tradeId; + if (!walletExists(walletName)) return false; + if (multisigWallets.containsKey(trade.getId())) closeMultisigWallet(tradeId); + deleteWallet(walletName); + return true; } - deleteWallet(walletName); - multisigWallets.remove(tradeId); - return true; } public MoneroTxWallet createTx(List destinations) { @@ -240,6 +236,11 @@ public class XmrWalletService { setWalletDaemonConnections(newConnection); }); } + + private boolean walletExists(String walletName) { + String path = walletDir.toString() + File.separator + walletName; + return new File(path + ".keys").exists(); + } private void tryInitMainWallet() { MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); @@ -288,7 +289,7 @@ public class XmrWalletService { // create wallet try { walletRpc.createWallet(config); - walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); + walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD); return walletRpc; } catch (Exception e) { e.printStackTrace(); @@ -305,7 +306,7 @@ public class XmrWalletService { // open wallet try { walletRpc.openWallet(config); - walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); + walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD); return walletRpc; } catch (Exception e) { e.printStackTrace(); @@ -370,7 +371,7 @@ public class XmrWalletService { } private void changeWalletPasswords(String oldPassword, String newPassword) { - List tradeIds = tradeManager.getTrades().stream().map(Trade::getId).collect(Collectors.toList()); + List tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList()); ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size())); pool.submit(new Runnable() { @Override @@ -404,7 +405,22 @@ public class XmrWalletService { throw new RuntimeException(e); } } - + + private void closeWallet(MoneroWallet walletRpc, boolean save) { + log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save); + MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save); + } + + private void deleteWallet(String walletName) { + log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); + if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); + String path = walletDir.toString() + File.separator + walletName; + if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); + if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); + if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); + // WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup? + } + private void closeAllWallets() { // collect wallets to shutdown diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index e7231e6fc9..0c857dfff7 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -413,7 +413,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol( model, transaction -> { - + // save reserve tx with open offer OpenOffer openOffer = new OpenOffer(offer, triggerPrice, model.getReserveTx().getHash(), model.getReserveTx().getFullHex(), model.getReserveTx().getKey()); openOffers.add(openOffer); @@ -428,8 +428,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, errorMessageHandler ); - - placeOfferProtocols.put(offer.getId(), placeOfferProtocol); + + synchronized (placeOfferProtocols) { + placeOfferProtocols.put(offer.getId(), placeOfferProtocol); + } placeOfferProtocol.placeOffer(); // TODO (woodser): if error placing offer (e.g. bad signature), remove protocol and unfreeze trade funds } @@ -567,13 +569,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void onRemoved(@NotNull OpenOffer openOffer, ResultHandler resultHandler, Offer offer) { + for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); openOffers.remove(openOffer); closedTradableManager.add(openOffer); log.info("onRemoved offerId={}", offer.getId()); btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); - for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage); requestPersistence(); resultHandler.handleResult(); } @@ -725,14 +727,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void handleSignOfferResponse(SignOfferResponse response, NodeAddress peer) { log.info("Received SignOfferResponse from {} with offerId {} and uid {}", peer, response.getOfferId(), response.getUid()); - + // get previously created protocol - PlaceOfferProtocol protocol = placeOfferProtocols.get(response.getOfferId()); - if (protocol == null) { - log.warn("No place offer protocol created for offer " + response.getOfferId()); - return; + PlaceOfferProtocol protocol; + synchronized (placeOfferProtocols) { + protocol = placeOfferProtocols.get(response.getOfferId()); + if (protocol == null) { + log.warn("No place offer protocol created for offer " + response.getOfferId()); + return; + } } - + // handle response protocol.handleSignOfferResponse(response, peer); } diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java index 6069413513..02d70513e4 100644 --- a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityProtocol.java @@ -114,7 +114,7 @@ public class OfferAvailabilityProtocol { /////////////////////////////////////////////////////////////////////////////////////////// private void handleOfferAvailabilityResponse(OfferAvailabilityResponse message, NodeAddress peersNodeAddress) { - log.info("Received handleOfferAvailabilityResponse from {} with offerId {} and uid {}", + log.info("Received OfferAvailabilityResponse from {} with offerId {} and uid {}", peersNodeAddress, message.getOfferId(), message.getUid()); stopTimeout(); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java index 07edf664ea..8549f37dd3 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java @@ -45,27 +45,26 @@ public class MakerReservesTradeFunds extends Task { try { runInterceptHook(); - // create transaction to reserve trade - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); - MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount); - - // freeze reserved outputs - // TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs - List reservedKeyImages = new ArrayList(); - MoneroWallet wallet = model.getXmrWalletService().getWallet(); - for (MoneroOutput input : reserveTx.getInputs()) { - reservedKeyImages.add(input.getKeyImage().getHex()); - wallet.freezeOutput(input.getKeyImage().getHex()); + // synchronize on wallet to reserve key images + synchronized (model.getXmrWalletService().getWallet()) { + + // create transaction to reserve trade + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); + MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount); + + // collect reserved key images // TODO (woodser): switch to proof of reserve? + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // save offer state + // TODO (woodser): persist + model.setReserveTx(reserveTx); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field + complete(); } - - // save offer state - // TODO (woodser): persist - model.setReserveTx(reserveTx); - offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); - offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field - complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java index 89a97ea3ff..ddf0a90058 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java @@ -84,6 +84,7 @@ public class MakerSendsSignOfferRequest extends Task { if (!sender.equals(arbitrator.getNodeAddress())) return; AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope(); if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return; + if (!ackMessage.getSourceUid().equals(request.getUid())) return; if (ackMessage.isSuccess()) { offer.setState(Offer.State.OFFER_FEE_RESERVED); model.getP2PService().removeDecryptedDirectMessageListener(this); diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java index 628a2beb9c..1d4666fd84 100644 --- a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -114,7 +114,7 @@ public class TakeOfferModel implements Model { this.clearModel(); this.offer = offer; this.paymentAccount = paymentAccount; - this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); + this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); // TODO (woodser): replace with xmr or remove validateModelInputs(); this.useSavingsWallet = useSavingsWallet; diff --git a/core/src/main/java/bisq/core/presentation/TradePresentation.java b/core/src/main/java/bisq/core/presentation/TradePresentation.java index a64383edfa..f3ffa04f39 100644 --- a/core/src/main/java/bisq/core/presentation/TradePresentation.java +++ b/core/src/main/java/bisq/core/presentation/TradePresentation.java @@ -17,6 +17,7 @@ package bisq.core.presentation; +import bisq.common.UserThread; import bisq.core.trade.TradeManager; import javax.inject.Inject; @@ -38,10 +39,12 @@ public class TradePresentation { public TradePresentation(TradeManager tradeManager) { tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> { long numPendingTrades = (long) newValue; - if (numPendingTrades > 0) - this.numPendingTrades.set(String.valueOf(numPendingTrades)); + UserThread.execute(() -> { + if (numPendingTrades > 0) + this.numPendingTrades.set(String.valueOf(numPendingTrades)); - showPendingTradesNotification.set(numPendingTrades > 0); + showPendingTradesNotification.set(numPendingTrades > 0); + }); }); } } diff --git a/core/src/main/java/bisq/core/provider/fee/FeeService.java b/core/src/main/java/bisq/core/provider/fee/FeeService.java index 5609ae0f3b..161b338a98 100644 --- a/core/src/main/java/bisq/core/provider/fee/FeeService.java +++ b/core/src/main/java/bisq/core/provider/fee/FeeService.java @@ -128,11 +128,6 @@ public class FeeService { } public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) { - if (feeProvider.getHttpClient().hasPendingRequest()) { - log.warn("We have a pending request open. We ignore that request. httpClient {}", feeProvider.getHttpClient()); - return; - } - long now = Instant.now().getEpochSecond(); // We all requests only each 2 minutes if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) { diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index e233e5447f..2f7a56181a 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -51,6 +51,7 @@ public abstract class SupportManager { protected final CoreMoneroConnectionsService connectionService; protected final CoreNotificationService notificationService; protected final Map delayMsgMap = new HashMap<>(); + private final Object lock = new Object(); private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); protected final MailboxMessageService mailboxMessageService; @@ -69,16 +70,24 @@ public abstract class SupportManager { // We get first the message handler called then the onBootstrapped p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { - // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was - // already stored - decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); + if (isReady()) applyDirectMessage(decryptedMessageWithPubKey); + else { + synchronized (lock) { + // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored + decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + } + } }); mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { - // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was - // already stored - decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); + if (isReady()) applyMailboxMessage(decryptedMessageWithPubKey); + else { + synchronized (lock) { + // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored + decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + } + } }); } @@ -138,6 +147,7 @@ public abstract class SupportManager { protected void onChatMessage(ChatMessage chatMessage) { final String tradeId = chatMessage.getTradeId(); final String uid = chatMessage.getUid(); + log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid); boolean channelOpen = channelOpen(chatMessage); if (!channelOpen) { log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); @@ -293,6 +303,11 @@ public abstract class SupportManager { } } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + private boolean isReady() { return allServicesInitialized && p2PService.isBootstrapped() && @@ -306,29 +321,34 @@ public abstract class SupportManager { /////////////////////////////////////////////////////////////////////////////////////////// private void applyMessages() { - decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - if (networkEnvelope instanceof SupportMessage) { - onSupportMessage((SupportMessage) networkEnvelope); - } else if (networkEnvelope instanceof AckMessage) { - onAckMessage((AckMessage) networkEnvelope); - } - }); - decryptedDirectMessageWithPubKeys.clear(); + synchronized (lock) { + decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); + decryptedDirectMessageWithPubKeys.clear(); + decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); + decryptedMailboxMessageWithPubKeys.clear(); + } + } - decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName()); - if (networkEnvelope instanceof SupportMessage) { - SupportMessage supportMessage = (SupportMessage) networkEnvelope; - onSupportMessage(supportMessage); - mailboxMessageService.removeMailboxMsg(supportMessage); - } else if (networkEnvelope instanceof AckMessage) { - AckMessage ackMessage = (AckMessage) networkEnvelope; - onAckMessage(ackMessage); - mailboxMessageService.removeMailboxMsg(ackMessage); - } - }); - decryptedMailboxMessageWithPubKeys.clear(); + private void applyDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof SupportMessage) { + onSupportMessage((SupportMessage) networkEnvelope); + } else if (networkEnvelope instanceof AckMessage) { + onAckMessage((AckMessage) networkEnvelope); + } + } + + private void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName()); + if (networkEnvelope instanceof SupportMessage) { + SupportMessage supportMessage = (SupportMessage) networkEnvelope; + onSupportMessage(supportMessage); + mailboxMessageService.removeMailboxMsg(supportMessage); + } else if (networkEnvelope instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) networkEnvelope; + onAckMessage(ackMessage); + mailboxMessageService.removeMailboxMsg(ackMessage); + } } } diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index 72d64201db..db88d88e2c 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -23,7 +23,7 @@ import bisq.core.proto.CoreProtoResolver; import bisq.core.support.SupportType; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Contract; - +import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkPayload; @@ -365,7 +365,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { public void setState(Dispute.State disputeState) { this.disputeState = disputeState; - this.isClosedProperty.set(disputeState == State.CLOSED); + UserThread.execute(() -> this.isClosedProperty.set(disputeState == State.CLOSED)); } public void setDisputeResult(DisputeResult disputeResult) { diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 0ef3068d3e..fed8feec3d 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -302,67 +302,81 @@ public abstract class DisputeManager> extends Sup return; } - String errorMessage = null; Dispute dispute = openNewDisputeMessage.getDispute(); + log.info("{}.onOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); // Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before dispute.setSupportType(openNewDisputeMessage.getSupportType()); // disputes from clients < 1.6.0 have state not set as the field didn't exist before dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release + + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } - Contract contract = dispute.getContract(); - addPriceInfoMessage(dispute, 0); + synchronized (trade) { - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - if (isAgent(dispute)) { + String errorMessage = null; + Contract contract = dispute.getContract(); + addPriceInfoMessage(dispute, 0); - // update arbitrator's multisig wallet - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex())); - System.out.println("Arbitrator multisig wallet updated on new dispute message, current txs:"); - System.out.println(multisigWallet.getTxs()); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + if (isAgent(dispute)) { - if (!disputeList.contains(dispute)) { - Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { - disputeList.add(dispute); - sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); - } else { - // valid case if both have opened a dispute and agent was not online. - log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", - dispute.getTradeId()); + // update arbitrator's multisig wallet + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); + multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex())); + log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId()); + + // close multisig wallet + xmrWalletService.closeMultisigWallet(dispute.getTradeId()); + + synchronized (disputeList) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + disputeList.add(dispute); + sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } } } else { - errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); + errorMessage = "Trader received openNewDisputeMessage. That must never happen."; + log.error(errorMessage); } - } else { - errorMessage = "Trader received openNewDisputeMessage. That must never happen."; - log.error(errorMessage); - } - // We use the ChatMessage not the openNewDisputeMessage for the ACK - ObservableList messages = openNewDisputeMessage.getDispute().getChatMessages(); - if (!messages.isEmpty()) { - ChatMessage msg = messages.get(0); - PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); - sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); - } + // We use the ChatMessage not the openNewDisputeMessage for the ACK + ObservableList messages = openNewDisputeMessage.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); + } - addMediationResultMessage(dispute); + addMediationResultMessage(dispute); - try { - TradeDataValidation.validatePaymentAccountPayloads(dispute); - TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx()); - //TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed? - TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); - TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config); - } catch (TradeDataValidation.AddressException | - TradeDataValidation.NodeAddressException | - TradeDataValidation.InvalidPaymentAccountPayloadException e) { - log.error(e.toString()); - validationExceptions.add(e); + try { + TradeDataValidation.validatePaymentAccountPayloads(dispute); + TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx()); + //TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed? + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config); + } catch (TradeDataValidation.AddressException | + TradeDataValidation.NodeAddressException | + TradeDataValidation.InvalidPaymentAccountPayloadException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + requestPersistence(); } - requestPersistence(); } // Not-dispute-requester receives that msg from dispute agent @@ -375,44 +389,49 @@ public abstract class DisputeManager> extends Sup String errorMessage = null; Dispute dispute = peerOpenedDisputeMessage.getDispute(); + log.info("{}.onPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); - Optional optionalTrade = tradeManager.getTradeById(dispute.getTradeId()); + Optional optionalTrade = tradeManager.getOpenTrade(dispute.getTradeId()); if (!optionalTrade.isPresent()) { return; } Trade trade = optionalTrade.get(); - - if (!isAgent(dispute)) { - if (!disputeList.contains(dispute)) { - Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { - disputeList.add(dispute); - trade.setDisputeState(getDisputeStateStartedByPeer()); - tradeManager.requestPersistence(); - errorMessage = null; - } else { - // valid case if both have opened a dispute and agent was not online. - log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", - dispute.getTradeId()); + + synchronized (trade) { + if (!isAgent(dispute)) { + synchronized (disputeList) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + disputeList.add(dispute); + trade.setDisputeState(getDisputeStateStartedByPeer()); + tradeManager.requestPersistence(); + errorMessage = null; + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } } } else { - errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); + errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen."; + log.error(errorMessage); } - } else { - errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen."; - log.error(errorMessage); - } - // We use the ChatMessage not the peerOpenedDisputeMessage for the ACK - ObservableList messages = peerOpenedDisputeMessage.getDispute().getChatMessages(); - if (!messages.isEmpty()) { - ChatMessage msg = messages.get(0); - sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); - } + // We use the ChatMessage not the peerOpenedDisputeMessage for the ACK + ObservableList messages = peerOpenedDisputeMessage.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + } - sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); - requestPersistence(); + sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + requestPersistence(); + } } @@ -425,107 +444,112 @@ public abstract class DisputeManager> extends Sup String updatedMultisigHex, ResultHandler resultHandler, FaultHandler faultHandler) { + log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); + T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return; } - if (disputeList.contains(dispute)) { - String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); - return; - } - - Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent() || reOpen) { - String disputeInfo = getDisputeInfo(dispute); - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) - : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); - - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - pubKeyRing.hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress()); - chatMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(chatMessage); - if (!reOpen) { - disputeList.add(dispute); + synchronized (disputeList) { + if (disputeList.contains(dispute)) { + String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + return; } - NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); - OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType(), - updatedMultisigHex); - log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - chatMessage.getUid()); - mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, - dispute.getAgentPubKeyRing(), - openNewDisputeMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - chatMessage.getUid()); + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent() || reOpen) { + String disputeInfo = getDisputeInfo(dispute); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) + : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - requestPersistence(); - resultHandler.handleResult(); + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + if (!reOpen) { + disputeList.add(dispute); + } + + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType(), + updatedMultisigHex); + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, + dispute.getAgentPubKeyRing(), + openNewDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + requestPersistence(); + faultHandler.handleFault("Sending dispute message failed: " + + errorMessage, new DisputeMessageDeliveryFailedException()); + } } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - chatMessage.getUid(), errorMessage); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - requestPersistence(); - faultHandler.handleFault("Sending dispute message failed: " + - errorMessage, new DisputeMessageDeliveryFailedException()); - } - } - ); - } else { - String msg = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + ); + } else { + String msg = "We got a dispute already open for that trade and trading peer.\n" + + "TradeId = " + dispute.getTradeId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + } } + requestPersistence(); } @@ -533,6 +557,7 @@ public abstract class DisputeManager> extends Sup private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing) { + log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId()); // We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is // being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct // message and not skip the system message of the peer as it would be the case if we have created the system msg @@ -604,7 +629,9 @@ public abstract class DisputeManager> extends Sup addPriceInfoMessage(dispute, 0); - disputeList.add(dispute); + synchronized (disputeList) { + disputeList.add(dispute); + } // We mirrored dispute already! Contract contract = dispute.getContract(); @@ -826,9 +853,9 @@ public abstract class DisputeManager> extends Sup } public Optional findTrade(Dispute dispute) { - Optional retVal = tradeManager.getTradeById(dispute.getTradeId()); + Optional retVal = tradeManager.getOpenTrade(dispute.getTradeId()); if (!retVal.isPresent()) { - retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst(); + retVal = tradeManager.getClosedTrade(dispute.getTradeId()); } return retVal; } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 0f02994469..bce35d6a0a 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -120,8 +120,8 @@ public final class ArbitrationManager extends DisputeManager tradeOptional = tradeManager.getTradeById(disputeResult.getTradeId()); + Optional tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId()); String tradeId = disputeResult.getTradeId(); + log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId()); Optional disputeOptional = findDispute(disputeResult); String uid = disputeResultMessage.getUid(); if (!disputeOptional.isPresent()) { @@ -239,7 +240,6 @@ public final class ArbitrationManager extends DisputeManager tradableOptional = closedTradableManager.getTradableById(tradeId); if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) { - payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); + payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); // TODO (woodser): payout tx is transient so won't exist after restart? } } @@ -334,7 +334,14 @@ public final class ArbitrationManager extends DisputeManager disputeOptional = findDispute(tradeId); - if (!disputeOptional.isPresent()) { - log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); - if (!delayMsgMap.containsKey(uid)) { - // We delay 3 sec. to be sure the close msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3); - delayMsgMap.put(uid, timer); - } else { - log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " + - "That should never happen. TradeId = " + tradeId); + Trade trade = tradeManager.getTrade(tradeId); + + synchronized (trade) { + + // get dispute and trade + Optional disputeOptional = findDispute(tradeId); + if (!disputeOptional.isPresent()) { + log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 3 sec. to be sure the close msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; } - return; + Dispute dispute = disputeOptional.get(); + + Contract contract = dispute.getContract(); + boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); + PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + + cleanupRetryMap(uid); + + // update multisig wallet + if (xmrWalletService.multisigWalletExists(tradeId)) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion? + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); + multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex())); + MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0); + xmrWalletService.closeMultisigWallet(tradeId); + dispute.setDisputePayoutTxId(parsedPayoutTx.getHash()); + XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx); + } + +// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs()); +// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx"); +// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet()); + + // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute + sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); + requestPersistence(); } - - Dispute dispute = disputeOptional.get(); - Contract contract = dispute.getContract(); - boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); - PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - - cleanupRetryMap(uid); - - // update multisig wallet - // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion? - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - if (multisigWallet != null) { - multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex())); - MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0); - dispute.setDisputePayoutTxId(parsedPayoutTx.getHash()); - XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx); - } - -// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs()); -// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx"); -// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet()); - - // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute - sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); - requestPersistence(); } // Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) { + log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName()); String tradeId = request.getTradeId(); - Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get(); - DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); - Contract contract = dispute.getContract(); + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { + Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get(); + DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); + Contract contract = dispute.getContract(); - // verify sender is co-signer and receiver is arbitrator - System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first - System.out.println(disputeResult); - System.out.println(disputeResult.getWinner()); - System.out.println(contract.getBuyerNodeAddress()); - System.out.println(contract.getSellerNodeAddress()); - boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress())); - boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher(); - boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing()); + // verify sender is co-signer and receiver is arbitrator + System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first + System.out.println(disputeResult); + System.out.println(disputeResult.getWinner()); + System.out.println(contract.getBuyerNodeAddress()); + System.out.println(contract.getSellerNodeAddress()); + boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress())); + boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher(); + boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing()); - System.out.println("TESTING PUB KEY RINGS"); - System.out.println(pubKeyRing); - System.out.println(dispute.getAgentPubKeyRing()); - System.out.println("Receiver is arbitrator: " + receiverIsArbitrator); + System.out.println("TESTING PUB KEY RINGS"); + System.out.println(pubKeyRing); + System.out.println(dispute.getAgentPubKeyRing()); + System.out.println("Receiver is arbitrator: " + receiverIsArbitrator); - if (!senderIsCosigner) { - log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId); - return; - } - if (!receiverIsArbitrator) { - log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId); - return; - } - - // update arbitrator's multisig wallet with co-signer's multisig hex - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - try { - multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex())); - } catch (Exception e) { - log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId); - return; - } - - // create updated payout tx - MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); - System.out.println("Arbitrator created updated payout tx for co-signer!!!"); - System.out.println(payoutTx); - - // send updated payout tx to sender - PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); - ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse( - tradeId, - p2PService.getAddress(), - UUID.randomUUID().toString(), - SupportType.ARBITRATION, - payoutTx.getTxSet().getMultisigTxHex()); - log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid()); - p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(), - senderPubKeyRing, - response, - new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid()); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage); - } + if (!senderIsCosigner) { + log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId); + return; } - ); + if (!receiverIsArbitrator) { + log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId); + return; + } + + // update arbitrator's multisig wallet with co-signer's multisig hex + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); + try { + multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex())); + } catch (Exception e) { + log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId); + return; + } + + // create updated payout tx + MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); + System.out.println("Arbitrator created updated payout tx for co-signer!!!"); + System.out.println(payoutTx); + + // close multisig wallet + xmrWalletService.closeMultisigWallet(tradeId); + + // send updated payout tx to sender + PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse( + tradeId, + p2PService.getAddress(), + UUID.randomUUID().toString(), + SupportType.ARBITRATION, + payoutTx.getTxSet().getMultisigTxHex()); + log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid()); + p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(), + senderPubKeyRing, + response, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage); + } + } + ); + } } // Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer) private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) { + log.info("{}.onArbitratorPayoutTxResponse()", getClass().getSimpleName()); // gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc String tradeId = response.getTradeId(); + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { - // verify and sign dispute payout tx - MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex()); + // verify and sign dispute payout tx + MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex()); - // process fully signed payout tx (publish, notify peer, etc) - onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx); + // process fully signed payout tx (publish, notify peer, etc) + onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx); + } + } + + private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) { + + // gather trade info + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); + Optional disputeOptional = findDispute(tradeId); + if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); + Dispute dispute = disputeOptional.get(); + Contract contract = dispute.getContract(); + DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); + +// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); +// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids +// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount(); +// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER); + + // parse arbitrator-signed payout tx + MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); + if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack + MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0); + log.info("Received updated multisig hex and partially signed payout tx from arbitrator:\n" + arbitratorSignedPayoutTx); + + // verify payout tx has 1 or 2 destinations + int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size(); + if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations"); + + // get buyer and seller destinations (order not preserved) + List destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations(); + boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); + MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null; + MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0); + + // verify payout addresses + if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract"); + if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract"); + + // verify change address is multisig's primary address + if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); + + // verify sum of outputs = destination amounts + change amount + BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount()); + if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); + + // TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx) + + // verify winner and loser payout amounts + BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change + BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); + BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); + if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0 + else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost + BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount(); + BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount(); + if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount); + if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount); + + // update multisig wallet from arbitrator + multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex())); + + // sign arbitrator-signed payout tx + MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex); + if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); + String signedMultisigTxHex = result.getSignedMultisigTxHex(); + parsedTxSet.setMultisigTxHex(signedMultisigTxHex); + return parsedTxSet; } private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) { // gather trade info - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); Optional disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) { log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); @@ -481,10 +570,12 @@ public final class ArbitrationManager extends DisputeManager txHashes = multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex()); + txSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed // update state trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? @@ -505,7 +596,7 @@ public final class ArbitrationManager extends DisputeManager openOfferOptional = openOfferManager.getOpenOfferById(tradeId); @@ -622,71 +713,4 @@ public final class ArbitrationManager extends DisputeManager disputeOptional = findDispute(tradeId); - if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); - Dispute dispute = disputeOptional.get(); - Contract contract = dispute.getContract(); - DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); - -// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); -// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids -// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount(); -// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER); - - // parse arbitrator-signed payout tx - MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); - if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack - MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0); - System.out.println("Parsed arbitrator-signed payout tx:\n" + arbitratorSignedPayoutTx); - - // verify payout tx has 1 or 2 destinations - int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size(); - if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations"); - - // get buyer and seller destinations (order not preserved) - List destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations(); - boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); - MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null; - MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0); - - // verify payout addresses - if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract"); - if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract"); - - // verify change address is multisig's primary address - if (arbitratorSignedPayoutTx.getChangeAddress() != null && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); - - // verify sum of outputs = destination amounts + change amount - BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount()); - if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); - - // TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx) - - // verify winner and loser payout amounts - BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change - BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); - BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); - if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0 - else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost - BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount(); - BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount(); - if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount); - if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount); - - // update multisig wallet from arbitrator - System.out.println("Updating multisig hex from arbitrator"); - multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex())); - - // sign arbitrator-signed payout tx - MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex); - if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); - String signedMultisigTxHex = result.getSignedMultisigTxHex(); - parsedTxSet.setMultisigTxHex(signedMultisigTxHex); - return parsedTxSet; - } } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index f4ae78213f..06575394a1 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -134,7 +134,7 @@ public final class MediationManager extends DisputeManager @Override public void cleanupDisputes() { disputeListService.cleanupDisputes(tradeId -> { - tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) + tradeManager.getOpenTrade(tradeId).filter(trade -> trade.getPayoutTx() != null) .ifPresent(trade -> { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); }); @@ -197,7 +197,7 @@ public final class MediationManager extends DisputeManager dispute.setDisputeResult(disputeResult); - Optional tradeOptional = tradeManager.getTradeById(tradeId); + Optional tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 064a9d195b..401ce5d40d 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -200,7 +200,7 @@ public final class RefundManager extends DisputeManager { dispute.setDisputeResult(disputeResult); - Optional tradeOptional = tradeManager.getTradeById(tradeId); + Optional tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED || @@ -215,7 +215,7 @@ public final class RefundManager extends DisputeManager { sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); // set state after payout as we call swapTradeEntryToAvailableEntry - if (tradeManager.getTradeById(tradeId).isPresent()) { + if (tradeManager.getOpenTrade(tradeId).isPresent()) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); diff --git a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java index a30448b228..4abcbe87f6 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -83,7 +83,7 @@ public class TraderChatManager extends SupportManager { @Override public NodeAddress getPeerNodeAddress(ChatMessage message) { - return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> { if (trade.getContract() != null) { return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get()); } else { @@ -94,7 +94,7 @@ public class TraderChatManager extends SupportManager { @Override public PubKeyRing getPeerPubKeyRing(ChatMessage message) { - return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> { if (trade.getContract() != null) { return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get()); } else { @@ -112,12 +112,12 @@ public class TraderChatManager extends SupportManager { @Override public boolean channelOpen(ChatMessage message) { - return tradeManager.getTradeById(message.getTradeId()).isPresent(); + return tradeManager.getOpenTrade(message.getTradeId()).isPresent(); } @Override public void addAndPersistChatMessage(ChatMessage message) { - tradeManager.getTradeById(message.getTradeId()).ifPresent(trade -> { + tradeManager.getOpenTrade(message.getTradeId()).ifPresent(trade -> { ObservableList chatMessages = trade.getChatMessages(); if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { if (chatMessages.isEmpty()) { diff --git a/core/src/main/java/bisq/core/trade/TradableList.java b/core/src/main/java/bisq/core/trade/TradableList.java index f0b163c5b9..65987a8ba1 100644 --- a/core/src/main/java/bisq/core/trade/TradableList.java +++ b/core/src/main/java/bisq/core/trade/TradableList.java @@ -54,10 +54,12 @@ public final class TradableList extends PersistableListAsObs @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setTradableList(protobuf.TradableList.newBuilder() - .addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class))) - .build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder() + .setTradableList(protobuf.TradableList.newBuilder() + .addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class))) + .build(); + } } public static TradableList fromProto(protobuf.TradableList proto, diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index eccd1f5c6f..5506ffceb1 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -63,6 +63,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; @@ -82,8 +83,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import monero.common.MoneroError; import monero.daemon.MoneroDaemon; +import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletListener; @@ -99,9 +100,11 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// public enum State { - // #################### Phase PREPARATION + // #################### Phase INIT // When trade protocol starts no funds are on stake PREPARATION(Phase.INIT), + CONTRACT_SIGNATURE_REQUESTED(Phase.INIT), // TODO (woodser): add more states for initializing multisig, etc to support trade initialization notifications + CONTRACT_SIGNED(Phase.INIT), // At first part maker/taker have different roles // taker perspective @@ -206,9 +209,9 @@ public abstract class Trade implements Tradable, Model { public enum Phase { INIT, - TAKER_FEE_PUBLISHED, + TAKER_FEE_PUBLISHED, // TODO (woodser): remove unused phases DEPOSIT_PUBLISHED, - DEPOSIT_CONFIRMED, + DEPOSIT_CONFIRMED, // TODO (woodser): rename to or add DEPOSIT_UNLOCKED FIAT_SENT, FIAT_RECEIVED, PAYOUT_PUBLISHED, @@ -463,8 +466,8 @@ public abstract class Trade implements Tradable, Model { transient MoneroWalletListener depositTxListener; transient Boolean makerDepositLocked; // null when unknown, true while locked, false when unlocked transient Boolean takerDepositLocked; - transient private MoneroTxWallet makerDepositTx; - transient private MoneroTxWallet takerDepositTx; + transient private MoneroTx makerDepositTx; + transient private MoneroTx takerDepositTx; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization @@ -696,72 +699,101 @@ public abstract class Trade implements Tradable, Model { else throw new RuntimeException("Unknown trade type: " + this.getClass().getName()); } - // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet. - void updateDepositTxFromWallet() { - if (getMakerDepositTx() != null && getTakerDepositTx() != null) { - MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(getId()); - applyDepositTxs(multisigWallet.getTx(getMakerDepositTx().getHash()), multisigWallet.getTx(getTakerDepositTx().getHash())); - } - } + /** + * Listen for deposit transactions to unlock and then apply the transactions. + * + * TODO: adopt for general purpose scheduling + * TODO: check and notify if deposits are dropped due to re-org + */ + public void listenForDepositTxs() { + log.info("Listening for deposit txs to unlock for trade {}", getId()); - public void applyDepositTxs(MoneroTxWallet makerDepositTx, MoneroTxWallet takerDepositTx) { - this.makerDepositTx = makerDepositTx; - this.takerDepositTx = takerDepositTx; - if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) { - setConfirmedState(); // TODO (woodser): bisq "confirmed" = xmr unlocked after 10 confirmations - } - } - - public void setupDepositTxsListener() { - // ignore if already listening if (depositTxListener != null) { log.warn("Trade {} already listening for deposit txs", getId()); return; } - - // create listener for deposit transactions - MoneroWallet multisigWallet = processModel.getXmrWalletService().getMultisigWallet(getId()); - depositTxListener = processModel.getXmrWalletService().new HavenoWalletListener(new MoneroWalletListener() { // TODO (woodser): separate into own class file - @Override - public void onOutputReceived(MoneroOutputWallet output) { + // get daemon and primary wallet + MoneroDaemon daemon = processModel.getXmrWalletService().getDaemon(); + MoneroWallet havenoWallet = processModel.getXmrWalletService().getWallet(); + + // fetch deposit txs from daemon + List txs = daemon.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()), true); + + // 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()) + 9; + if (havenoWallet.getHeight() >= unlockHeight) { + setConfirmedState(); + return; + } + } + } + + // create block listener + depositTxListener = processModel.getXmrWalletService().new HavenoWalletListener(new MoneroWalletListener() { // TODO (woodser): separate into own class file + + Long unlockHeight = null; + + @Override + public void onNewBlock(long height) { + // ignore if no longer listening if (depositTxListener == null) return; - - // TODO (woodser): remove this - if (output.getTx().isConfirmed() && output.getTx().isLocked() && (processModel.getMaker().getDepositTxHash().equals(output.getTx().getHash()) || processModel.getTaker().getDepositTxHash().equals(output.getTx().getHash()))) { - System.out.println("Deposit output for tx " + output.getTx().getHash() + " is confirmed at height " + output.getTx().getHeight()); - } - - // update locked state - if (output.getTx().getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositLocked = output.getTx().isLocked(); - else if (output.getTx().getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositLocked = output.getTx().isLocked(); - - // deposit txs seen when both locked states seen - if (makerDepositLocked != null && takerDepositLocked != null) { + + // 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); } - - // confirm trade and update ui when both deposits unlock - if (Boolean.FALSE.equals(makerDepositLocked) && Boolean.FALSE.equals(takerDepositLocked)) { - System.out.println("Multisig deposit txs unlocked!"); - applyDepositTxs(multisigWallet.getTx(processModel.getMaker().getDepositTxHash()), multisigWallet.getTx(processModel.getTaker().getDepositTxHash())); - multisigWallet.removeListener(depositTxListener); // remove listener when notified + + // 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()) + 9; + } + + // check if 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 + havenoWallet.removeListener(depositTxListener); // remove listener when notified depositTxListener = null; // prevent re-applying trade state in subsequent requests } } }); // register wallet listener - multisigWallet.addListener(depositTxListener); + havenoWallet.addListener(depositTxListener); } @Nullable - public MoneroTxWallet getTakerDepositTx() { + public MoneroTx getTakerDepositTx() { String depositTxHash = getProcessModel().getTaker().getDepositTxHash(); try { - if (takerDepositTx == null) takerDepositTx = depositTxHash == null ? null : xmrWalletService.getMultisigWallet(getId()).getTx(depositTxHash); + if (takerDepositTx == null) takerDepositTx = depositTxHash == null ? null : getXmrWalletService().getDaemon().getTx(depositTxHash); return takerDepositTx; } catch (MoneroError e) { log.error("Wallet is missing taker deposit tx " + depositTxHash); @@ -770,10 +802,10 @@ public abstract class Trade implements Tradable, Model { } @Nullable - public MoneroTxWallet getMakerDepositTx() { + public MoneroTx getMakerDepositTx() { String depositTxHash = getProcessModel().getMaker().getDepositTxHash(); try { - if (makerDepositTx == null) makerDepositTx = depositTxHash == null ? null : xmrWalletService.getMultisigWallet(getId()).getTx(depositTxHash); + if (makerDepositTx == null) makerDepositTx = depositTxHash == null ? null : getXmrWalletService().getDaemon().getTx(depositTxHash); return makerDepositTx; } catch (MoneroError e) { log.error("Wallet is missing maker deposit tx " + depositTxHash); @@ -1047,10 +1079,10 @@ public abstract class Trade implements Tradable, Model { private long getTradeStartTime() { long now = System.currentTimeMillis(); long startTime; - final MoneroTxWallet takerDepositTx = getTakerDepositTx(); - final MoneroTxWallet makerDepositTx = getMakerDepositTx(); + final MoneroTx takerDepositTx = getTakerDepositTx(); + final MoneroTx makerDepositTx = getMakerDepositTx(); if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) { - if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) { + if (isDepositConfirmed()) { final long tradeTime = getTakeOfferDate().getTime(); long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight()); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 1dc8fe4051..d4bba8a349 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -118,7 +118,7 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import monero.wallet.model.MoneroTxWallet; +import monero.daemon.model.MoneroTx; public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener { @@ -346,7 +346,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void initPersistedTrade(Trade trade) { initTradeAndProtocol(trade, getTradeProtocol(trade)); - trade.updateDepositTxFromWallet(); // TODO (woodser): this re-opens all multisig wallets. only open active wallets requestPersistence(); } @@ -405,7 +404,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } Trade trade; - Optional tradeOptional = getTradeById(offer.getId()); + Optional tradeOptional = getOpenTrade(offer.getId()); if (tradeOptional.isPresent()) { trade = tradeOptional.get(); @@ -446,11 +445,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } initTradeAndProtocol(trade, getTradeProtocol(trade)); - tradableList.add(trade); + synchronized (tradableList) { + tradableList.add(trade); + } } ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { - log.warn("Arbitrator error during trade initialization: " + errorMessage); + log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); removeTrade(trade); }); @@ -514,8 +515,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex()); trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey()); trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages()); - tradableList.add(trade); - + synchronized (tradableList) { + tradableList.add(trade); + } + // notify on phase changes // TODO (woodser): save subscription, bind on startup EasyBind.subscribe(trade.statePhaseProperty(), phase -> { @@ -545,7 +548,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getTradeId()); return; @@ -564,7 +567,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getTradeId()); return; @@ -583,7 +586,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getTradeId()); return; @@ -602,7 +605,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getTradeId()); return; @@ -621,7 +624,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(response.getTradeId()); + Optional tradeOptional = getOpenTrade(response.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + response.getTradeId()); return; @@ -640,7 +643,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getTradeId()); return; @@ -659,7 +662,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - Optional tradeOptional = getTradeById(request.getTradeId()); + Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) throw new RuntimeException("No trade with id " + request.getTradeId()); // TODO (woodser): error handling Trade trade = tradeOptional.get(); getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> { @@ -737,7 +740,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (prev != null) { log.error("We had already an entry with uid {}", trade.getUid()); } - tradableList.add(trade); + synchronized (tradableList) { + tradableList.add(trade); + } initTradeAndProtocol(trade, tradeProtocol); @@ -753,7 +758,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); } }, - errorMessageHandler); + errorMessage -> { + log.warn("Taker error during check offer availability: " + errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + }); requestPersistence(); } @@ -797,8 +805,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades public void onTradeCompleted(Trade trade) { - removeTrade(trade); closedTradableManager.add(trade); + removeTrade(trade); // TODO The address entry should have been removed already. Check and if its the case remove that. xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId()); @@ -811,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) { - Optional tradeOptional = getTradeById(tradeId); + Optional tradeOptional = getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); trade.setDisputeState(disputeState); @@ -897,9 +905,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi .collect(Collectors.toSet())); tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn() .map(trade -> { - MoneroTxWallet makerDepositTx = trade.getMakerDepositTx(); + MoneroTx makerDepositTx = trade.getMakerDepositTx(); if (makerDepositTx != null) { - if (makerDepositTx.isLocked()) { + if (!makerDepositTx.isConfirmed()) { tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx } else { log.warn("We found a closed trade with locked up funds. " + @@ -909,7 +917,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } - MoneroTxWallet takerDepositTx = trade.getTakerDepositTx(); + MoneroTx takerDepositTx = trade.getTakerDepositTx(); if (takerDepositTx != null) { if (!takerDepositTx.isConfirmed()) { tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); @@ -940,9 +948,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi initPersistedTrade(trade); - if (!tradableList.contains(trade)) { - tradableList.add(trade); + synchronized (tradableList) { + if (!tradableList.contains(trade)) { + tradableList.add(trade); + } } + return true; } @@ -967,7 +978,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// public ObservableList getObservableList() { - return tradableList.getObservableList(); + synchronized (tradableList) { + return tradableList.getObservableList(); + } } public BooleanProperty persistedTradesInitializedProperty() { @@ -979,7 +992,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public boolean wasOfferAlreadyUsedInTrade(String offerId) { - return getTradeById(offerId).isPresent() || + return getOpenTrade(offerId).isPresent() || failedTradesManager.getTradeById(offerId).isPresent() || closedTradableManager.getTradableById(offerId).isPresent(); } @@ -991,37 +1004,58 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi else return offer.getDirection() == OfferPayload.Direction.SELL; } + + // TODO (woodser): make Optional versus Trade return types consistent + public Trade getTrade(String tradeId) { + return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> null)); + } - public Optional getTradeById(String tradeId) { + public Optional getOpenTrade(String tradeId) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } - public List getTrades() { + public List getOpenTrades() { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> e) .collect(Collectors.toList())); } + public Optional getClosedTrade(String tradeId) { + return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); + } + private void removeTrade(Trade trade) { - if (tradableList.remove(trade)) { + log.info("TradeManager.removeTrade()"); + synchronized(tradableList) { + if (!tradableList.contains(trade)) return; - // unreserve taker trade key images - if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { - for (String keyImage : trade.getSelf().getReserveTxKeyImages()) { - xmrWalletService.getWallet().thawOutput(keyImage); + synchronized (trade) { + + // unreserve trade key images + if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { + for (String keyImage : trade.getSelf().getReserveTxKeyImages()) { + xmrWalletService.getWallet().thawOutput(keyImage); + } } - } - p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); - xmrWalletService.deleteMultisigWallet(trade.getId()); // TODO (woodser): don't delete multisig wallet until payout tx unlocked? - requestPersistence(); + // delete multisig wallet // TODO (woodser): don't delete multisig wallet until payout tx unlocked + if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId()); + else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId()); + + // unregister and persist + p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); + tradableList.remove(trade); + requestPersistence(); + } } } private void addTrade(Trade trade) { - if (tradableList.add(trade)) { - requestPersistence(); + synchronized(tradableList) { + if (tradableList.add(trade)) { + requestPersistence(); + } } } diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java index aba80589fa..fa774cdf89 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -137,9 +137,9 @@ public class TradeUtils { } /** - * Create a transaction to reserve a trade. The deposit amount is returned - * to the sender's payout address. Additional funds are reserved to allow - * fluctuations in the mining fee. + * Create a transaction to reserve a trade and freeze its funds. The deposit + * amount is returned to the sender's payout address. Additional funds are + * reserved to allow fluctuations in the mining fee. * * @param xmrWalletService * @param offerId @@ -147,8 +147,8 @@ public class TradeUtils { * @param depositAmount * @return a transaction to reserve a trade */ - public static MoneroTxWallet createReserveTx(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) { - + public static MoneroTxWallet reserveTradeFunds(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) { + // get expected mining fee MoneroWallet wallet = xmrWalletService.getWallet(); MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig() @@ -156,13 +156,18 @@ public class TradeUtils { .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) .addDestination(returnAddress, depositAmount)); BigInteger miningFee = miningFeeTx.getFee(); - + // create reserve tx MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig() .setAccountIndex(0) .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) .addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit? + // freeze trade funds + for (MoneroOutput input : reserveTx.getInputs()) { + wallet.freezeOutput(input.getKeyImage().getHex()); + } + return reserveTx; } @@ -222,6 +227,9 @@ public class TradeUtils { if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images"); } + // verify the unlock height + if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); + // verify trade fee String feeAddress = TradeUtils.FEE_ADDRESS; MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); 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 9bfdf68835..5233d531d1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java @@ -8,14 +8,14 @@ import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeAndMultisigRequests; -import bisq.core.trade.protocol.tasks.ProcessDepositRequest; +import bisq.core.trade.protocol.tasks.ArbitratorProcessesDepositRequest; import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest; import bisq.core.trade.protocol.tasks.ArbitratorProcessesReserveTx; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessSignContractRequest; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import lombok.extern.slf4j.Slf4j; @@ -34,94 +34,118 @@ public class ArbitratorProtocol extends DisputeProtocol { public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - this.errorMessageHandler = errorMessageHandler; - processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set - //processModel.setTempTradingPeerNodeAddress(peer); - expect(phase(Trade.Phase.INIT) - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessInitTradeRequest.class, - ArbitratorProcessesReserveTx.class, - ArbitratorSendsInitTradeAndMultisigRequests.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(peer, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + synchronized (trade) { + this.errorMessageHandler = errorMessageHandler; + processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set + CountDownLatch latch = new CountDownLatch(1); + //processModel.setTempTradingPeerNodeAddress(peer); + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessInitTradeRequest.class, + ArbitratorProcessesReserveTx.class, + ArbitratorSendsInitTradeAndMultisigRequests.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(peer, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { System.out.println("ArbitratorProtocol.handleInitMultisigRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessInitMultisigRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ProcessInitMultisigRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(sender, request, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { System.out.println("ArbitratorProtocol.handleSignContractRequest()"); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(sender, message, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } public void handleDepositRequest(DepositRequest request, NodeAddress sender) { System.out.println("ArbitratorProtocol.handleDepositRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessDepositRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ArbitratorProcessesDepositRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + stopTimeout(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(sender, request, errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } /////////////////////////////////////////////////////////////////////////////////////////// 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 528bf8ef7b..ea82e14d7b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -19,6 +19,7 @@ package bisq.core.trade.protocol; import bisq.core.trade.BuyerAsMakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; @@ -47,11 +48,12 @@ import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; +import org.fxmisc.easybind.EasyBind; @Slf4j public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol { @@ -74,142 +76,191 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(message) - .from(peer)) - .setup(tasks( - ProcessInitTradeRequest.class, - //ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here - //VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee - MakerSendsInitTradeRequestIfUnreserved.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(peer, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + synchronized (trade) { + this.errorMessageHandler = errorMessageHandler; + CountDownLatch latch = new CountDownLatch(1); + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ProcessInitTradeRequest.class, + //ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here + //VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee + MakerSendsInitTradeRequestIfUnreserved.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(peer, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessInitMultisigRequest.class, - SendSignContractRequestAfterMultisig.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ProcessInitMultisigRequest.class, + SendSignContractRequestAfterMultisig.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()"); - 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, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()"); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) { + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender); + }); + } + } } @Override public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleDepositResponse()"); - Validator.checkTradeId(processModel.getOfferId(), response); - processModel.setTradeMessage(response); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(response) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessDepositResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, response); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, response, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), response); + processModel.setTradeMessage(response); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(response) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessDepositResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, response); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, response, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(request) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessPaymentAccountPayloadRequest.class, - MakerRemovesOpenOffer.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) { + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) + .with(request) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessPaymentAccountPayloadRequest.class, + MakerRemovesOpenOffer.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + stopTimeout(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender); + }); + } + } } /////////////////////////////////////////////////////////////////////////////////////////// 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 2c516db5d2..e683db45f6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -21,6 +21,7 @@ package bisq.core.trade.protocol; import bisq.core.offer.Offer; import bisq.core.trade.BuyerAsTakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DepositResponse; @@ -32,6 +33,7 @@ import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.TakerProtocol.TakerEvent; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ProcessDepositResponse; import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest; @@ -56,11 +58,12 @@ import bisq.core.trade.protocol.tasks.taker.TakerSendsInitTradeRequestToArbitrat import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; +import org.fxmisc.easybind.EasyBind; import static com.google.common.base.Preconditions.checkNotNull; @@ -83,150 +86,198 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction: Take offer - /////////////////////////////////////////////////////////////////////////////////////////// - - // TODO (woodser): this implementation is duplicated with SellerAsTakerProtocol - @Override - public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println("onTakeOffer()"); - this.tradeResultHandler = tradeResultHandler; - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(TakerEvent.TAKE_OFFER) - .from(trade.getTradingPeerNodeAddress())) - .setup(tasks( - ApplyFilter.class, - TakerReservesTradeFunds.class, - TakerSendsInitTradeRequestToArbitrator.class) // TODO (woodser): app hangs if this pipeline fails. use .using() like below - .using(new TradeTaskRunner(trade, - () -> { }, - errorMessageHandler)) - .withTimeout(30)) - .executeTasks(); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // TakerProtocol + // Take offer /////////////////////////////////////////////////////////////////////////////////////////// // TODO (woodser): these methods are duplicated with SellerAsTakerProtocol due to single inheritance @Override - public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessInitMultisigRequest.class, - SendSignContractRequestAfterMultisig.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + public void onTakeOffer(TradeResultHandler tradeResultHandler, + ErrorMessageHandler errorMessageHandler) { + System.out.println(getClass().getCanonicalName() + ".onTakeOffer()"); + synchronized (trade) { + this.tradeResultHandler = tradeResultHandler; + this.errorMessageHandler = errorMessageHandler; + CountDownLatch latch = new CountDownLatch(1); + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER) + .from(trade.getTradingPeerNodeAddress())) + .setup(tasks( + ApplyFilter.class, + TakerReservesTradeFunds.class, + TakerSendsInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - + + @Override + public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { + System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ProcessInitMultisigRequest.class, + SendSignContractRequestAfterMultisig.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } + } + @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleSignContractRequest()"); - 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, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - + @Override public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleSignContractResponse()"); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) { + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state != State.CONTRACT_SIGNATURE_REQUESTED) return; + handleSignContractResponse(message, sender); + }); + } + } } - + @Override public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleDepositResponse()"); - Validator.checkTradeId(processModel.getOfferId(), response); - processModel.setTradeMessage(response); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(response) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessDepositResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, response); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, response, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), response); + processModel.setTradeMessage(response); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(response) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessDepositResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, response); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, response, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - + @Override public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(request) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessPaymentAccountPayloadRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - tradeResultHandler.handleResult(trade); // trade is initialized - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + CountDownLatch latch = new CountDownLatch(1); + if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) { + processModel.setTradeMessage(request); + expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG + .with(request) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessPaymentAccountPayloadRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + stopTimeout(); + handleTaskRunnerSuccess(sender, request); + tradeResultHandler.handleResult(trade); // trade is initialized + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender); + }); + } + } } /////////////////////////////////////////////////////////////////////////////////////////// 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 c04938d8b1..e0f286efbb 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStar import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; @@ -127,27 +127,31 @@ public abstract class BuyerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - BuyerEvent event = BuyerEvent.PAYMENT_SENT; - expect(phase(Trade.Phase.DEPOSIT_CONFIRMED) - .with(event) - .preCondition(trade.confirmPermitted())) - .setup(tasks(ApplyFilter.class, - getVerifyPeersFeePaymentClass(), - UpdateMultisigWithTradingPeer.class, - BuyerCreateAndSignPayoutTx.class, - BuyerSetupPayoutTxListener.class, - BuyerSendCounterCurrencyTransferStartedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - resultHandler.handleResult(); - handleTaskRunnerSuccess(event); - }, - (errorMessage) -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(event, errorMessage); - }))) - .run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED)) - .executeTasks(); + System.out.println("BuyerProtocol.onPaymentStarted()"); + synchronized (trade) { // TODO (woodser): UpdateMultisigWithTradingPeer sends UpdateMultisigRequest and waits for UpdateMultisigResponse which is new thread, so synchronized (trade) in subsequent pipeline blocks forever if we hold on with countdown latch in this function + System.out.println("BuyerProtocol.onPaymentStarted() has the lock!!!"); + BuyerEvent event = BuyerEvent.PAYMENT_SENT; + expect(phase(Trade.Phase.DEPOSIT_CONFIRMED) + .with(event) + .preCondition(trade.confirmPermitted())) + .setup(tasks(ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + UpdateMultisigWithTradingPeer.class, + BuyerCreateAndSignPayoutTx.class, + BuyerSetupPayoutTxListener.class, + BuyerSendCounterCurrencyTransferStartedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, + (errorMessage) -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED)) + .executeTasks(); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -155,22 +159,29 @@ public abstract class BuyerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { - processModel.setTradeMessage(message); - processModel.setTempTradingPeerNodeAddress(peer); - expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) - .with(message) - .from(peer)) - .setup(tasks( - getVerifyPeersFeePaymentClass(), - BuyerProcessPayoutTxPublishedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - handleTaskRunnerFault(peer, message, errorMessage); - }))) - .executeTasks(); + log.info("BuyerProtocol.handle(PayoutTxPublishedMessage)"); + synchronized (trade) { + processModel.setTradeMessage(message); + processModel.setTempTradingPeerNodeAddress(peer); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + .with(message) + .from(peer)) + .setup(tasks( + getVerifyPeersFeePaymentClass(), + BuyerProcessPayoutTxPublishedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(peer, message, errorMessage); + }))) + .executeTasks(); + wait(latch); + } } 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 aab84ead40..7b04e0f263 100644 --- a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java @@ -91,27 +91,29 @@ public class FluentProtocol { } return this; } + + synchronized (tradeProtocol.trade) { + if (setup.getTimeoutSec() > 0) { + tradeProtocol.startTimeout(setup.getTimeoutSec()); + } - 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; } - - 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; } diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index ba9ab58a89..5b6dae8504 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -181,14 +181,11 @@ public class ProcessModel implements Model, PersistablePayload { @Nullable @Getter @Setter - private boolean multisigSetupComplete; + private String multisigAddress; @Nullable @Getter @Setter - private boolean makerReadyToFundMultisig; // TODO (woodser): remove - @Getter - @Setter - private boolean multisigDepositInitiated; + private boolean multisigSetupComplete; // TODO (woodser): redundant with multisigAddress existing, remove @Nullable transient private MoneroTxWallet buyerSignedPayoutTx; // TODO (woodser): remove @@ -251,9 +248,8 @@ public class ProcessModel implements Model, PersistablePayload { Optional.ofNullable(backupArbitrator).ifPresent(e -> builder.setBackupArbitrator(backupArbitrator.toProtoMessage())); Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex)); Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); + Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress)); Optional.ofNullable(multisigSetupComplete).ifPresent(e -> builder.setMultisigSetupComplete(multisigSetupComplete)); - Optional.ofNullable(makerReadyToFundMultisig).ifPresent(e -> builder.setMakerReadyToFundMultisig(makerReadyToFundMultisig)); - Optional.ofNullable(multisigDepositInitiated).ifPresent(e -> builder.setMultisigSetupComplete(multisigDepositInitiated)); return builder.build(); } @@ -284,9 +280,8 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null); processModel.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex())); processModel.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex())); + processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); processModel.setMultisigSetupComplete(proto.getMultisigSetupComplete()); - processModel.setMakerReadyToFundMultisig(proto.getMakerReadyToFundMultisig()); - processModel.setMultisigDepositInitiated(proto.getMultisigDepositInitiated()); String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState()); MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString); 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 20d361ff16..d2099c7bd7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -20,6 +20,7 @@ package bisq.core.trade.protocol; import bisq.core.trade.SellerAsMakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositTxMessage; @@ -47,11 +48,12 @@ import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepo import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; +import org.fxmisc.easybind.EasyBind; @Slf4j public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol { @@ -74,142 +76,191 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(message) - .from(peer)) - .setup(tasks( - ProcessInitTradeRequest.class, - //ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here - //VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee - MakerSendsInitTradeRequestIfUnreserved.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(peer, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + synchronized (trade) { + this.errorMessageHandler = errorMessageHandler; + CountDownLatch latch = new CountDownLatch(1); + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ProcessInitTradeRequest.class, + //ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here + //VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee + MakerSendsInitTradeRequestIfUnreserved.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(peer, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - + @Override public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessInitMultisigRequest.class, - SendSignContractRequestAfterMultisig.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ProcessInitMultisigRequest.class, + SendSignContractRequestAfterMultisig.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - + @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()"); - 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, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()"); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) { + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender); + }); + } + } } @Override public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handleDepositResponse()"); - Validator.checkTradeId(processModel.getOfferId(), response); - processModel.setTradeMessage(response); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(response) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessDepositResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, response); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, response, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), response); + processModel.setTradeMessage(response); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(response) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessDepositResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, response); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, response, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) { - System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(request) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessPaymentAccountPayloadRequest.class, - MakerRemovesOpenOffer.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) { + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) + .with(request) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessPaymentAccountPayloadRequest.class, + MakerRemovesOpenOffer.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + stopTimeout(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender); + }); + } + } } /////////////////////////////////////////////////////////////////////////////////////////// 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 411da668fc..3702630f5b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -21,6 +21,7 @@ package bisq.core.trade.protocol; import bisq.core.offer.Offer; import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; import bisq.core.trade.messages.DepositResponse; @@ -51,11 +52,12 @@ import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; +import org.fxmisc.easybind.EasyBind; import static com.google.common.base.Preconditions.checkNotNull; @@ -76,150 +78,198 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction: Take offer + // Take offer /////////////////////////////////////////////////////////////////////////////////////////// + + // TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println("onTakeOffer()"); - this.tradeResultHandler = tradeResultHandler; - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(TakerEvent.TAKE_OFFER) - .from(trade.getTradingPeerNodeAddress())) - .setup(tasks( - ApplyFilter.class, - TakerReservesTradeFunds.class, - TakerSendsInitTradeRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { }, - errorMessageHandler)) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".onTakeOffer()"); + synchronized (trade) { + this.tradeResultHandler = tradeResultHandler; + this.errorMessageHandler = errorMessageHandler; + CountDownLatch latch = new CountDownLatch(1); + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER) + .from(trade.getTradingPeerNodeAddress())) + .setup(tasks( + ApplyFilter.class, + TakerReservesTradeFunds.class, + TakerSendsInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // TakerProtocol - /////////////////////////////////////////////////////////////////////////////////////////// - - // TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance @Override public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT) - .with(request) - .from(sender)) - .setup(tasks( - ProcessInitMultisigRequest.class, - SendSignContractRequestAfterMultisig.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - handleTaskRunnerFault(sender, request, errorMessage); - errorMessageHandler.handleErrorMessage(errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + processModel.setTradeMessage(request); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(request) + .from(sender)) + .setup(tasks( + ProcessInitMultisigRequest.class, + SendSignContractRequestAfterMultisig.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, request); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleSignContractRequest()"); - 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, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.INIT) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleSignContractResponse()"); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.INIT) - .with(message) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessSignContractResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, message); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, message, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) { + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(message) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessSignContractResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, message); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, message, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state != State.CONTRACT_SIGNATURE_REQUESTED) return; + handleSignContractResponse(message, sender); + }); + } + } } @Override public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handleDepositResponse()"); - Validator.checkTradeId(processModel.getOfferId(), response); - processModel.setTradeMessage(response); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) - .with(response) - .from(sender)) - .setup(tasks( - // TODO (woodser): validate request - ProcessDepositResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, response); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, response, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), response); + processModel.setTradeMessage(response); + CountDownLatch latch = new CountDownLatch(1); + expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED) + .with(response) + .from(sender)) + .setup(tasks( + // TODO (woodser): validate request + ProcessDepositResponse.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + handleTaskRunnerSuccess(sender, response); + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, response, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } } @Override public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) { - System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()"); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) // TODO: only deposit_published should be expected - .with(request) - .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() - .setup(tasks( - // TODO (woodser): validate request - ProcessPaymentAccountPayloadRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - tradeResultHandler.handleResult(trade); // trade is initialized - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(30)) - .executeTasks(); + System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()"); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), request); + CountDownLatch latch = new CountDownLatch(1); + if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) { + processModel.setTradeMessage(request); + expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG + .with(request) + .from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress() + .setup(tasks( + // TODO (woodser): validate request + ProcessPaymentAccountPayloadRequest.class) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + stopTimeout(); + handleTaskRunnerSuccess(sender, request); + tradeResultHandler.handleResult(trade); // trade is initialized + }, + errorMessage -> { + latch.countDown(); + handleTaskRunnerFault(sender, request, errorMessage); + errorMessageHandler.handleErrorMessage(errorMessage); + })) + .withTimeout(TRADE_TIMEOUT)) + .executeTasks(); + wait(latch); + } else { + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender); + }); + } + } } 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 a5e634c8b8..22482ebe68 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -30,7 +30,7 @@ import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx; import bisq.network.p2p.NodeAddress; - +import java.util.concurrent.CountDownLatch; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; @@ -77,27 +77,39 @@ public abstract class SellerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + log.info("SellerProtocol.handle(CounterCurrencyTransferStartedMessage)"); // We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case // that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received // a mailbox message with CounterCurrencyTransferStartedMessage. // TODO A better fix would be to add a listener for the wallet sync state and process // the mailbox msg once wallet is ready and trade state set. - expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED) - .with(message) - .from(peer) - .preCondition(trade.getPayoutTx() == null, - () -> { - log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " + - "so we ignore the message. This can happen if the ACK message to the peer did not " + - "arrive and the peer repeats sending us the message. We send another ACK msg."); - sendAckMessage(peer, message, true, null); - removeMailboxMessageAfterProcessing(message); - })) - .setup(tasks( - SellerProcessCounterCurrencyTransferStartedMessage.class, - ApplyFilter.class, - getVerifyPeersFeePaymentClass())) - .executeTasks(); + synchronized (trade) { + CountDownLatch latch = new CountDownLatch(1); + expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED) + .with(message) + .from(peer) + .preCondition(trade.getPayoutTx() == null, + () -> { + log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " + + "so we ignore the message. This can happen if the ACK message to the peer did not " + + "arrive and the peer repeats sending us the message. We send another ACK msg."); + sendAckMessage(peer, message, true, null); + removeMailboxMessageAfterProcessing(message); + })) + .setup(tasks( + SellerProcessCounterCurrencyTransferStartedMessage.class, + ApplyFilter.class, + getVerifyPeersFeePaymentClass()) + .using(new TradeTaskRunner(trade, + () -> { + latch.countDown(); + }, + (errorMessage) -> { + latch.countDown(); + }))) + .executeTasks(); + wait(latch); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -105,26 +117,33 @@ public abstract class SellerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - SellerEvent event = SellerEvent.PAYMENT_RECEIVED; - expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) - .with(event) - .preCondition(trade.confirmPermitted())) - .setup(tasks( - ApplyFilter.class, - getVerifyPeersFeePaymentClass(), - SellerSignAndPublishPayoutTx.class, - // SellerSignAndFinalizePayoutTx.class, - // SellerBroadcastPayoutTx.class, - SellerSendPayoutTxPublishedMessage.class) - .using(new TradeTaskRunner(trade, () -> { - resultHandler.handleResult(); - handleTaskRunnerSuccess(event); - }, (errorMessage) -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(event, errorMessage); - }))) - .run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT)) - .executeTasks(); + log.info("SellerProtocol.onPaymentReceived()"); + synchronized (trade) { + CountDownLatch latch = new CountDownLatch(1); + SellerEvent event = SellerEvent.PAYMENT_RECEIVED; + expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + .with(event) + .preCondition(trade.confirmPermitted())) + .setup(tasks( + ApplyFilter.class, + getVerifyPeersFeePaymentClass(), + SellerSignAndPublishPayoutTx.class, + // SellerSignAndFinalizePayoutTx.class, + // SellerBroadcastPayoutTx.class, + SellerSendPayoutTxPublishedMessage.class) + .using(new TradeTaskRunner(trade, () -> { + latch.countDown(); + resultHandler.handleResult(); + handleTaskRunnerSuccess(event); + }, (errorMessage) -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(event, errorMessage); + }))) + .run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT)) + .executeTasks(); + wait(latch); + } } 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 dea4a5b01f..97c1c7cedf 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -49,6 +49,7 @@ import bisq.common.taskrunner.Task; import java.util.Collection; import java.util.Collections; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @@ -58,6 +59,8 @@ import javax.annotation.Nullable; @Slf4j public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener { + public static final int TRADE_TIMEOUT = 60; + protected final ProcessModel processModel; protected final Trade trade; private Timer timeoutTimer; @@ -213,23 +216,28 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // TODO (woodser): update to use fluent for consistency public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - - TradeTaskRunner taskRunner = new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest"); - }, - errorMessage -> { - errorMessageHandler.handleErrorMessage(errorMessage); - handleTaskRunnerFault(peer, message, errorMessage); - }); - taskRunner.addTasks( - ProcessUpdateMultisigRequest.class - ); - startTimeout(60); // TODO (woodser): what timeout to use? don't hardcode - taskRunner.run(); + synchronized (trade) { + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + CountDownLatch latch = new CountDownLatch(1); + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> { + stopTimeout(); + latch.countDown(); + handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest"); + }, + errorMessage -> { + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(peer, message, errorMessage); + }); + taskRunner.addTasks( + ProcessUpdateMultisigRequest.class + ); + startTimeout(TRADE_TIMEOUT); + taskRunner.run(); + wait(latch); + } } @@ -265,6 +273,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected FluentProtocol.Condition anyPhase(Trade.Phase... expectedPhases) { return new FluentProtocol.Condition(trade).anyPhase(expectedPhases); } + + protected FluentProtocol.Condition state(Trade.State expectedState) { + return new FluentProtocol.Condition(trade).state(expectedState); + } + + protected FluentProtocol.Condition anyState(Trade.State... expectedStates) { + return new FluentProtocol.Condition(trade).anyState(expectedStates); + } @SafeVarargs public final FluentProtocol.Setup tasks(Class>... tasks) { @@ -291,8 +307,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.info("Received AckMessage for {} from {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getSourceUid()); } else { - log.warn("Received AckMessage with error state for {} from {} with tradeId {} and errorMessage={}", - ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getErrorMessage()); + String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + + " from "+ peer + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage(); + log.warn(err); + stopTimeout(); + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(err); } } @@ -348,6 +367,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D /////////////////////////////////////////////////////////////////////////////////////////// // Timeout /////////////////////////////////////////////////////////////////////////////////////////// + + protected void wait(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } protected void startTimeout(long timeoutSec) { stopTimeout(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java similarity index 74% rename from core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java index 534b9ca205..fb7b698f6c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java @@ -40,10 +40,10 @@ import monero.daemon.MoneroDaemon; import monero.wallet.MoneroWallet; @Slf4j -public class ProcessDepositRequest extends TradeTask { +public class ArbitratorProcessesDepositRequest extends TradeTask { @SuppressWarnings({"unused"}) - public ProcessDepositRequest(TaskRunner taskHandler, Trade trade) { + public ArbitratorProcessesDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -78,8 +78,7 @@ public class ProcessDepositRequest extends TradeTask { boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress()); boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferPayload.Direction.SELL : offer.getDirection() == OfferPayload.Direction.BUY; BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); - MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId()); // TODO (woodser): only get, do not create - String depositAddress = multisigWallet.getPrimaryAddress(); + String depositAddress = processModel.getMultisigAddress(); BigInteger tradeFee; TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress()); if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee()); @@ -103,34 +102,30 @@ public class ProcessDepositRequest extends TradeTask { null, false); - // sychronize to send only one response - synchronized(processModel) { + // set deposit info + trader.setDepositTxHex(request.getDepositTxHex()); + trader.setDepositTxKey(request.getDepositTxKey()); + + // 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) { - // set deposit info - trader.setDepositTxHex(request.getDepositTxHex()); - trader.setDepositTxKey(request.getDepositTxKey()); + // relay txs + daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted + daemon.submitTxHex(processModel.getTaker().getDepositTxHex()); - // 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) { - - // relay txs - daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); - daemon.submitTxHex(processModel.getTaker().getDepositTxHex()); - - // create deposit response - DepositResponse response = new DepositResponse( - trade.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime()); - - // send deposit response to maker and taker - sendDepositResponse(trade.getMakerNodeAddress(), trade.getMakerPubKeyRing(), response); - sendDepositResponse(trade.getTakerNodeAddress(), trade.getTakerPubKeyRing(), response); - } + // create deposit response + DepositResponse response = new DepositResponse( + trade.getOffer().getId(), + processModel.getMyNodeAddress(), + processModel.getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + new Date().getTime()); + + // send deposit response to maker and taker + sendDepositResponse(trade.getMakerNodeAddress(), trade.getMakerPubKeyRing(), response); + sendDepositResponse(trade.getTakerNodeAddress(), trade.getTakerPubKeyRing(), response); } // TODO (woodser): request persistence? diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java index fa0120d735..1884dbfba1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendsInitTradeAndMultisigRequests.java @@ -44,9 +44,6 @@ import monero.wallet.MoneroWallet; @Slf4j public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { - private boolean takerAck; - private boolean makerAck; - @SuppressWarnings({"unused"}) public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -97,14 +94,15 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { null, null); - // listen for maker to ack InitTradeRequest + // send init multisig requests on ack // TODO (woodser): only send InitMultisigRequests if arbitrator has maker reserve tx, else wait for that TradeListener listener = new TradeListener() { @Override public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - if (sender.equals(trade.getMakerNodeAddress()) && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) { + if (sender.equals(trade.getMakerNodeAddress()) && + ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName()) && + ackMessage.getSourceUid().equals(makerRequest.getUid())) { trade.removeListener(this); if (ackMessage.isSuccess()) sendInitMultisigRequests(); - else failed("Received unsuccessful ack for InitTradeRequest from maker"); // TODO (woodser): maker should not do this, penalize them by broadcasting reserve tx? } } }; @@ -120,6 +118,7 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { @Override public void onArrived() { log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid()); + complete(); } @Override public void onFault(String errorMessage) { @@ -172,8 +171,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { @Override public void onArrived() { log.info("{} arrived at arbitrator: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid()); - makerAck = true; - checkComplete(); } @Override public void onFault(String errorMessage) { @@ -194,8 +191,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { @Override public void onArrived() { log.info("{} arrived at peer: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid()); - takerAck = true; - checkComplete(); } @Override public void onFault(String errorMessage) { @@ -206,8 +201,4 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask { } ); } - - private void checkComplete() { - if (makerAck && takerAck) complete(); - } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositResponse.java index f2b7ce7170..586d378de9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositResponse.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositResponse.java @@ -60,6 +60,7 @@ public class ProcessDepositResponse extends TradeTask { processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() { @Override public void onArrived() { + complete(); log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId()); } @Override @@ -69,8 +70,6 @@ public class ProcessDepositResponse extends TradeTask { failed(); } }); - - complete(); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 88fb3ff67e..5ef01cf5bd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -52,8 +52,6 @@ public class ProcessInitMultisigRequest extends TradeTask { private boolean ack1 = false; private boolean ack2 = false; - private boolean failed = false; - private static Object lock = new Object(); MoneroWallet multisigWallet; @SuppressWarnings({"unused"}) @@ -71,120 +69,119 @@ public class ProcessInitMultisigRequest extends TradeTask { checkTradeId(processModel.getOfferId(), request); XmrWalletService xmrWalletService = processModel.getProvider().getXmrWalletService(); - System.out.println("PROCESS MULTISIG MESSAGE"); - System.out.println(request); -// System.out.println("PROCESS MULTISIG MESSAGE TRADE"); -// System.out.println(trade); - // TODO (woodser): verify request including sender's signature in previous pipeline task // TODO (woodser): run in separate thread to not block UI thread? // TODO (woodser): validate message has expected sender in previous step - // synchronize access to wallet - synchronized (lock) { + // get peer multisig participant + TradingPeer multisigParticipant; + if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) multisigParticipant = processModel.getMaker(); + else if (request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) multisigParticipant = processModel.getTaker(); + else if (request.getSenderNodeAddress().equals(trade.getArbitratorNodeAddress())) multisigParticipant = processModel.getArbitrator(); + else throw new RuntimeException("Invalid sender to process init trade message: " + trade.getClass().getName()); - // get peer multisig participant - TradingPeer multisigParticipant; - if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) multisigParticipant = processModel.getMaker(); - else if (request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) multisigParticipant = processModel.getTaker(); - else if (request.getSenderNodeAddress().equals(trade.getArbitratorNodeAddress())) multisigParticipant = processModel.getArbitrator(); - else throw new RuntimeException("Invalid sender to process init trade message: " + trade.getClass().getName()); + // reconcile peer's established multisig hex with message + if (multisigParticipant.getPreparedMultisigHex() == null) multisigParticipant.setPreparedMultisigHex(request.getPreparedMultisigHex()); + else if (!multisigParticipant.getPreparedMultisigHex().equals(request.getPreparedMultisigHex())) throw new RuntimeException("Message's prepared multisig differs from previous messages, previous: " + multisigParticipant.getPreparedMultisigHex() + ", message: " + request.getPreparedMultisigHex()); + if (multisigParticipant.getMadeMultisigHex() == null) multisigParticipant.setMadeMultisigHex(request.getMadeMultisigHex()); + else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages: " + request.getMadeMultisigHex() + " versus " + multisigParticipant.getMadeMultisigHex()); - // reconcile peer's established multisig hex with message - if (multisigParticipant.getPreparedMultisigHex() == null) multisigParticipant.setPreparedMultisigHex(request.getPreparedMultisigHex()); - else if (!multisigParticipant.getPreparedMultisigHex().equals(request.getPreparedMultisigHex())) throw new RuntimeException("Message's prepared multisig differs from previous messages, previous: " + multisigParticipant.getPreparedMultisigHex() + ", message: " + request.getPreparedMultisigHex()); - if (multisigParticipant.getMadeMultisigHex() == null) multisigParticipant.setMadeMultisigHex(request.getMadeMultisigHex()); - else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages"); - - // prepare multisig if applicable - boolean updateParticipants = false; - if (processModel.getPreparedMultisigHex() == null) { - System.out.println("Preparing multisig wallet!"); - multisigWallet = xmrWalletService.createMultisigWallet(trade.getId()); - processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig()); - updateParticipants = true; - } else { - multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); - } - - // make multisig if applicable - TradingPeer[] peers = getMultisigPeers(); - if (processModel.getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) { - System.out.println("Making multisig wallet!"); - MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)? - processModel.setMadeMultisigHex(result.getMultisigHex()); - updateParticipants = true; - } - - // exchange multisig keys if applicable - if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) { - System.out.println("Exchanging multisig wallet!"); - multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword()); - processModel.setMultisigSetupComplete(true); - } - - // update multisig participants if new state to communicate - if (updateParticipants) { - - // get destination addresses and pub key rings // TODO: better way, use getMultisigPeers() - NodeAddress peer1Address; - PubKeyRing peer1PubKeyRing; - NodeAddress peer2Address; - PubKeyRing peer2PubKeyRing; - if (trade instanceof ArbitratorTrade) { - peer1Address = trade.getTakerNodeAddress(); - peer1PubKeyRing = trade.getTakerPubKeyRing(); - peer2Address = trade.getMakerNodeAddress(); - peer2PubKeyRing = trade.getMakerPubKeyRing(); - } else if (trade instanceof MakerTrade) { - peer1Address = trade.getTakerNodeAddress(); - peer1PubKeyRing = trade.getTakerPubKeyRing(); - peer2Address = trade.getArbitratorNodeAddress(); - peer2PubKeyRing = trade.getArbitratorPubKeyRing(); - } else { - peer1Address = trade.getMakerNodeAddress(); - peer1PubKeyRing = trade.getMakerPubKeyRing(); - peer2Address = trade.getArbitratorNodeAddress(); - peer2PubKeyRing = trade.getArbitratorPubKeyRing(); - } - - if (peer1Address == null) throw new RuntimeException("Peer1 address is null"); - if (peer1PubKeyRing == null) throw new RuntimeException("Peer1 pub key ring is null"); - if (peer2Address == null) throw new RuntimeException("Peer2 address is null"); - if (peer2PubKeyRing == null) throw new RuntimeException("Peer2 pub key ring null"); - - // complete on successful ack messages - TradeListener ackListener = new TradeListener() { - @Override - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - if (!ackMessage.getSourceMsgClassName().equals(InitMultisigRequest.class.getSimpleName())) return; - if (ackMessage.isSuccess()) { - if (sender.equals(peer1Address)) ack1 = true; - if (sender.equals(peer2Address)) ack2 = true; - if (ack1 && ack2) { - trade.removeListener(this); - completeAux(); - } - } else { - if (!failed) { - failed = true; - failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task? - } - } - } - }; - trade.addListener(ackListener); - - // send to peers - sendInitMultisigRequest(peer1Address, peer1PubKeyRing); - sendInitMultisigRequest(peer2Address, peer2PubKeyRing); - } else { - completeAux(); - } + // prepare multisig if applicable + boolean updateParticipants = false; + if (processModel.getPreparedMultisigHex() == null) { + log.info("Preparing multisig wallet for trade {}", trade.getId()); + multisigWallet = xmrWalletService.createMultisigWallet(trade.getId()); + processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig()); + updateParticipants = true; + } else if (!processModel.isMultisigSetupComplete()) { + multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); } - } catch (Throwable t) { - failed(t); - } + + // make multisig if applicable + TradingPeer[] peers = getMultisigPeers(); + if (processModel.getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) { + log.info("Making multisig wallet for trade {}", trade.getId()); + MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)? + processModel.setMadeMultisigHex(result.getMultisigHex()); + updateParticipants = true; + } + + // exchange multisig keys if applicable + if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) { + log.info("Exchanging multisig wallet keys for trade {}", trade.getId()); + multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword()); + processModel.setMultisigSetupComplete(true); // TODO: (woodser): remove this field? + processModel.setMultisigAddress(multisigWallet.getPrimaryAddress()); + processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // save and close multisig wallet once it's created + } + + // update multisig participants if new state to communicate + if (updateParticipants) { + + // get destination addresses and pub key rings // TODO: better way, use getMultisigPeers() + NodeAddress peer1Address; + PubKeyRing peer1PubKeyRing; + NodeAddress peer2Address; + PubKeyRing peer2PubKeyRing; + if (trade instanceof ArbitratorTrade) { + peer1Address = trade.getTakerNodeAddress(); + peer1PubKeyRing = trade.getTakerPubKeyRing(); + peer2Address = trade.getMakerNodeAddress(); + peer2PubKeyRing = trade.getMakerPubKeyRing(); + } else if (trade instanceof MakerTrade) { + peer1Address = trade.getTakerNodeAddress(); + peer1PubKeyRing = trade.getTakerPubKeyRing(); + peer2Address = trade.getArbitratorNodeAddress(); + peer2PubKeyRing = trade.getArbitratorPubKeyRing(); + } else { + peer1Address = trade.getMakerNodeAddress(); + peer1PubKeyRing = trade.getMakerPubKeyRing(); + peer2Address = trade.getArbitratorNodeAddress(); + peer2PubKeyRing = trade.getArbitratorPubKeyRing(); + } + + if (peer1Address == null) throw new RuntimeException("Peer1 address is null"); + if (peer1PubKeyRing == null) throw new RuntimeException("Peer1 pub key ring is null"); + if (peer2Address == null) throw new RuntimeException("Peer2 address is null"); + if (peer2PubKeyRing == null) throw new RuntimeException("Peer2 pub key ring null"); + + // send to peer 1 + sendInitMultisigRequest(peer1Address, peer1PubKeyRing, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer1Address, request.getTradeId(), request.getUid()); + ack1 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer1Address, errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + + // send to peer 2 + sendInitMultisigRequest(peer2Address, peer2PubKeyRing, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer2Address, request.getTradeId(), request.getUid()); + ack2 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer2Address, errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + } else { + completeAux(); + } + } catch (Throwable t) { + failed(t); + } } private TradingPeer[] getMultisigPeers() { @@ -202,9 +199,9 @@ public class ProcessInitMultisigRequest extends TradeTask { return peers; } - private void sendInitMultisigRequest(NodeAddress recipient, PubKeyRing pubKeyRing) { + private void sendInitMultisigRequest(NodeAddress recipient, PubKeyRing pubKeyRing, SendDirectMessageListener listener) { - // create request with current multisig hex + // create multisig message with current multisig hex InitMultisigRequest request = new InitMultisigRequest( processModel.getOffer().getId(), processModel.getMyNodeAddress(), @@ -214,24 +211,12 @@ public class ProcessInitMultisigRequest extends TradeTask { new Date().getTime(), processModel.getPreparedMultisigHex(), processModel.getMadeMultisigHex()); - + log.info("Send {} with offerId {} and uid {} to peer {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), recipient); - processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), recipient, request.getTradeId(), request.getUid()); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), recipient, errorMessage); - appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); - failed(); - } - }); + processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, listener); } private void completeAux() { - multisigWallet.save(); complete(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentAccountPayloadRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentAccountPayloadRequest.java index a275be9a63..ec69d9516e 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentAccountPayloadRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentAccountPayloadRequest.java @@ -60,13 +60,7 @@ public class ProcessPaymentAccountPayloadRequest extends TradeTask { // set payment account payload trade.getTradingPeer().setPaymentAccountPayload(paymentAccountPayload); - - // subscribe to trade state to notify ui when deposit txs seen in network - tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { - if (trade.isDepositPublished()) applyPublishedDepositTxs(); - }); - if (trade.isDepositPublished()) applyPublishedDepositTxs(); // deposit txs might be seen before subcription - + // persist and complete processModel.getTradeManager().requestPersistence(); complete(); @@ -74,14 +68,6 @@ public class ProcessPaymentAccountPayloadRequest extends TradeTask { failed(t); } } - - private void applyPublishedDepositTxs() { - MoneroWallet multisigWallet = processModel.getXmrWalletService().getMultisigWallet(trade.getId()); - MoneroTxWallet makerDepositTx = checkNotNull(multisigWallet.getTx(processModel.getMaker().getDepositTxHash())); - MoneroTxWallet takerDepositTx = checkNotNull(multisigWallet.getTx(processModel.getTaker().getDepositTxHash())); - trade.applyDepositTxs(makerDepositTx, takerDepositTx); - UserThread.execute(this::unSubscribe); // remove trade state subscription at callback - } private void unSubscribe() { if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe(); 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 e04e268dac..6658a43cad 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 @@ -26,12 +26,11 @@ import bisq.common.util.Utilities; import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.Contract; import bisq.core.trade.Trade; +import bisq.core.trade.Trade.State; import bisq.core.trade.TradeUtils; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; -import bisq.core.trade.protocol.TradeListener; import bisq.core.trade.protocol.TradingPeer; -import bisq.network.p2p.AckMessage; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendDirectMessageListener; import java.util.Date; @@ -43,7 +42,6 @@ public class ProcessSignContractRequest extends TradeTask { private boolean ack1 = false; private boolean ack2 = false; - private boolean failed = false; @SuppressWarnings({"unused"}) public ProcessSignContractRequest(TaskRunner taskHandler, Trade trade) { @@ -71,77 +69,71 @@ public class ProcessSignContractRequest extends TradeTask { complete(); return; } - + // create and sign contract Contract contract = TradeUtils.createContract(trade); String contractAsJson = Utilities.objectToJson(contract); String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); - + // save contract and signature trade.setContract(contract); trade.setContractAsJson(contractAsJson); trade.getSelf().setContractSignature(signature); - + + // create response with contract signature + SignContractResponse response = new SignContractResponse( + trade.getOffer().getId(), + processModel.getMyNodeAddress(), + processModel.getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + new Date().getTime(), + signature); + // get response recipients. only arbitrator sends response to both peers NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMakerNodeAddress() : trade.getTradingPeerNodeAddress(); PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMakerPubKeyRing() : trade.getTradingPeerPubKeyRing(); NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTakerNodeAddress() : null; PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTakerPubKeyRing() : null; - - // complete on successful ack messages - TradeListener ackListener = new TradeListener() { + + // send response to recipient 1 + processModel.getP2PService().sendEncryptedDirectMessage(recipient1, recipient1PubKey, response, new SendDirectMessageListener() { @Override - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - if (!ackMessage.getSourceMsgClassName().equals(SignContractResponse.class.getSimpleName())) return; - if (ackMessage.isSuccess()) { - if (sender.equals(trade.getTradingPeerNodeAddress())) ack1 = true; - if (sender.equals(trade.getArbitratorNodeAddress())) ack2 = true; - if (trade instanceof ArbitratorTrade ? ack1 && ack2 : ack1) { // only arbitrator sends response to both peers - trade.removeListener(this); - complete(); - } - } else { - if (!failed) { - failed = true; - failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task? - } - } + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient1, trade.getId()); + ack1 = true; + if (ack1 && (recipient2 == null || ack2)) complete(); } - }; - trade.addListener(ackListener); - - // send contract signature response(s) - if (recipient1 != null) sendSignContractResponse(recipient1, recipient1PubKey, signature); - if (recipient2 != null) sendSignContractResponse(recipient2, recipient2PubKey, signature); + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient1, trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + + // send response to recipient 2 if applicable + if (recipient2 != null) { + processModel.getP2PService().sendEncryptedDirectMessage(recipient2, recipient2PubKey, response, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient2, trade.getId()); + ack2 = true; + if (ack1 && ack2) complete(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient2, trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + } + + // update trade state + trade.setState(State.CONTRACT_SIGNATURE_REQUESTED); } catch (Throwable t) { failed(t); } } - - private void sendSignContractResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, String contractSignature) { - - // create response with contract signature - SignContractResponse response = new SignContractResponse( - trade.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime(), - contractSignature); - - // send request - processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), nodeAddress, trade.getId()); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), nodeAddress, trade.getId(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java index 6b58e4a5f3..e5a1d0b0de 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java @@ -27,7 +27,6 @@ import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.protocol.TradingPeer; import bisq.network.p2p.SendDirectMessageListener; -import common.utils.GenUtils; import java.util.Date; import java.util.UUID; import lombok.extern.slf4j.Slf4j; @@ -44,18 +43,12 @@ public class ProcessSignContractResponse extends TradeTask { protected void run() { try { runInterceptHook(); - - // wait until contract is available from peer's sign contract request - // TODO (woodser): this will loop if peer disappears; use proper notification - while (trade.getContract() == null) { - GenUtils.waitFor(250); - } - + // get contract and signature String contractAsJson = trade.getContractAsJson(); SignContractResponse response = (SignContractResponse) processModel.getTradeMessage(); // TODO (woodser): verify response String signature = response.getContractSignature(); - + // get peer info // TODO (woodser): make these utilities / refactor model PubKeyRing peerPubKeyRing; @@ -64,20 +57,20 @@ public class ProcessSignContractResponse extends TradeTask { else if (peer == processModel.getMaker()) peerPubKeyRing = trade.getMakerPubKeyRing(); else if (peer == processModel.getTaker()) peerPubKeyRing = trade.getTakerPubKeyRing(); else throw new RuntimeException(response.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); - + // verify signature // TODO (woodser): transfer contract for convenient comparison? if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid"); - + // set peer's signature peer.setContractSignature(signature); - + // send deposit request when all contract signatures received if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { - + // start listening for deposit txs - trade.setupDepositTxsListener(); - + trade.listenForDepositTxs(); + // create request for arbitrator to deposit funds to multisig DepositRequest request = new DepositRequest( trade.getOffer().getId(), @@ -89,12 +82,14 @@ public class ProcessSignContractResponse extends TradeTask { trade.getSelf().getContractSignature(), processModel.getDepositTxXmr().getFullHex(), processModel.getDepositTxXmr().getKey()); - + // send request to arbitrator processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId()); + processModel.getTradeManager().requestPersistence(); + complete(); } @Override public void onFault(String errorMessage) { @@ -103,11 +98,9 @@ public class ProcessSignContractResponse extends TradeTask { failed(); } }); + } else { + complete(); // does not yet have needed signatures } - - // persist and complete - processModel.getTradeManager().requestPersistence(); - complete(); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java index 124028d947..9961198ef2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java @@ -74,6 +74,9 @@ public class ProcessUpdateMultisigRequest extends TradeTask { // import the multisig hex int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex())); System.out.println("Num outputs signed by imported multisig hex: " + numOutputsSigned); + + // close multisig wallet + processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // respond with updated multisig hex UpdateMultisigResponse response = new UpdateMultisigResponse( @@ -90,9 +93,6 @@ public class ProcessUpdateMultisigRequest extends TradeTask { @Override public void onArrived() { log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid()); - - // save multisig wallet - multisigWallet.save(); // TODO (woodser): save on each step or after multisig wallets created? complete(); } @Override 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 37a81cc616..b9628a2932 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 @@ -44,9 +44,8 @@ import monero.wallet.model.MoneroTxWallet; @Slf4j public class SendSignContractRequestAfterMultisig extends TradeTask { - private boolean ack1 = false; + private boolean ack1 = false; // TODO (woodser) these represent onArrived(), not the ack private boolean ack2 = false; - private boolean failed = false; @SuppressWarnings({"unused"}) public SendSignContractRequestAfterMultisig(TaskRunner taskHandler, Trade trade) { @@ -57,97 +56,94 @@ public class SendSignContractRequestAfterMultisig extends TradeTask { protected void run() { try { runInterceptHook(); - - // skip if multisig wallet not complete - if (!processModel.isMultisigSetupComplete()) return; // TODO: woodser: this does not ack original request? - - // skip if deposit tx already created - if (processModel.getDepositTxXmr() != null) return; - // thaw reserved outputs - MoneroWallet wallet = trade.getXmrWalletService().getWallet(); - for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) { - wallet.thawOutput(reserveTxKeyImage); - } - - // create deposit tx - BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee()); - Offer offer = processModel.getOffer(); - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit()); - MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId()); - String multisigAddress = multisigWallet.getPrimaryAddress(); - MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount); - - // freeze deposit outputs - // TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig - for (MoneroOutput input : depositTx.getInputs()) { - wallet.freezeOutput(input.getKeyImage().getHex()); - } - - // save process state - processModel.setDepositTxXmr(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - - // complete on successful ack messages - TradeListener ackListener = new TradeListener() { - @Override - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - if (!ackMessage.getSourceMsgClassName().equals(SignContractRequest.class.getSimpleName())) return; - if (ackMessage.isSuccess()) { - if (sender.equals(trade.getTradingPeerNodeAddress())) ack1 = true; - if (sender.equals(trade.getArbitratorNodeAddress())) ack2 = true; - if (ack1 && ack2) { - trade.removeListener(this); - completeAux(); - } - } else { - if (!failed) { - failed = true; - failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task? - } - } + synchronized (trade.getXmrWalletService().getWallet()) { // synchronize on wallet to create deposit tx and freeze funds + + // skip if multisig wallet not complete + if (!processModel.isMultisigSetupComplete()) { + complete(); + return; // TODO: woodser: this does not ack original request? + } + + // skip if deposit tx already created + if (processModel.getDepositTxXmr() != null) { + complete(); + return; } - }; - trade.addListener(ackListener); - // send sign contract requests to peer and arbitrator - sendSignContractRequest(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), offer, depositTx); - sendSignContractRequest(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), offer, depositTx); + // thaw reserved outputs + MoneroWallet wallet = trade.getXmrWalletService().getWallet(); + for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) { + wallet.thawOutput(reserveTxKeyImage); + } + + // create deposit tx + BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee()); + Offer offer = processModel.getOffer(); + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit()); + String multisigAddress = processModel.getMultisigAddress(); + MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount); + + // freeze deposit outputs + // TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig + for (MoneroOutput input : depositTx.getInputs()) { + wallet.freezeOutput(input.getKeyImage().getHex()); + } + + // save process state + processModel.setDepositTxXmr(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + + // create request for peer and arbitrator to sign contract + SignContractRequest request = new SignContractRequest( + trade.getOffer().getId(), + processModel.getMyNodeAddress(), + processModel.getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + new Date().getTime(), + trade.getProcessModel().getAccountId(), + trade.getProcessModel().getPaymentAccountPayload(trade).getHash(), + trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), + depositTx.getHash()); + + // send request to trading peer + processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId()); + ack1 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + + // send request to arbitrator + processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId()); + ack2 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + } } catch (Throwable t) { failed(t); } } - private void sendSignContractRequest(NodeAddress nodeAddress, PubKeyRing pubKeyRing, Offer offer, MoneroTxWallet depositTx) { - - // create request to sign contract - SignContractRequest request = new SignContractRequest( - trade.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), // TODO: ensure not reusing request id across protocol - Version.getP2PMessageVersion(), - new Date().getTime(), - trade.getProcessModel().getAccountId(), - trade.getProcessModel().getPaymentAccountPayload(trade).getHash(), - trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), - depositTx.getHash()); - - // send request - processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, request, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), nodeAddress, trade.getId()); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), nodeAddress, trade.getId(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - } - private void completeAux() { processModel.getXmrWalletService().getWallet().save(); complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java index 1c686651cd..e2defbec2b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java @@ -33,7 +33,7 @@ public class SetupDepositTxsListener extends TradeTask { protected void run() { try { runInterceptHook(); - trade.setupDepositTxsListener(); + trade.listenForDepositTxs(); complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java index 007a21a6cb..39719cc796 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java @@ -57,7 +57,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask { // fetch relevant trade info XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); + MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerCreateAndSignPayoutTx // skip if multisig wallet does not need updated if (!multisigWallet.isMultisigImportNeeded()) { @@ -74,7 +74,6 @@ public class UpdateMultisigWithTradingPeer extends TradeTask { UpdateMultisigResponse response = (UpdateMultisigResponse) message; multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex())); multisigWallet.sync(); - multisigWallet.save(); trade.removeListener(updateMultisigResponseListener); complete(); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java index 2f287faa45..59eb1070df 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java @@ -18,7 +18,6 @@ package bisq.core.trade.protocol.tasks.buyer; import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.offer.Offer; import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; @@ -42,10 +41,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import monero.common.MoneroError; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroAccount; -import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroSubaddress; import monero.wallet.model.MoneroTxConfig; -import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; @Slf4j @@ -65,7 +62,7 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask { Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); - Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + checkNotNull(trade.getOffer(), "offer must not be null"); // gather relevant trade info XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); @@ -84,8 +81,8 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask { if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Multisig import is still needed!!!"); MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig() .setAccountIndex(0) - .addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))) // reduce payment amount to compute fee of similar tx - .addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))) + .addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx + .addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) .setRelay(false) ); @@ -98,19 +95,18 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask { numAttempts++; payoutTx = multisigWallet.createTx(new MoneroTxConfig() .setAccountIndex(0) - .addDestination(new MoneroDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))) // split fee subtracted from each payout amount - .addDestination(new MoneroDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))) // TODO (woodser): support addDestination(addr, amt) without new + .addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount + .addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) .setRelay(false)); } catch (MoneroError e) { - //e.printStackTrace(); - //System.out.println("FAILED TO CREATE PAYOUT TX, ITERATING..."); + // exception expected } } - if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx"); - System.out.println("PAYOUT TX GENERATED ON ATTEMPT " + numAttempts); - System.out.println(payoutTx); + if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts"); + log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx); processModel.setBuyerSignedPayoutTx(payoutTx); + walletService.closeMultisigWallet(trade.getId()); complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java index 393954cf57..080498d418 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java @@ -63,6 +63,7 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask { trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0))); XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx()); trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG); + walletService.closeMultisigWallet(trade.getId()); //processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); } else { log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java index df790f4ce3..d690dd2556 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java @@ -18,7 +18,6 @@ package bisq.core.trade.protocol.tasks.seller; import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.offer.Offer; import bisq.core.trade.Contract; import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; @@ -31,10 +30,6 @@ import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkNotNull; - - - import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; @@ -59,29 +54,17 @@ public class SellerSignAndPublishPayoutTx extends TradeTask { MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex(); Contract contract = trade.getContract(); - Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs trade.getDepositTxId() necessary or avoidable? BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount(); BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount()); - System.out.println("SELLER VERIFYING PAYOUT TX"); - System.out.println("Trade amount: " + trade.getTradeAmount()); - System.out.println("Buyer deposit amount: " + buyerDepositAmount); - System.out.println("Seller deposit amount: " + sellerDepositAmount); - - BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(offer.getBuyerSecurityDeposit().add(trade.getTradeAmount())); - System.out.println("Buyer payout amount (with multiplier): " + buyerPayoutAmount); - BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(offer.getSellerSecurityDeposit()); - System.out.println("Seller payout amount (with multiplier): " + sellerPayoutAmount); - // parse buyer-signed payout tx MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(buyerSignedPayoutTxHex)); - if (parsedTxSet.getTxs().get(0).getTxSet() != parsedTxSet) System.out.println("LINKS ARE WRONG STRAIGHT FROM PARSING!!!"); - if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): nack + if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): test nack MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0); - System.out.println("Parsed buyer signed tx hex:\n" + buyerSignedPayoutTx); // verify payout tx has exactly 2 destinations + log.info("Seller verifying buyer-signed payout tx"); if (buyerSignedPayoutTx.getOutgoingTransfer() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Buyer-signed payout tx does not have exactly two destinations"); // get buyer and seller destinations (order not preserved) @@ -93,8 +76,8 @@ public class SellerSignAndPublishPayoutTx extends TradeTask { if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract"); if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract"); - // verify change address is multisig's primary address // TODO (woodser): ideally change amount is 0, seen with 0 conf payout tx - if (!buyerSignedPayoutTx.getChangeAmount().equals(new BigInteger("0")) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); + // verify change address is multisig's primary address + if (!buyerSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); // verify sum of outputs = destination amounts + change amount if (!buyerSignedPayoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(buyerSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); @@ -117,69 +100,16 @@ public class SellerSignAndPublishPayoutTx extends TradeTask { // submit fully signed payout tx to the network multisigWallet.submitMultisigTxHex(signedMultisigTxHex); + + // close multisig wallet + walletService.closeMultisigWallet(trade.getId()); - // update state - parsedTxSet.setMultisigTxHex(signedMultisigTxHex); - if (parsedTxSet.getTxs().get(0).getTxSet() != parsedTxSet) System.out.println("LINKS ARE WRONG!!!"); - trade.setPayoutTx(parsedTxSet.getTxs().get(0)); - trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash()); - trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); - complete(); - -// checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); -// -// Offer offer = trade.getOffer(); -// TradingPeer tradingPeer = trade.getTradingPeer(); -// BtcWalletService walletService = processModel.getBtcWalletService(); -// String id = processModel.getOffer().getId(); -// -// final byte[] buyerSignature = tradingPeer.getSignature(); -// -// Coin buyerPayoutAmount = checkNotNull(offer.getBuyerSecurityDeposit()).add(trade.getTradeAmount()); -// Coin sellerPayoutAmount = offer.getSellerSecurityDeposit(); -// -// final String buyerPayoutAddressString = tradingPeer.getPayoutAddressString(); -// String sellerPayoutAddressString = walletService.getOrCreateAddressEntry(id, -// AddressEntry.Context.TRADE_PAYOUT).getAddressString(); -// -// final byte[] buyerMultiSigPubKey = tradingPeer.getMultiSigPubKey(); -// byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); -// -// Optional multiSigAddressEntryOptional = walletService.getAddressEntry(id, -// AddressEntry.Context.MULTI_SIG); -// if (!multiSigAddressEntryOptional.isPresent() || !Arrays.equals(sellerMultiSigPubKey, -// multiSigAddressEntryOptional.get().getPubKey())) { -// // In some error edge cases it can be that the address entry is not marked (or was unmarked). -// // We do not want to fail in that case and only report a warning. -// // One case where that helped to avoid a failed payout attempt was when the taker had a power failure -// // at the moment when the offer was taken. This caused first to not see step 1 in the trade process -// // (all greyed out) but after the deposit tx was confirmed the trade process was on step 2 and -// // everything looked ok. At the payout multiSigAddressEntryOptional was not present and payout -// // could not be done. By changing the previous behaviour from fail if multiSigAddressEntryOptional -// // is not present to only log a warning the payout worked. -// log.warn("sellerMultiSigPubKey from AddressEntry does not match the one from the trade data. " + -// "Trade id ={}, multiSigAddressEntryOptional={}", id, multiSigAddressEntryOptional); -// } -// -// DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(id, sellerMultiSigPubKey); -// -// Transaction transaction = processModel.getTradeWalletService().sellerSignsAndFinalizesPayoutTx( -// checkNotNull(trade.getDepositTx()), -// buyerSignature, -// buyerPayoutAmount, -// sellerPayoutAmount, -// buyerPayoutAddressString, -// sellerPayoutAddressString, -// multiSigKeyPair, -// buyerMultiSigPubKey, -// sellerMultiSigPubKey -// ); -// -// trade.setPayoutTx(transaction); -// -// walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.MULTI_SIG); -// -// complete(); + // update trade state + parsedTxSet.setMultisigTxHex(signedMultisigTxHex); // TODO (woodser): better place to store this? + trade.setPayoutTx(parsedTxSet.getTxs().get(0)); + trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash()); + trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); + complete(); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java index f122dd4104..a3007b852e 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java @@ -40,29 +40,28 @@ public class TakerReservesTradeFunds extends TradeTask { protected void run() { try { runInterceptHook(); - - // create transaction to reserve trade - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); - BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong()); - MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount); - - // freeze trade funds - // TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs - List reserveTxKeyImages = new ArrayList(); - MoneroWallet wallet = model.getXmrWalletService().getWallet(); - for (MoneroOutput input : reserveTx.getInputs()) { - reserveTxKeyImages.add(input.getKeyImage().getHex()); - wallet.freezeOutput(input.getKeyImage().getHex()); + + // synchronize on wallet to reserve key images + synchronized (model.getXmrWalletService().getWallet()) { + + // create transaction to reserve trade + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); + BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong()); + MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount); + + // collect reserved key images // TODO (woodser): switch to proof of reserve? + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // save process state + // TODO (woodser): persist + processModel.setReserveTx(reserveTx); + processModel.getTaker().setReserveTxKeyImages(reservedKeyImages); + trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used? + //trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states + complete(); } - - // save process state - // TODO (woodser): persist - processModel.setReserveTx(reserveTx); - processModel.getTaker().setReserveTxKeyImages(reserveTxKeyImages); - trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used? - //trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states - complete(); } catch (Throwable t) { trade.setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendsInitTradeRequestToArbitrator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendsInitTradeRequestToArbitrator.java index 4f51b70a12..56559423f4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendsInitTradeRequestToArbitrator.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendsInitTradeRequestToArbitrator.java @@ -42,12 +42,11 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask { try { runInterceptHook(); - // send request to offer signer + // send request to arbitrator sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); - complete(); } // send request to backup arbitrator if signer unavailable @@ -63,7 +62,6 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask { @Override public void onArrived() { log.info("{} arrived at backup arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); - complete(); } @Override public void onFault(String errorMessage) { // TODO (woodser): distinguish nack from offline @@ -73,6 +71,7 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask { }); } }); + complete(); // TODO (woodser): onArrived() doesn't get called if arbitrator rejects concurrent requests. always complete before onArrived()? } catch (Throwable t) { failed(t); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java index b1a0963c05..c03a60d620 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java @@ -70,7 +70,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler { } @Override - public void handleErrorMessage(String errorMessage) { + public synchronized void handleErrorMessage(String errorMessage) { // A task runner may call handleErrorMessage(String) more than once. // Throw only one exception if that happens, to avoid looping until the // grpc stream is closed diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java index ed2e85a093..13124febb6 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcExceptionHandler.java @@ -47,7 +47,7 @@ class GrpcExceptionHandler { public GrpcExceptionHandler() { } - public void handleException(Logger log, + public synchronized void handleException(Logger log, Throwable t, StreamObserver responseObserver) { // Log the core api error (this is last chance to do that), wrap it in a new @@ -58,7 +58,7 @@ class GrpcExceptionHandler { throw grpcStatusRuntimeException; } - public void handleExceptionAsWarning(Logger log, + public synchronized void handleExceptionAsWarning(Logger log, String calledMethod, Throwable t, StreamObserver responseObserver) { diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcNotificationsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcNotificationsService.java index 02cac9365e..4021c62ec5 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcNotificationsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcNotificationsService.java @@ -8,7 +8,7 @@ import bisq.proto.grpc.NotificationsGrpc.NotificationsImplBase; import bisq.proto.grpc.RegisterNotificationListenerRequest; import bisq.proto.grpc.SendNotificationReply; import bisq.proto.grpc.SendNotificationRequest; - +import io.grpc.Context; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; @@ -46,24 +46,30 @@ class GrpcNotificationsService extends NotificationsImplBase { @Override public void registerNotificationListener(RegisterNotificationListenerRequest request, StreamObserver responseObserver) { - try { - coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver)); - // No onNext / onCompleted, as the response observer should be kept open - } catch (Throwable t) { - exceptionHandler.handleException(log, t, responseObserver); - } + Context ctx = Context.current().fork(); // context is independent for long-lived request + ctx.run(() -> { + try { + coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver)); + // No onNext / onCompleted, as the response observer should be kept open + } catch (Throwable t) { + exceptionHandler.handleException(log, t, responseObserver); + } + }); } @Override public void sendNotification(SendNotificationRequest request, StreamObserver responseObserver) { - try { - coreApi.sendNotification(request.getNotification()); - responseObserver.onNext(SendNotificationReply.newBuilder().build()); - responseObserver.onCompleted(); - } catch (Throwable t) { - exceptionHandler.handleException(log, t, responseObserver); - } + Context ctx = Context.current().fork(); // context is independent from notification delivery + ctx.run(() -> { + try { + coreApi.sendNotification(request.getNotification()); + responseObserver.onNext(SendNotificationReply.newBuilder().build()); + responseObserver.onCompleted(); + } catch (Throwable t) { + exceptionHandler.handleException(log, t, responseObserver); + } + }); } @Value diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index ca5a2db49f..16d5302d33 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -141,6 +141,11 @@ class GrpcOffersService extends OffersImplBase { @Override public void createOffer(CreateOfferRequest req, StreamObserver responseObserver) { + GrpcErrorMessageHandler errorMessageHandler = + new GrpcErrorMessageHandler(getCreateOfferMethod().getFullMethodName(), + responseObserver, + exceptionHandler, + log); try { coreApi.createAnPlaceOffer( req.getCurrencyCode(), @@ -162,6 +167,10 @@ class GrpcOffersService extends OffersImplBase { .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); + }, + errorMessage -> { + if (!errorMessageHandler.isErrorHandled()) + errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index 70b10cafab..e17d7cea31 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -65,7 +65,6 @@ public class GrpcServer { GrpcMoneroConnectionsService moneroConnectionsService, GrpcMoneroNodeService moneroNodeService) { this.server = ServerBuilder.forPort(config.apiPort) - .executor(UserThread.getExecutor()) .addService(interceptForward(accountService, accountService.interceptors())) .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) .addService(interceptForward(disputesService, disputesService.interceptors())) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 8f2c62086f..72eae92e93 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -19,7 +19,6 @@ package bisq.daemon.grpc; import bisq.core.api.CoreApi; import bisq.core.api.model.TradeInfo; -import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Trade; import bisq.proto.grpc.ConfirmPaymentReceivedReply; @@ -40,7 +39,6 @@ import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.WithdrawFundsReply; import bisq.proto.grpc.WithdrawFundsRequest; - import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; @@ -122,20 +120,24 @@ class GrpcTradesService extends TradesImplBase { responseObserver, exceptionHandler, log); - coreApi.takeOffer(req.getOfferId(), - req.getPaymentAccountId(), - trade -> { - TradeInfo tradeInfo = toTradeInfo(trade); - var reply = TakeOfferReply.newBuilder() - .setTrade(tradeInfo.toProtoMessage()) - .build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - }, - errorMessage -> { - if (!errorMessageHandler.isErrorHandled()) - errorMessageHandler.handleErrorMessage(errorMessage); - }); + try { + coreApi.takeOffer(req.getOfferId(), + req.getPaymentAccountId(), + trade -> { + TradeInfo tradeInfo = toTradeInfo(trade); + var reply = TakeOfferReply.newBuilder() + .setTrade(tradeInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }, + errorMessage -> { + if (!errorMessageHandler.isErrorHandled()) + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } } @Override diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 8fbd194b6d..74d8121800 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -17,6 +17,7 @@ package bisq.daemon.grpc; +import bisq.common.UserThread; import bisq.core.api.CoreApi; import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.TxFeeRateInfo; @@ -102,16 +103,18 @@ class GrpcWalletsService extends WalletsImplBase { @Override public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { - try { - var balances = coreApi.getBalances(req.getCurrencyCode()); - var reply = GetBalancesReply.newBuilder() - .setBalances(balances.toProtoMessage()) - .build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (Throwable cause) { - exceptionHandler.handleException(log, cause, responseObserver); - } + UserThread.execute(() -> { // TODO (woodser): Balances.updateBalances() runs on UserThread for JFX components, so call from user thread, else the properties may not be updated. remove JFX properties or push delay into CoreWalletsService.getXmrBalances()? + try { + var balances = coreApi.getBalances(req.getCurrencyCode()); + var reply = GetBalancesReply.newBuilder() + .setBalances(balances.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + }); } @Override diff --git a/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java index 73096a0336..678be0ec42 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java +++ b/daemon/src/main/java/bisq/daemon/grpc/interceptor/GrpcCallRateMeter.java @@ -40,31 +40,37 @@ public class GrpcCallRateMeter { } public boolean checkAndIncrement() { - if (getCallsCount() < allowedCallsPerTimeWindow) { - incrementCallsCount(); - return true; - } else { - return false; + synchronized (callTimestamps) { + if (getCallsCount() < allowedCallsPerTimeWindow) { + incrementCallsCount(); + return true; + } else { + return false; + } } } public int getCallsCount() { - removeStaleCallTimestamps(); - return callTimestamps.size(); + synchronized (callTimestamps) { + removeStaleCallTimestamps(); + return callTimestamps.size(); + } } public String getCallsCountProgress(String calledMethodName) { - String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase()); - // Just print 'GetVersion has been called N times...', - // not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...' - String loggedMethodName = calledMethodName.split("/")[1]; - return format("%s has been called %d time%s in the last %s, rate limit is %d/%s", - loggedMethodName, - callTimestamps.size(), - callTimestamps.size() == 1 ? "" : "s", - shortTimeUnitName, - allowedCallsPerTimeWindow, - shortTimeUnitName); + synchronized (callTimestamps) { + String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase()); + // Just print 'GetVersion has been called N times...', + // not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...' + String loggedMethodName = calledMethodName.split("/")[1]; + return format("%s has been called %d time%s in the last %s, rate limit is %d/%s", + loggedMethodName, + callTimestamps.size(), + callTimestamps.size() == 1 ? "" : "s", + shortTimeUnitName, + allowedCallsPerTimeWindow, + shortTimeUnitName); + } } private void incrementCallsCount() { @@ -85,11 +91,13 @@ public class GrpcCallRateMeter { @Override public String toString() { - return "GrpcCallRateMeter{" + - "allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow + - ", timeUnit=" + timeUnit.name() + - ", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds + - ", callsCount=" + callTimestamps.size() + - '}'; + synchronized (callTimestamps) { + return "GrpcCallRateMeter{" + + "allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow + + ", timeUnit=" + timeUnit.name() + + ", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds + + ", callsCount=" + callTimestamps.size() + + '}'; + } } } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 0be3eff7b2..c893b69b62 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -736,8 +736,10 @@ public class MainView extends InitializableView { }); model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> { - p2PNetworkIcon.setOpacity(1); - p2pNetworkProgressBar.setProgress(0); + UserThread.execute(() -> { + p2PNetworkIcon.setOpacity(1); + p2pNetworkProgressBar.setProgress(0); + }); }); p2pNetworkProgressBar = new JFXProgressBar(-1); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java index 56c3e8f099..8cbc4c3efe 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java @@ -240,7 +240,7 @@ public class LockedView extends ActivatableView { private Optional getTradable(LockedListItem item) { String offerId = item.getAddressEntry().getOfferId(); - Optional tradeOptional = tradeManager.getTradeById(offerId); + Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { diff --git a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java index ecd4c9b79d..85ba838b4c 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java @@ -239,7 +239,7 @@ public class ReservedView extends ActivatableView { private Optional getTradable(ReservedListItem item) { String offerId = item.getAddressEntry().getOfferId(); - Optional tradeOptional = tradeManager.getTradeById(offerId); + Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java index 185bb3d773..108bcd2ab3 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java @@ -32,9 +32,7 @@ import javafx.collections.ObservableList; import java.util.Optional; import lombok.extern.slf4j.Slf4j; - - - +import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroTxWallet; @@ -82,14 +80,14 @@ class TransactionAwareTrade implements TransactionAwareTradable { private boolean isMakerDepositTx(String txId) { return Optional.ofNullable(trade.getMakerDepositTx()) - .map(MoneroTxWallet::getHash) + .map(MoneroTx::getHash) .map(hash -> hash.equals(txId)) .orElse(false); } private boolean isTakerDepositTx(String txId) { return Optional.ofNullable(trade.getTakerDepositTx()) - .map(MoneroTxWallet::getHash) + .map(MoneroTx::getHash) .map(hash -> hash.equals(txId)) .orElse(false); } diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java index 6826ff5196..c3f34022d0 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java @@ -382,17 +382,19 @@ public class OfferBookChartView extends ActivatableViewAndModel { + seriesBuy.getData().clear(); + seriesSell.getData().clear(); + areaChart.getData().clear(); - boolean isCrypto = CurrencyUtil.isCryptoCurrency(model.getCurrencyCode()); + boolean isCrypto = CurrencyUtil.isCryptoCurrency(model.getCurrencyCode()); - // crypto: left-sell, right-buy. fiat: left-buy, right-sell - seriesBuy.getData().addAll(filterOutliersBuy(model.getBuyData(), isCrypto)); - seriesSell.getData().addAll(filterOutliersSell(model.getSellData(), isCrypto)); + // crypto: left-sell, right-buy. fiat: left-buy, right-sell + seriesBuy.getData().addAll(filterOutliersBuy(model.getBuyData(), isCrypto)); + seriesSell.getData().addAll(filterOutliersSell(model.getSellData(), isCrypto)); - areaChart.getData().addAll(List.of(seriesBuy, seriesSell)); + areaChart.getData().addAll(List.of(seriesBuy, seriesSell)); + }); } List> filterOutliersBuy(List> buy, boolean isCrypto) { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 02a2819b63..387c6aedb6 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -62,7 +62,7 @@ import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; - +import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.config.Config; import bisq.common.util.Tuple3; @@ -305,7 +305,7 @@ public class OfferBookView extends ActivatableViewAndModel nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())); + offerListListener = c -> UserThread.execute(() -> nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size()))); // Fixes incorrect ordering of Available offers: // https://github.com/bisq-network/bisq-desktop/issues/588 diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 3c8460c248..37cfc0a541 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -51,7 +51,7 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.network.CloseConnectionReason; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.ConnectionListener; - +import bisq.common.UserThread; import bisq.common.app.DevEnv; import org.bitcoinj.core.Coin; @@ -524,23 +524,25 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } private void updateSpinnerInfo() { - if (!showPayFundsScreenDisplayed.get() || - offerWarning.get() != null || - errorMessage.get() != null || - showTransactionPublishedScreen.get()) { - spinnerInfoText.set(""); - } else if (dataModel.getIsBtcWalletFunded().get()) { - spinnerInfoText.set(""); - /* if (dataModel.isFeeFromFundingTxSufficient.get()) { + UserThread.execute(() -> { + if (!showPayFundsScreenDisplayed.get() || + offerWarning.get() != null || + errorMessage.get() != null || + showTransactionPublishedScreen.get()) { spinnerInfoText.set(""); + } else if (dataModel.getIsBtcWalletFunded().get()) { + spinnerInfoText.set(""); + /* if (dataModel.isFeeFromFundingTxSufficient.get()) { + spinnerInfoText.set(""); + } else { + spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); + }*/ } else { - spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); - }*/ - } else { - spinnerInfoText.set(Res.get("shared.waitingForFunds")); - } + spinnerInfoText.set(Res.get("shared.waitingForFunds")); + } - isWaitingForFunds.set(!spinnerInfoText.get().isEmpty()); + isWaitingForFunds.set(!spinnerInfoText.get().isEmpty()); + }); } private void addListeners() { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java index cce50686f1..0817bafdc9 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java @@ -515,47 +515,49 @@ public abstract class Overlay> { if (owner != null) { Scene rootScene = owner.getScene(); if (rootScene != null) { - Scene scene = new Scene(getRootContainer()); - scene.getStylesheets().setAll(rootScene.getStylesheets()); - scene.setFill(Color.TRANSPARENT); + UserThread.execute(() -> { + Scene scene = new Scene(getRootContainer()); + scene.getStylesheets().setAll(rootScene.getStylesheets()); + scene.setFill(Color.TRANSPARENT); - setupKeyHandler(scene); + setupKeyHandler(scene); - stage = new Stage(); - stage.setScene(scene); - Window window = rootScene.getWindow(); - setModality(); - stage.initStyle(StageStyle.TRANSPARENT); - stage.setOnCloseRequest(event -> { - event.consume(); - doClose(); + stage = new Stage(); + stage.setScene(scene); + Window window = rootScene.getWindow(); + setModality(); + stage.initStyle(StageStyle.TRANSPARENT); + stage.setOnCloseRequest(event -> { + event.consume(); + doClose(); + }); + stage.sizeToScene(); + stage.show(); + + layout(); + + addEffectToBackground(); + + // On Linux the owner stage does not move the child stage as it does on Mac + // So we need to apply centerPopup. Further with fast movements the handler loses + // the latest position, with a delay it fixes that. + // Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS + positionListener = (observable, oldValue, newValue) -> { + if (stage != null) { + layout(); + if (centerTime != null) + centerTime.stop(); + + centerTime = UserThread.runAfter(this::layout, 3); + } + }; + window.xProperty().addListener(positionListener); + window.yProperty().addListener(positionListener); + window.widthProperty().addListener(positionListener); + + animateDisplay(); + isDisplayed = true; }); - stage.sizeToScene(); - stage.show(); - - layout(); - - addEffectToBackground(); - - // On Linux the owner stage does not move the child stage as it does on Mac - // So we need to apply centerPopup. Further with fast movements the handler loses - // the latest position, with a delay it fixes that. - // Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS - positionListener = (observable, oldValue, newValue) -> { - if (stage != null) { - layout(); - if (centerTime != null) - centerTime.stop(); - - centerTime = UserThread.runAfter(this::layout, 3); - } - }; - window.xProperty().addListener(positionListener); - window.yProperty().addListener(positionListener); - window.widthProperty().addListener(positionListener); - - animateDisplay(); - isDisplayed = true; } } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java index d7de327c68..9f8ab48eb0 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java @@ -64,7 +64,9 @@ public class Notification extends Overlay { if (autoClose && autoCloseTimer == null) autoCloseTimer = UserThread.runAfter(this::doClose, 6); - stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose()); + UserThread.execute(() -> { + stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose()); + }); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index b3b3723046..243087b6da 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -54,7 +54,7 @@ import bisq.core.trade.protocol.SellerProtocol; import bisq.core.user.Preferences; import bisq.network.p2p.P2PService; - +import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRingProvider; import bisq.common.handlers.ErrorMessageHandler; @@ -87,8 +87,7 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; - - +import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; @@ -360,54 +359,56 @@ public class PendingTradesDataModel extends ActivatableDataModel { } private void doSelectItem(@Nullable PendingTradesListItem item) { - if (selectedTrade != null) - selectedTrade.stateProperty().removeListener(tradeStateChangeListener); + UserThread.execute(() -> { + if (selectedTrade != null) + selectedTrade.stateProperty().removeListener(tradeStateChangeListener); - if (item != null) { - selectedTrade = item.getTrade(); - if (selectedTrade == null) { - log.error("selectedTrade is null"); - return; - } - - MoneroTxWallet makerDepositTx = selectedTrade.getMakerDepositTx(); - MoneroTxWallet takerDepositTx = selectedTrade.getTakerDepositTx(); - String tradeId = selectedTrade.getId(); - tradeStateChangeListener = (observable, oldValue, newValue) -> { - if (makerDepositTx != null && takerDepositTx != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable - makerTxId.set(makerDepositTx.getHash()); - takerTxId.set(takerDepositTx.getHash()); - notificationCenter.setSelectedTradeId(tradeId); - selectedTrade.stateProperty().removeListener(tradeStateChangeListener); - } else { - makerTxId.set(""); - takerTxId.set(""); + if (item != null) { + selectedTrade = item.getTrade(); + if (selectedTrade == null) { + log.error("selectedTrade is null"); + return; } - }; - selectedTrade.stateProperty().addListener(tradeStateChangeListener); - Offer offer = selectedTrade.getOffer(); - if (offer == null) { - log.error("offer is null"); - return; - } + MoneroTx makerDepositTx = selectedTrade.getMakerDepositTx(); + MoneroTx takerDepositTx = selectedTrade.getTakerDepositTx(); + String tradeId = selectedTrade.getId(); + tradeStateChangeListener = (observable, oldValue, newValue) -> { + if (makerDepositTx != null && takerDepositTx != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable + makerTxId.set(makerDepositTx.getHash()); + takerTxId.set(takerDepositTx.getHash()); + notificationCenter.setSelectedTradeId(tradeId); + selectedTrade.stateProperty().removeListener(tradeStateChangeListener); + } else { + makerTxId.set(""); + takerTxId.set(""); + } + }; + selectedTrade.stateProperty().addListener(tradeStateChangeListener); - isMaker = tradeManager.isMyOffer(offer); - if (makerDepositTx != null && takerDepositTx != null) { - makerTxId.set(makerDepositTx.getHash()); - takerTxId.set(takerDepositTx.getHash()); + Offer offer = selectedTrade.getOffer(); + if (offer == null) { + log.error("offer is null"); + return; + } + + isMaker = tradeManager.isMyOffer(offer); + if (makerDepositTx != null && takerDepositTx != null) { + makerTxId.set(makerDepositTx.getHash()); + takerTxId.set(takerDepositTx.getHash()); + } else { + makerTxId.set(""); + takerTxId.set(""); + } + notificationCenter.setSelectedTradeId(tradeId); } else { + selectedTrade = null; makerTxId.set(""); takerTxId.set(""); + notificationCenter.setSelectedTradeId(null); } - notificationCenter.setSelectedTradeId(tradeId); - } else { - selectedTrade = null; - makerTxId.set(""); - takerTxId.set(""); - notificationCenter.setSelectedTradeId(null); - } - selectedItemProperty.set(item); + selectedItemProperty.set(item); + }); } private void tryOpenDispute(boolean isSupportTicket) { @@ -458,8 +459,9 @@ public class PendingTradesDataModel extends ActivatableDataModel { byte[] payoutTxSerialized = null; String payoutTxHashAsString = null; MoneroTxWallet payoutTx = trade.getPayoutTx(); - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); String updatedMultisigHex = multisigWallet.getMultisigHex(); + xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet if (payoutTx != null) { // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxHashAsString = payoutTx.getHashAsString(); 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 d3f72fa189..03d4a00805 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 @@ -363,7 +363,7 @@ public class PendingTradesView extends ActivatableViewAndModel isMaybeInvalidTrade(item.getTrade()))); + UserThread.execute(() -> moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade())))); } private boolean isMaybeInvalidTrade(Trade trade) { 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 541074cb11..c8db1349e9 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 @@ -137,7 +137,8 @@ public class BuyerStep2View extends TradeStepView { showPopup(); } else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG.ordinal()) { if (!trade.hasFailed()) { - switch (state) { + UserThread.execute(() -> { + switch (state) { case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED: case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG: busyAnimation.play(); @@ -169,7 +170,8 @@ public class BuyerStep2View extends TradeStepView { busyAnimation.stop(); statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); break; - } + } + }); } else { log.warn("Trade contains error message {}", trade.getErrorMessage()); statusLabel.setText(""); diff --git a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java index ed48fdf8f1..aa208dc128 100644 --- a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java @@ -471,43 +471,45 @@ public class ChatView extends AnchorPane { } private void updateMsgState(ChatMessage message) { - boolean visible; - AwesomeIcon icon = null; - String text = null; - statusIcon.getStyleClass().add("status-icon"); - statusInfoLabel.getStyleClass().add("status-icon"); - statusHBox.setOpacity(1); - log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), - message.acknowledgedProperty().get(), message.arrivedProperty().get()); - if (message.acknowledgedProperty().get()) { - visible = true; - icon = AwesomeIcon.OK_SIGN; - text = Res.get("support.acknowledged"); - } else if (message.ackErrorProperty().get() != null) { - visible = true; - icon = AwesomeIcon.EXCLAMATION_SIGN; - text = Res.get("support.error", message.ackErrorProperty().get()); - statusIcon.getStyleClass().add("error-text"); - statusInfoLabel.getStyleClass().add("error-text"); - } else if (message.arrivedProperty().get()) { - visible = true; - icon = AwesomeIcon.OK; - text = Res.get("support.arrived"); - } else if (message.storedInMailboxProperty().get()) { - visible = true; - icon = AwesomeIcon.ENVELOPE; - text = Res.get("support.savedInMailbox"); - } else { - visible = false; - log.debug("updateMsgState called but no msg state available. message={}", message); - } + UserThread.execute(() -> { + boolean visible; + AwesomeIcon icon = null; + String text = null; + statusIcon.getStyleClass().add("status-icon"); + statusInfoLabel.getStyleClass().add("status-icon"); + statusHBox.setOpacity(1); + log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), + message.acknowledgedProperty().get(), message.arrivedProperty().get()); + if (message.acknowledgedProperty().get()) { + visible = true; + icon = AwesomeIcon.OK_SIGN; + text = Res.get("support.acknowledged"); + } else if (message.ackErrorProperty().get() != null) { + visible = true; + icon = AwesomeIcon.EXCLAMATION_SIGN; + text = Res.get("support.error", message.ackErrorProperty().get()); + statusIcon.getStyleClass().add("error-text"); + statusInfoLabel.getStyleClass().add("error-text"); + } else if (message.arrivedProperty().get()) { + visible = true; + icon = AwesomeIcon.OK; + text = Res.get("support.arrived"); + } else if (message.storedInMailboxProperty().get()) { + visible = true; + icon = AwesomeIcon.ENVELOPE; + text = Res.get("support.savedInMailbox"); + } else { + visible = false; + log.debug("updateMsgState called but no msg state available. message={}", message); + } - statusHBox.setVisible(visible); - if (visible) { - AwesomeDude.setIcon(statusIcon, icon, "14"); - statusIcon.setTooltip(new Tooltip(text)); - statusInfoLabel.setText(text); - } + statusHBox.setVisible(visible); + if (visible) { + AwesomeDude.setIcon(statusIcon, icon, "14"); + statusIcon.setTooltip(new Tooltip(text)); + statusInfoLabel.setText(text); + } + }); } }; } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java index 84e9c235ef..817d6f9073 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java @@ -85,76 +85,78 @@ public class DisputeChatPopup { } public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) { - closeChat(); - this.selectedDispute = selectedDispute; - selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); - disputeManager.requestPersistence(); - - ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName); - chatView.setAllowAttachments(true); - chatView.setDisplayHeader(false); - chatView.initialize(); - - AnchorPane pane = new AnchorPane(chatView); - pane.setPrefSize(760, 500); - AnchorPane.setLeftAnchor(chatView, 10d); - AnchorPane.setRightAnchor(chatView, 10d); - AnchorPane.setTopAnchor(chatView, -20d); - AnchorPane.setBottomAnchor(chatView, 10d); - pane.getStyleClass().add("dispute-chat-border"); - Button closeDisputeButton = null; - if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) { - closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); - } - chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); - chatView.activate(); - chatView.scrollToBottom(); - chatPopupStage = new Stage(); - chatPopupStage.setTitle(Res.get("disputeChat.chatWindowTitle", selectedDispute.getShortTradeId()) - + " " + selectedDispute.getRoleString()); - StackPane owner = MainView.getRootContainer(); - Scene rootScene = owner.getScene(); - chatPopupStage.initOwner(rootScene.getWindow()); - chatPopupStage.initModality(Modality.NONE); - chatPopupStage.initStyle(StageStyle.DECORATED); - chatPopupStage.setOnHiding(event -> { - chatView.deactivate(); - // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. + UserThread.execute(() -> { + closeChat(); + this.selectedDispute = selectedDispute; selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); disputeManager.requestPersistence(); - chatPopupStage = null; - }); - Scene scene = new Scene(pane); - CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false); - scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { - if (ev.getCode() == KeyCode.ESCAPE) { - ev.consume(); - chatPopupStage.hide(); + ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName); + chatView.setAllowAttachments(true); + chatView.setDisplayHeader(false); + chatView.initialize(); + + AnchorPane pane = new AnchorPane(chatView); + pane.setPrefSize(760, 500); + AnchorPane.setLeftAnchor(chatView, 10d); + AnchorPane.setRightAnchor(chatView, 10d); + AnchorPane.setTopAnchor(chatView, -20d); + AnchorPane.setBottomAnchor(chatView, 10d); + pane.getStyleClass().add("dispute-chat-border"); + Button closeDisputeButton = null; + if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) { + closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); } + chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); + chatView.activate(); + chatView.scrollToBottom(); + chatPopupStage = new Stage(); + chatPopupStage.setTitle(Res.get("disputeChat.chatWindowTitle", selectedDispute.getShortTradeId()) + + " " + selectedDispute.getRoleString()); + StackPane owner = MainView.getRootContainer(); + Scene rootScene = owner.getScene(); + chatPopupStage.initOwner(rootScene.getWindow()); + chatPopupStage.initModality(Modality.NONE); + chatPopupStage.initStyle(StageStyle.DECORATED); + chatPopupStage.setOnHiding(event -> { + chatView.deactivate(); + // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. + selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); + disputeManager.requestPersistence(); + chatPopupStage = null; + }); + + Scene scene = new Scene(pane); + CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false); + scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { + if (ev.getCode() == KeyCode.ESCAPE) { + ev.consume(); + chatPopupStage.hide(); + } + }); + chatPopupStage.setScene(scene); + chatPopupStage.setOpacity(0); + chatPopupStage.show(); + + xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; + chatPopupStage.xProperty().addListener(xPositionListener); + yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; + chatPopupStage.yProperty().addListener(yPositionListener); + + if (chatPopupStageXPosition == -1) { + Window rootSceneWindow = rootScene.getWindow(); + double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); + chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); + chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); + } else { + chatPopupStage.setX(chatPopupStageXPosition); + chatPopupStage.setY(chatPopupStageYPosition); + } + + // Delay display to next render frame to avoid that the popup is first quickly displayed in default position + // and after a short moment in the correct position + UserThread.execute(() -> chatPopupStage.setOpacity(1)); }); - chatPopupStage.setScene(scene); - chatPopupStage.setOpacity(0); - chatPopupStage.show(); - - xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; - chatPopupStage.xProperty().addListener(xPositionListener); - yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; - chatPopupStage.yProperty().addListener(yPositionListener); - - if (chatPopupStageXPosition == -1) { - Window rootSceneWindow = rootScene.getWindow(); - double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); - chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); - chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); - } else { - chatPopupStage.setX(chatPopupStageXPosition); - chatPopupStage.setY(chatPopupStageYPosition); - } - - // Delay display to next render frame to avoid that the popup is first quickly displayed in default position - // and after a short moment in the correct position - UserThread.execute(() -> chatPopupStage.setOpacity(1)); } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 691a0d4641..d2820e3bc2 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -1131,7 +1131,7 @@ public abstract class DisputeView extends ActivatableView { super.updateItem(item, empty); if (item != null && !empty) { - Optional tradeOptional = tradeManager.getTradeById(item.getTradeId()); + Optional tradeOptional = tradeManager.getOpenTrade(item.getTradeId()); if (tradeOptional.isPresent()) { field = new HyperlinkWithIcon(item.getShortTradeId()); field.setMouseTransparent(false); @@ -1349,31 +1349,33 @@ public abstract class DisputeView extends ActivatableView { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - if (closedProperty != null) { - closedProperty.removeListener(listener); - } + UserThread.execute(() -> { + if (item != null && !empty) { + if (closedProperty != null) { + closedProperty.removeListener(listener); + } - listener = (observable, oldValue, newValue) -> { - setText(newValue ? Res.get("support.closed") : Res.get("support.open")); + listener = (observable, oldValue, newValue) -> { + setText(newValue ? Res.get("support.closed") : Res.get("support.open")); + if (getTableRow() != null) + getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); + if (item.isClosed() && item == chatPopup.getSelectedDispute()) + chatPopup.closeChat(); // close the chat popup when the associated ticket is closed + }; + closedProperty = item.isClosedProperty(); + closedProperty.addListener(listener); + boolean isClosed = item.isClosed(); + setText(isClosed ? Res.get("support.closed") : Res.get("support.open")); if (getTableRow() != null) - getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); - if (item.isClosed() && item == chatPopup.getSelectedDispute()) - chatPopup.closeChat(); // close the chat popup when the associated ticket is closed - }; - closedProperty = item.isClosedProperty(); - closedProperty.addListener(listener); - boolean isClosed = item.isClosed(); - setText(isClosed ? Res.get("support.closed") : Res.get("support.open")); - if (getTableRow() != null) - getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); - } else { - if (closedProperty != null) { - closedProperty.removeListener(listener); - closedProperty = null; + getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); + } else { + if (closedProperty != null) { + closedProperty.removeListener(listener); + closedProperty = null; + } + setText(""); } - setText(""); - } + }); } }; } @@ -1389,27 +1391,29 @@ public abstract class DisputeView extends ActivatableView { } private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) { - if (chatBadge == null) - return; - // when the chat popup is active, we do not display new message count indicator for that item - if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) { - chatBadge.setText(""); - chatBadge.setEnabled(false); - chatBadge.refreshBadge(); - // have to UserThread.execute or the new message will be sent to peer as "read" - UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag())); - return; - } + UserThread.execute(() -> { + if (chatBadge == null) + return; + // when the chat popup is active, we do not display new message count indicator for that item + if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) { + chatBadge.setText(""); + chatBadge.setEnabled(false); + chatBadge.refreshBadge(); + // have to UserThread.execute or the new message will be sent to peer as "read" + UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag())); + return; + } - if (dispute.unreadMessageCount(senderFlag()) > 0) { - chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag()))); - chatBadge.setEnabled(true); - } else { - chatBadge.setText(""); - chatBadge.setEnabled(false); - } - chatBadge.refreshBadge(); - dispute.refreshAlertLevel(senderFlag()); + if (dispute.unreadMessageCount(senderFlag()) > 0) { + chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag()))); + chatBadge.setEnabled(true); + } else { + chatBadge.setText(""); + chatBadge.setEnabled(false); + } + chatBadge.refreshBadge(); + dispute.refreshAlertLevel(senderFlag()); + }); } private String getCounterpartyName() { diff --git a/desktop/src/main/java/bisq/desktop/util/Transitions.java b/desktop/src/main/java/bisq/desktop/util/Transitions.java index f94efdb4c4..d5596e28ee 100644 --- a/desktop/src/main/java/bisq/desktop/util/Transitions.java +++ b/desktop/src/main/java/bisq/desktop/util/Transitions.java @@ -92,12 +92,12 @@ public class Transitions { public void fadeOutAndRemove(Node node, int duration, EventHandler handler) { FadeTransition fade = fadeOut(node, getDuration(duration)); fade.setInterpolator(Interpolator.EASE_IN); - fade.setOnFinished(actionEvent -> { + fade.setOnFinished(actionEvent -> UserThread.execute(() -> { ((Pane) (node.getParent())).getChildren().remove(node); //Profiler.printMsgWithTime("fadeOutAndRemove"); if (handler != null) handler.handle(actionEvent); - }); + })); } // Blur diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 5b6a81a493..ca16a6b0e4 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -112,6 +112,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static final int MAX_PERMITTED_MESSAGE_SIZE = 10 * 1024 * 1024; // 10 MB (425 offers resulted in about 660 kb, mailbox msg will add more to it) offer has usually 2 kb, mailbox 3kb. //TODO decrease limits again after testing private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(180); + private static final int MAX_CONNECTION_THREADS = 10; public static int getPermittedMessageSize() { return PERMITTED_MESSAGE_SIZE; @@ -130,6 +131,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { @Getter private final String uid; private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Connection.java executor-service")); + private final ExecutorService connectionThreadPool = Executors.newFixedThreadPool(MAX_CONNECTION_THREADS); + // holder of state shared between InputHandler and Connection @Getter private final Statistic statistic; @@ -429,12 +432,18 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { // Only receive non - CloseConnectionMessage network_messages @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { - checkArgument(connection.equals(this)); - if (networkEnvelope instanceof BundleOfEnvelopes) { - onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection); - } else { - UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection))); - } + Connection that = this; + connectionThreadPool.submit(new Runnable() { + @Override + public void run() { + checkArgument(connection.equals(that)); + if (networkEnvelope instanceof BundleOfEnvelopes) { + onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection); + } else { + messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)); + } + } + }); } private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) { @@ -466,8 +475,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { envelopesToProcess.add(networkEnvelope); } } - envelopesToProcess.forEach(envelope -> UserThread.execute(() -> - messageListeners.forEach(listener -> listener.onMessage(envelope, connection)))); + envelopesToProcess.forEach(envelope -> + messageListeners.forEach(listener -> listener.onMessage(envelope, connection))); } @@ -793,11 +802,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { return; } - // Throttle inbound network_messages + // Throttle inbound network messages long now = System.currentTimeMillis(); long elapsed = now - lastReadTimeStamp; if (elapsed < 10) { - log.debug("We got 2 network_messages received in less than 10 ms. We set the thread to sleep " + + log.info("We got 2 network messages received in less than 10 ms. We set the thread to sleep " + "for 20 ms to avoid getting flooded by our peer. lastReadTimeStamp={}, now={}, elapsed={}", lastReadTimeStamp, now, elapsed); Thread.sleep(20); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 6043f2dab2..736885fcc2 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1497,36 +1497,38 @@ message Trade { enum State { PB_ERROR_STATE = 0; PREPARATION = 1; - TAKER_PUBLISHED_TAKER_FEE_TX = 2; - MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 3; - MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 4; - MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 5; - MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 6; - TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 7; - TAKER_PUBLISHED_DEPOSIT_TX = 8; - TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 9; - TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 10; - TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 11; - TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 12; - TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 13; - MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 14; - MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 15; - DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 16; - BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 17; - BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 18; - BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 19; - BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 20; - BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 21; - SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 22; - SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 23; - SELLER_PUBLISHED_PAYOUT_TX = 24; - SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 25; - SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 26; - SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 27; - SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 28; - BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 29; - BUYER_SAW_PAYOUT_TX_IN_NETWORK = 30; - WITHDRAW_COMPLETED = 31; + CONTRACT_SIGNATURE_REQUESTED = 2; + CONTRACT_SIGNED = 3; + TAKER_PUBLISHED_TAKER_FEE_TX = 4; + MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 5; + MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 6; + MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 7; + MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 8; + TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9; + TAKER_PUBLISHED_DEPOSIT_TX = 10; + TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 11; + TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 12; + TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 13; + TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 14; + TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 15; + MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 16; + MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 17; + DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 18; + BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 19; + BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 20; + BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 21; + BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 22; + BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 23; + SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 24; + SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 25; + SELLER_PUBLISHED_PAYOUT_TX = 26; + SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 27; + SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 28; + SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 29; + SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 30; + BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 31; + BUYER_SAW_PAYOUT_TX_IN_NETWORK = 32; + WITHDRAW_COMPLETED = 33; } enum Phase { @@ -1654,9 +1656,8 @@ message ProcessModel { NodeAddress temp_trading_peer_node_address = 1006; string prepared_multisig_hex = 1007; string made_multisig_hex = 1008; - bool multisig_setup_complete = 1009; - bool maker_ready_to_fund_multisig = 1010; - bool multisig_deposit_initiated = 1011; + string multisig_address = 1009; + bool multisig_setup_complete = 1010; // TODO: remove this field } message TradingPeer {