diff --git a/common/src/main/java/bisq/common/taskrunner/Task.java b/common/src/main/java/bisq/common/taskrunner/Task.java index c8c8a1be0c..bb8bcefda6 100644 --- a/common/src/main/java/bisq/common/taskrunner/Task.java +++ b/common/src/main/java/bisq/common/taskrunner/Task.java @@ -17,8 +17,6 @@ package bisq.common.taskrunner; -import java.io.PrintWriter; -import java.io.StringWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +58,10 @@ public abstract class Task { taskHandler.handleComplete(); } + public boolean isCompleted() { + return completed; + } + protected void failed(String message) { appendToErrorMessage(message); failed(); diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index d6d752a42b..c9e8125fc0 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -23,7 +23,6 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; -import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import com.google.inject.name.Named; @@ -40,8 +39,6 @@ import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; -import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroTxWallet; @Singleton @Slf4j @@ -101,9 +98,7 @@ public class CoreDisputesService { // 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.exportMultisigHex(); - disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); + disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler); tradeManager.requestPersistence(); } } @@ -141,26 +136,26 @@ public class CoreDisputesService { isSupportTicket, SupportType.ARBITRATION); - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - return dispute; } } public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { try { - 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)); - + + // get winning dispute + Dispute winningDispute; Trade trade = tradeManager.getTrade(tradeId); + var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() + .filter(d -> tradeId.equals(d.getTradeId())) + .filter(d -> trade.getTradingPeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller())) + .findFirst(); + if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); + else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); + synchronized (trade) { var closeDate = new Date(); - var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); - var contract = dispute.getContract(); + var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); DisputePayout payout; if (customWinnerAmount > 0) { @@ -172,30 +167,28 @@ public class CoreDisputesService { } else { throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); } - applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); - - // apply dispute payout - applyDisputePayout(dispute, disputeResult, contract); + applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount); // close dispute ticket - closeDispute(arbitrationManager, dispute, disputeResult, false); + closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> { + arbitrationManager.requestPersistence(); - // 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()); - applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); - closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); - } else { - throw new IllegalStateException("could not find peer dispute"); - } - arbitrationManager.requestPersistence(); + // close peer's dispute ticket + var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() + .filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.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()); + closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> { + arbitrationManager.requestPersistence(); + }); + } else { + throw new IllegalStateException("could not find peer dispute"); + } + }); } } catch (Exception e) { throw new IllegalStateException(e); @@ -246,49 +239,13 @@ public class CoreDisputesService { } } - public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) { - // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) - if (!dispute.isMediationDispute()) { - try { - synchronized (tradeManager.getTrade(dispute.getTradeId())) { - System.out.println(disputeResult); - //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? - //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); - - // determine if dispute is in context of publisher - boolean isOpener = dispute.isOpener(); - boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER); - boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner; - - // open multisig wallet - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - - // if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx - if (isOpener) { - MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); - System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); - - // if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex - if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); - } - - // send arbitrator's updated multisig hex with dispute result - disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex()); - } - } catch (AddressFormatException e2) { - log.error("Error at close dispute", e2); - return; - } - } - } - // From DisputeSummaryWindow.java - public void closeDispute(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, boolean isRefundAgent) { + public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler) { dispute.setDisputeResult(disputeResult); dispute.setIsClosed(); DisputeResult.Reason reason = disputeResult.getReason(); - String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator"); + String role = Res.get("shared.arbitrator"); String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress(); Contract contract = dispute.getContract(); String currencyCode = contract.getOfferPayload().getCurrencyCode(); @@ -314,13 +271,8 @@ public class CoreDisputesService { } String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); - - if (isRefundAgent) { - summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); - } else { - summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); - } - disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText); + summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); + disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler); } public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { diff --git a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java index bd067cdb0c..3423238f6b 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java @@ -241,7 +241,6 @@ public final class CoreMoneroConnectionsService { else { boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri()); if (isLocal) { - updateDaemonInfo(); if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing } else { @@ -410,6 +409,7 @@ public final class CoreMoneroConnectionsService { private void startPollingDaemon() { if (updateDaemonLooper != null) updateDaemonLooper.stop(); + updateDaemonInfo(); updateDaemonLooper = new TaskLooper(() -> { updateDaemonInfo(); }); diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index 8441f31480..753bcad8ea 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -80,12 +80,16 @@ public class TradeInfo implements Payload { private final String phase; private final String periodState; private final String payoutState; + private final String disputeState; private final boolean isDepositPublished; + private final boolean isDepositConfirmed; private final boolean isDepositUnlocked; private final boolean isPaymentSent; private final boolean isPaymentReceived; - private final boolean isCompleted; private final boolean isPayoutPublished; + private final boolean isPayoutConfirmed; + private final boolean isPayoutUnlocked; + private final boolean isCompleted; private final String contractAsJson; private final ContractInfo contract; @@ -109,11 +113,15 @@ public class TradeInfo implements Payload { this.phase = builder.getPhase(); this.periodState = builder.getPeriodState(); this.payoutState = builder.getPayoutState(); + this.disputeState = builder.getDisputeState(); this.isDepositPublished = builder.isDepositPublished(); + this.isDepositConfirmed = builder.isDepositConfirmed(); this.isDepositUnlocked = builder.isDepositUnlocked(); this.isPaymentSent = builder.isPaymentSent(); this.isPaymentReceived = builder.isPaymentReceived(); this.isPayoutPublished = builder.isPayoutPublished(); + this.isPayoutConfirmed = builder.isPayoutConfirmed(); + this.isPayoutUnlocked = builder.isPayoutUnlocked(); this.isCompleted = builder.isCompleted(); this.contractAsJson = builder.getContractAsJson(); this.contract = builder.getContract(); @@ -161,11 +169,15 @@ public class TradeInfo implements Payload { .withPhase(trade.getPhase().name()) .withPeriodState(trade.getPeriodState().name()) .withPayoutState(trade.getPayoutState().name()) + .withDisputeState(trade.getDisputeState().name()) .withIsDepositPublished(trade.isDepositPublished()) + .withIsDepositConfirmed(trade.isDepositConfirmed()) .withIsDepositUnlocked(trade.isDepositUnlocked()) .withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentReceived(trade.isPaymentReceived()) .withIsPayoutPublished(trade.isPayoutPublished()) + .withIsPayoutConfirmed(trade.isPayoutConfirmed()) + .withIsPayoutUnlocked(trade.isPayoutUnlocked()) .withIsCompleted(trade.isCompleted()) .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) @@ -199,12 +211,16 @@ public class TradeInfo implements Payload { .setPhase(phase) .setPeriodState(periodState) .setPayoutState(payoutState) + .setDisputeState(disputeState) .setIsDepositPublished(isDepositPublished) + .setIsDepositConfirmed(isDepositConfirmed) .setIsDepositUnlocked(isDepositUnlocked) .setIsPaymentSent(isPaymentSent) .setIsPaymentReceived(isPaymentReceived) .setIsCompleted(isCompleted) .setIsPayoutPublished(isPayoutPublished) + .setIsPayoutConfirmed(isPayoutConfirmed) + .setIsPayoutUnlocked(isPayoutUnlocked) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) .build(); @@ -227,16 +243,20 @@ public class TradeInfo implements Payload { .withVolume(proto.getTradeVolume()) .withPeriodState(proto.getPeriodState()) .withPayoutState(proto.getPayoutState()) + .withDisputeState(proto.getDisputeState()) .withState(proto.getState()) .withPhase(proto.getPhase()) .withArbitratorNodeAddress(proto.getArbitratorNodeAddress()) .withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress()) .withIsDepositPublished(proto.getIsDepositPublished()) + .withIsDepositConfirmed(proto.getIsDepositConfirmed()) .withIsDepositUnlocked(proto.getIsDepositUnlocked()) .withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsCompleted(proto.getIsCompleted()) .withIsPayoutPublished(proto.getIsPayoutPublished()) + .withIsPayoutConfirmed(proto.getIsPayoutConfirmed()) + .withIsPayoutUnlocked(proto.getIsPayoutUnlocked()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) .build(); @@ -262,12 +282,16 @@ public class TradeInfo implements Payload { ", phase='" + phase + '\'' + "\n" + ", periodState='" + periodState + '\'' + "\n" + ", payoutState='" + payoutState + '\'' + "\n" + + ", disputeState='" + disputeState + '\'' + "\n" + ", isDepositPublished=" + isDepositPublished + "\n" + - ", isDepositConfirmed=" + isDepositUnlocked + "\n" + + ", isDepositConfirmed=" + isDepositConfirmed + "\n" + + ", isDepositUnlocked=" + isDepositUnlocked + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" + - ", isCompleted=" + isCompleted + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" + + ", isPayoutConfirmed=" + isPayoutConfirmed + "\n" + + ", isPayoutUnlocked=" + isPayoutUnlocked + "\n" + + ", isCompleted=" + isCompleted + "\n" + ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + diff --git a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java index ceeb3972d5..448e021dc0 100644 --- a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java @@ -52,11 +52,15 @@ public final class TradeInfoV1Builder { private String phase; private String periodState; private String payoutState; + private String disputeState; private boolean isDepositPublished; + private boolean isDepositConfirmed; private boolean isDepositUnlocked; private boolean isPaymentSent; private boolean isPaymentReceived; private boolean isPayoutPublished; + private boolean isPayoutConfirmed; + private boolean isPayoutUnlocked; private boolean isCompleted; private String contractAsJson; private ContractInfo contract; @@ -152,6 +156,11 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withDisputeState(String disputeState) { + this.disputeState = disputeState; + return this; + } + public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) { this.arbitratorNodeAddress = arbitratorNodeAddress; return this; @@ -167,6 +176,11 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withIsDepositConfirmed(boolean isDepositConfirmed) { + this.isDepositConfirmed = isDepositConfirmed; + return this; + } + public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) { this.isDepositUnlocked = isDepositUnlocked; return this; @@ -187,6 +201,16 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withIsPayoutConfirmed(boolean isPayoutConfirmed) { + this.isPayoutConfirmed = isPayoutConfirmed; + return this; + } + + public TradeInfoV1Builder withIsPayoutUnlocked(boolean isPayoutUnlocked) { + this.isPayoutUnlocked = isPayoutUnlocked; + return this; + } + public TradeInfoV1Builder withIsCompleted(boolean isCompleted) { this.isCompleted = isCompleted; return this; diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index d22c0a59d5..8b6dd093d0 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -95,17 +95,14 @@ public class Balances { } private void updatedBalances() { - // Need to delay a bit to get the balances correct - UserThread.execute(() -> { // TODO (woodser): running on user thread because JFX properties updated for legacy app - updateAvailableBalance(); - updatePendingBalance(); - updateReservedOfferBalance(); - updateReservedTradeBalance(); - updateReservedBalance(); - }); + updateAvailableBalance(); + updatePendingBalance(); + updateReservedOfferBalance(); + updateReservedTradeBalance(); + updateReservedBalance(); } - // TODO (woodser): balances being set as Coin from BigInteger.longValue(), which can lose precision. should be in centineros for consistency with the rest of the application + // TODO (woodser): converting to long should generally be avoided since can lose precision, but in practice these amounts are below max value private void updateAvailableBalance() { availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact())); 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 afc32160d0..c3839d4ca9 100644 --- a/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/bisq/core/btc/setup/MoneroWalletRpcManager.java @@ -68,9 +68,21 @@ public class MoneroWalletRpcManager { int numAttempts = 0; while (numAttempts < NUM_ALLOWED_ATTEMPTS) { int port = -1; + ServerSocket socket = null; try { numAttempts++; - port = registerPort(); + + // get port + if (startPort != null) port = registerNextPort(); + else { + socket = new ServerSocket(0); + port = socket.getLocalPort(); + synchronized (registeredPorts) { + registeredPorts.put(port, null); + } + } + + // start monero-wallet-rpc List cmdCopy = new ArrayList<>(cmd); // preserve original cmd cmdCopy.add(RPC_BIND_PORT_ARGUMENT); cmdCopy.add("" + port); @@ -84,6 +96,8 @@ public class MoneroWalletRpcManager { log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); throw e; } + } finally { + if (socket != null) socket.close(); // close socket if used } } throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here @@ -121,23 +135,12 @@ public class MoneroWalletRpcManager { walletRpc.stopProcess(); } - private int registerPort() throws IOException { + private int registerNextPort() 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 auto-assigned port - else { - int port = getLocalPort(); - registeredPorts.put(port, null); - return port; - } } } @@ -146,11 +149,4 @@ public class MoneroWalletRpcManager { 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(); - return 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 c05b97806f..3b6dd54bd6 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -86,7 +86,7 @@ public class XmrWalletService { private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_"; private static final int MINER_FEE_PADDING_MULTIPLIER = 2; // extra padding for miner fees = estimated fee * multiplier - private static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of expected fee + private static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee private final CoreAccountService accountService; private final CoreMoneroConnectionsService connectionsService; @@ -103,6 +103,7 @@ public class XmrWalletService { private Map multisigWallets; private Map walletLocks = new HashMap(); private final Map> txCache = new HashMap>(); + private boolean isShutDown = false; @Inject XmrWalletService(CoreAccountService accountService, @@ -193,15 +194,15 @@ public class XmrWalletService { public boolean multisigWalletExists(String tradeId) { initWalletLock(tradeId); - synchronized(walletLocks.get(tradeId)) { + synchronized (walletLocks.get(tradeId)) { return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); } } public MoneroWallet createMultisigWallet(String tradeId) { + log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId); - synchronized(walletLocks.get(tradeId)) { - log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); + synchronized (walletLocks.get(tradeId)) { if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port @@ -212,8 +213,9 @@ public class XmrWalletService { // TODO (woodser): provide progress notifications during open? public MoneroWallet getMultisigWallet(String tradeId) { + if (isShutDown) throw new RuntimeException(getClass().getName() + " is shut down"); initWalletLock(tradeId); - synchronized(walletLocks.get(tradeId)) { + synchronized (walletLocks.get(tradeId)) { if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId); @@ -229,9 +231,9 @@ public class XmrWalletService { } public void closeMultisigWallet(String tradeId) { + log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId); - synchronized(walletLocks.get(tradeId)) { - log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); + synchronized (walletLocks.get(tradeId)) { if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); MoneroWallet wallet = multisigWallets.remove(tradeId); closeWallet(wallet, true); @@ -239,9 +241,9 @@ public class XmrWalletService { } public boolean deleteMultisigWallet(String tradeId) { + log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId); - synchronized(walletLocks.get(tradeId)) { - log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); + synchronized (walletLocks.get(tradeId)) { String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; if (!walletExists(walletName)) return false; if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); @@ -253,7 +255,7 @@ public class XmrWalletService { public MoneroTxWallet createTx(List destinations) { try { MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); - printTxs("XmrWalletService.createTx", tx); + //printTxs("XmrWalletService.createTx", tx); return tx; } catch (Exception e) { throw e; @@ -268,7 +270,7 @@ public class XmrWalletService { * @param tradeFee - trade fee * @param depositAmount - amount needed for the trade minus the trade fee * @param returnAddress - return address for deposit amount - * @param addPadding - reserve extra padding for miner fee fluctuations + * @param addPadding - reserve additional padding to cover future mining fee * @return a transaction to reserve a trade */ public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount, boolean addPadding) { @@ -278,14 +280,26 @@ public class XmrWalletService { // add miner fee padding to deposit amount if (addPadding) { - // get expected mining fee + // get estimated mining fee with deposit amount MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig() .setAccountIndex(0) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(returnAddress, depositAmount)); BigInteger feeEstimate = feeEstimateTx.getFee(); - // add extra padding to deposit amount + BigInteger daemonFeeEstimate = getFeeEstimate(feeEstimateTx.getWeight()); + log.info("createReserveTx() 1st feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate))); + + // get estimated mining fee with deposit amount + previous estimated mining fee for better accuracy + feeEstimateTx = wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) + .addDestination(returnAddress, depositAmount.add(feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER))))); + feeEstimate = feeEstimateTx.getFee(); + + log.info("createReserveTx() 2nd feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate))); + + // add padding to deposit amount BigInteger minerFeePadding = feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)); depositAmount = depositAmount.add(minerFeePadding); } @@ -295,11 +309,11 @@ public class XmrWalletService { .setAccountIndex(0) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(returnAddress, depositAmount)); + log.info("Reserve tx weight={}, fee={}, depositAmount={}", reserveTx.getWeight(), reserveTx.getFee(), depositAmount); // freeze inputs for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); wallet.save(); - return reserveTx; } } @@ -343,12 +357,13 @@ public class XmrWalletService { * @param txHex is the transaction hex * @param txKey is the transaction key * @param keyImages are expected key images of inputs, ignored if null - * @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase + * @param addPadding verifies depositAmount has additional padding to cover future mining fee */ - public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List keyImages, boolean miningFeePadding) { + public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List keyImages, boolean addPadding) { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); try { + log.info("Verifying trade tx with deposit amount={}", depositAmount); // verify tx not submitted to pool MoneroTx tx = daemon.getTx(txHash); @@ -379,12 +394,18 @@ public class XmrWalletService { BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Mining fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee()); + log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff); // verify deposit amount check = wallet.checkTxKey(txHash, txKey, depositAddress); if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); - if (miningFeePadding) depositAmount = depositAmount.add(feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER))); // prove reserve of at least deposit amount + miner fee padding - if (check.getReceivedAmount().compareTo(depositAmount) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount()); + if (addPadding) { + BigInteger minPadding = BigInteger.valueOf((long) (tx.getFee().multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)).doubleValue() * (1.0 - MINER_FEE_TOLERANCE))); + BigInteger actualPadding = check.getReceivedAmount().subtract(depositAmount); + if (actualPadding.compareTo(minPadding) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount.add(minPadding) + " (with padding) but was " + check.getReceivedAmount()); + } else if (check.getReceivedAmount().compareTo(depositAmount) < 0) { + throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount()); + } } finally { try { daemon.flushTxPool(txHash); // flush tx from pool @@ -405,11 +426,12 @@ public class XmrWalletService { // get fee estimates per kB from daemon MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); - BigInteger baseFeeRate = feeEstimates.getFee(); // get normal fee per kB + BigInteger baseFeeEstimate = feeEstimates.getFee(); // get normal fee per kB BigInteger qmask = feeEstimates.getQuantizationMask(); + log.info("Monero base fee estimate={}, qmask={}: " + baseFeeEstimate, qmask); // get tx base fee - BigInteger baseFee = baseFeeRate.multiply(BigInteger.valueOf(txWeight)); + BigInteger baseFee = baseFeeEstimate.multiply(BigInteger.valueOf(txWeight)); // round up to multiple of quantization mask BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask); @@ -468,6 +490,7 @@ public class XmrWalletService { } public void shutDown() { + this.isShutDown = true; closeAllWallets(); } @@ -573,7 +596,7 @@ public class XmrWalletService { // start syncing wallet in background new Thread(() -> { - log.info("Syncing wallet " + config.getPath() + " in background"); + log.info("Starting background syncing for wallet " + config.getPath()); walletRpc.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); log.info("Done starting background sync for wallet " + config.getPath()); }).start(); @@ -645,47 +668,41 @@ public class XmrWalletService { } private void changeWalletPasswords(String oldPassword, String newPassword) { - 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 - public void run() { - try { - wallet.changePassword(oldPassword, newPassword); - saveWallet(wallet); - } catch (Exception e) { - e.printStackTrace(); - throw e; - } + + // create task to change main wallet password + List tasks = new ArrayList(); + tasks.add(() -> { + try { + wallet.changePassword(oldPassword, newPassword); + saveWallet(wallet); + } catch (Exception e) { + e.printStackTrace(); + throw e; } }); + + // create tasks to change multisig wallet passwords + List tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList()); for (String tradeId : tradeIds) { - pool.submit(new Runnable() { - @Override - public void run() { - MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open - if (multisigWallet == null) return; - multisigWallet.changePassword(oldPassword, newPassword); - saveWallet(multisigWallet); - } + tasks.add(() -> { + MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open + if (multisigWallet == null) return; + multisigWallet.changePassword(oldPassword, newPassword); + saveWallet(multisigWallet); }); } - pool.shutdown(); - try { - if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); - } catch (InterruptedException e) { - try { pool.shutdownNow(); } - catch (Exception e2) { } - throw new RuntimeException(e); - } + + // excute tasks in parallel + HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.size())); } private void closeWallet(MoneroWallet walletRpc, boolean save) { log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save); MoneroError err = null; try { - if (save) saveWallet(walletRpc); - walletRpc.close(); + String path = walletRpc.getPath(); + walletRpc.close(save); + if (save) backupWallet(path); } catch (MoneroError e) { err = e; } @@ -721,7 +738,7 @@ public class XmrWalletService { log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); } }); - HavenoUtils.awaitTasks(tasks); + HavenoUtils.executeTasks(tasks); // clear wallets wallet = null; diff --git a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java index 065e6e7253..f772fa7299 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java @@ -43,6 +43,7 @@ public class TradeEvents { private final PubKeyRingProvider pubKeyRingProvider; private final TradeManager tradeManager; private final MobileNotificationService mobileNotificationService; + private boolean isInitialized = false; @Inject public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) { @@ -59,10 +60,11 @@ public class TradeEvents { } }); tradeManager.getObservableList().forEach(this::setTradePhaseListener); + isInitialized = true; } private void setTradePhaseListener(Trade trade) { - log.info("We got a new trade. id={}", trade.getId()); + if (isInitialized) log.info("We got a new trade. id={}", trade.getId()); if (!trade.isPayoutPublished()) { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { String msg = null; diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 32b4a4b975..67ff62eea5 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -117,14 +117,14 @@ public class CreateOfferService { double buyerSecurityDepositAsDouble, PaymentAccount paymentAccount) { - log.info("create and get offer with offerId={}, \n" + - "currencyCode={}, \n" + - "direction={}, \n" + - "price={}, \n" + - "useMarketBasedPrice={}, \n" + - "marketPriceMargin={}, \n" + - "amount={}, \n" + - "minAmount={}, \n" + + log.info("create and get offer with offerId={}, " + + "currencyCode={}, " + + "direction={}, " + + "price={}, " + + "useMarketBasedPrice={}, " + + "marketPriceMargin={}, " + + "amount={}, " + + "minAmount={}, " + "buyerSecurityDeposit={}", offerId, currencyCode, diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index afdf196a2c..831f8ede59 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -675,6 +675,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // handle unscheduled offer if (openOffer.getScheduledTxHashes() == null) { + log.info("Scheduling offer " + openOffer.getId()); // check for sufficient balance - scheduled offers amount if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount()).compareTo(offerReserveAmount) < 0) { @@ -743,6 +744,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Coin offerReserveAmount, // TODO: switch to BigInteger boolean useSavingsWallet, // TODO: remove this TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + log.info("Signing and posting offer " + openOffer.getId()); // create model PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(), diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java index ea516d220b..e7b74694c6 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java @@ -99,7 +99,6 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp .setTakersTradePrice(takersTradePrice) .setIsTakerApiUser(isTakerApiUser) .setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest()); - Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java index 8530ec5adb..d4ad9adfae 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java @@ -62,7 +62,6 @@ public class PlaceOfferProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void placeOffer() { - log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId()); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); @@ -96,10 +95,12 @@ public class PlaceOfferProtocol { // ignore if timer already stopped if (timeoutTimer == null) { - log.warn("Ignoring sign offer response from arbitrator because timeout has expired"); + log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOffer().getId()); return; } + // reset timer + stopTimeoutTimer(); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); }, TradeProtocol.TRADE_TIMEOUT); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 6cc19109b8..a6ab4b4c54 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -23,12 +23,15 @@ import bisq.core.btc.model.XmrAddressEntry; import bisq.core.offer.Offer; import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.util.ParsingUtils; +import lombok.extern.slf4j.Slf4j; + import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; +@Slf4j public class MakerReserveOfferFunds extends Task { public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) { @@ -47,6 +50,7 @@ public class MakerReserveOfferFunds extends Task { String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); + log.info("Maker creating reserve tx with maker fee={} and depositAmount={}", makerFee, depositAmount); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount, true); // collect reserved key images // TODO (woodser): switch to proof of reserve? diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index edecb72ff0..d5edd1e8c7 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -79,8 +79,7 @@ public class MakerSendSignOfferRequest extends Task { sendSignOfferRequests(request, () -> { complete(); }, (errorMessage) -> { - log.warn("Error signing offer: " + errorMessage); - appendToErrorMessage("Error signing offer: " + errorMessage); + appendToErrorMessage("Error signing offer " + request.getOfferId() + ": " + errorMessage); failed(errorMessage); }); } catch (Throwable t) { @@ -94,7 +93,7 @@ public class MakerSendSignOfferRequest extends Task { private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager()); if (leastUsedArbitrator == null) { - errorMessageHandler.handleErrorMessage("Could not get least used arbitrator"); + errorMessageHandler.handleErrorMessage("Could not get least used arbitrator to send " + request.getClass().getSimpleName() + " for offer " + request.getOfferId()); return; } sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet(), resultHandler, errorMessageHandler); @@ -102,7 +101,7 @@ public class MakerSendSignOfferRequest extends Task { private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - // complete on successful ack message + // complete on successful ack message, fail on first nack DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() { @Override public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) { @@ -117,8 +116,7 @@ public class MakerSendSignOfferRequest extends Task { model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED); resultHandler.handleResult(); } else { - log.warn("Arbitrator nacked request: {}", errorMessage); - handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler); + errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); } } }; @@ -135,7 +133,14 @@ public class MakerSendSignOfferRequest extends Task { @Override public void onFault(String errorMessage) { log.warn("Arbitrator unavailable: {}", errorMessage); - handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler); + excludedArbitrators.add(arbitratorNodeAddress); + Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators); + if (altArbitrator == null) { + errorMessageHandler.handleErrorMessage("Offer " + request.getOfferId() + " could not be signed by any arbitrator"); + return; + } + log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); + sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); } }); } @@ -156,15 +161,4 @@ public class MakerSendSignOfferRequest extends Task { listener ); } - - private void handleArbitratorFailure(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - excludedArbitrators.add(arbitratorNodeAddress); - Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators); - if (altArbitrator == null) { - errorMessageHandler.handleErrorMessage("Offer could not be signed by any arbitrator"); - return; - } - log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); - sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); - } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index 17f7efb3b9..92ad49d61a 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -47,7 +47,7 @@ public class PaymentAccountUtil { public static boolean isAnyPaymentAccountValidForOffer(Offer offer, Collection paymentAccounts) { - for (PaymentAccount paymentAccount : paymentAccounts) { + for (PaymentAccount paymentAccount : new ArrayList(paymentAccounts)) { if (isPaymentAccountValidForOffer(offer, paymentAccount)) return true; } diff --git a/core/src/main/java/bisq/core/presentation/BalancePresentation.java b/core/src/main/java/bisq/core/presentation/BalancePresentation.java index 7488828172..deae1b53af 100644 --- a/core/src/main/java/bisq/core/presentation/BalancePresentation.java +++ b/core/src/main/java/bisq/core/presentation/BalancePresentation.java @@ -17,6 +17,7 @@ package bisq.core.presentation; +import bisq.common.UserThread; import bisq.core.btc.Balances; import javax.inject.Inject; @@ -43,13 +44,13 @@ public class BalancePresentation { @Inject public BalancePresentation(Balances balances) { balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> { - availableBalance.set(longToXmr(newValue.value)); + UserThread.execute(() -> availableBalance.set(longToXmr(newValue.value))); }); balances.getPendingBalance().addListener((observable, oldValue, newValue) -> { - pendingBalance.set(longToXmr(newValue.value)); + UserThread.execute(() -> pendingBalance.set(longToXmr(newValue.value))); }); balances.getReservedBalance().addListener((observable, oldValue, newValue) -> { - reservedBalance.set(longToXmr(newValue.value)); + UserThread.execute(() -> reservedBalance.set(longToXmr(newValue.value))); }); } diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index bb8acd88e3..bdf943e905 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -29,13 +29,9 @@ import bisq.core.offer.messages.SignOfferRequest; import bisq.core.offer.messages.SignOfferResponse; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; -import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; import bisq.core.support.dispute.mediation.mediator.Mediator; -import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest; -import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse; -import bisq.core.support.dispute.messages.DisputeResultMessage; -import bisq.core.support.dispute.messages.OpenNewDisputeMessage; -import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.messages.DisputeClosedMessage; +import bisq.core.support.dispute.messages.DisputeOpenedMessage; import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.messages.PaymentSentMessage; @@ -170,20 +166,12 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion); - case OPEN_NEW_DISPUTE_MESSAGE: - return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion); - case PEER_OPENED_DISPUTE_MESSAGE: - return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion); + case DISPUTE_OPENED_MESSAGE: + return DisputeOpenedMessage.fromProto(proto.getDisputeOpenedMessage(), this, messageVersion); + case DISPUTE_CLOSED_MESSAGE: + return DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), messageVersion); case CHAT_MESSAGE: return ChatMessage.fromProto(proto.getChatMessage(), messageVersion); - case DISPUTE_RESULT_MESSAGE: - return DisputeResultMessage.fromProto(proto.getDisputeResultMessage(), messageVersion); - case PEER_PUBLISHED_DISPUTE_PAYOUT_TX_MESSAGE: - return PeerPublishedDisputePayoutTxMessage.fromProto(proto.getPeerPublishedDisputePayoutTxMessage(), messageVersion); - case ARBITRATOR_PAYOUT_TX_REQUEST: - return ArbitratorPayoutTxRequest.fromProto(proto.getArbitratorPayoutTxRequest(), this, messageVersion); - case ARBITRATOR_PAYOUT_TX_RESPONSE: - return ArbitratorPayoutTxResponse.fromProto(proto.getArbitratorPayoutTxResponse(), this, messageVersion); case PRIVATE_NOTIFICATION_MESSAGE: return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion); diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index 2f7a56181a..42e98829fa 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -144,7 +144,7 @@ public abstract class SupportManager { // Message handler /////////////////////////////////////////////////////////////////////////////////////////// - protected void onChatMessage(ChatMessage chatMessage) { + protected void handleChatMessage(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); @@ -152,7 +152,7 @@ public abstract class SupportManager { if (!channelOpen) { log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { - Timer timer = UserThread.runAfter(() -> onChatMessage(chatMessage), 1); + Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1); delayMsgMap.put(uid, timer); } else { String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; 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 d7542469ab..d9feae5fcf 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -31,15 +31,19 @@ import bisq.core.offer.OpenOfferManager; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportManager; -import bisq.core.support.dispute.messages.DisputeResultMessage; -import bisq.core.support.dispute.messages.OpenNewDisputeMessage; -import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.DisputeResult.Winner; +import bisq.core.support.dispute.messages.DisputeClosedMessage; +import bisq.core.support.dispute.messages.DisputeOpenedMessage; import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Contract; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeManager; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.util.ParsingUtils; import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; @@ -63,8 +67,9 @@ import javafx.beans.property.IntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.math.BigInteger; import java.security.KeyPair; - +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; @@ -75,6 +80,10 @@ import java.util.stream.Collectors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroError; +import monero.wallet.MoneroWallet; +import monero.wallet.model.MoneroTxConfig; +import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; @@ -82,8 +91,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import monero.wallet.MoneroWallet; - @Slf4j public abstract class DisputeManager> extends SupportManager { protected final TradeWalletService tradeWalletService; @@ -197,12 +204,10 @@ public abstract class DisputeManager> extends Sup /////////////////////////////////////////////////////////////////////////////////////////// // We get that message at both peers. The dispute object is in context of the trader - public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage); + public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage); public abstract NodeAddress getAgentNodeAddress(Dispute dispute); - protected abstract Trade.DisputeState getDisputeStateStartedByPeer(); - public abstract void cleanupDisputes(); protected abstract String getDisputeInfo(Dispute dispute); @@ -299,157 +304,26 @@ public abstract class DisputeManager> extends Sup } /////////////////////////////////////////////////////////////////////////////////////////// - // Message handler + // Dispute handling /////////////////////////////////////////////////////////////////////////////////////////// - // arbitrator receives that from trader who opens dispute - protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { - T disputeList = getDisputeList(); - if (disputeList == null) { - log.warn("disputes is null"); - return; - } + // trader sends message to arbitrator to open dispute + public void sendDisputeOpenedMessage(Dispute dispute, + boolean reOpen, + String updatedMultisigHex, + ResultHandler resultHandler, + FaultHandler faultHandler) { - 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 - + // get trade Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", dispute.getTradeId()); return; } - synchronized (trade) { - - String errorMessage = null; - Contract contract = dispute.getContract(); - addPriceInfoMessage(dispute, 0); - - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - if (isAgent(dispute)) { - - // update arbitrator's multisig wallet - trade.syncWallet(); - trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex()); - trade.saveWallet(); - log.info("Arbitrator multisig wallet updated on new dispute message for trade " + 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 = "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); - } - - addMediationResultMessage(dispute); - - try { - TradeDataValidation.validatePaymentAccountPayload(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(); - } - } - - // Not-dispute-requester receives that msg from dispute agent - protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { - T disputeList = getDisputeList(); - if (disputeList == null) { - log.warn("disputes is null"); - return; - } - - String errorMessage = null; - Dispute dispute = peerOpenedDisputeMessage.getDispute(); - log.info("{}.onPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); - - Optional optionalTrade = tradeManager.getOpenTrade(dispute.getTradeId()); - if (!optionalTrade.isPresent()) { - return; - } - Trade trade = optionalTrade.get(); - - 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 = "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); - } - - sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); - requestPersistence(); - } - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Send message - /////////////////////////////////////////////////////////////////////////////////////////// - - public void sendOpenNewDisputeMessage(Dispute dispute, - boolean reOpen, - String updatedMultisigHex, - ResultHandler resultHandler, - FaultHandler faultHandler) { - log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); + log.info("Sending {} for {} {}, dispute {}", + DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(), + dispute.getTradeId(), dispute.getId()); T disputeList = getDisputeList(); if (disputeList == null) { @@ -469,8 +343,8 @@ public abstract class DisputeManager> extends Sup 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); + Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : + Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); ChatMessage chatMessage = new ChatMessage( getSupportType(), @@ -486,31 +360,33 @@ public abstract class DisputeManager> extends Sup } NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); - OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, + DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType(), - updatedMultisigHex); + updatedMultisigHex, + trade.getBuyer().getPaymentSentMessage()); log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid()); mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, dispute.getAgentPubKeyRing(), - openNewDisputeMessage, + disputeOpenedMessage, 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.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.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); + trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); requestPersistence(); resultHandler.handleResult(); } @@ -519,13 +395,14 @@ public abstract class DisputeManager> extends Sup public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.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); + trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); requestPersistence(); resultHandler.handleResult(); } @@ -533,9 +410,9 @@ public abstract class DisputeManager> extends Sup @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.uid={}, errorMessage={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid(), errorMessage); // We use the chatMessage wrapped inside the openNewDisputeMessage for @@ -545,8 +422,7 @@ public abstract class DisputeManager> extends Sup 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(); @@ -558,10 +434,111 @@ public abstract class DisputeManager> extends Sup requestPersistence(); } - // Dispute agent sends that to trading peer when he received openDispute request - private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, + // arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator + protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) { + Dispute dispute = message.getDispute(); + log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); + + // intialize + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + dispute.setSupportType(message.getSupportType()); + dispute.setState(Dispute.State.NEW); // TODO: unused, remove? + Contract contract = dispute.getContract(); + + // validate dispute + try { + TradeDataValidation.validatePaymentAccountPayload(dispute); + TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx()); + //TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed? + TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config); + TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config); + } catch (TradeDataValidation.AddressException | + TradeDataValidation.NodeAddressException | + TradeDataValidation.InvalidPaymentAccountPayloadException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + + // get trade + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } + + // get sender + PubKeyRing senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing(); + TradingPeer sender = trade.getTradingPeer(senderPubKeyRing); + if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); + + // message to trader is expected from arbitrator + if (!trade.isArbitrator() && sender != trade.getArbitrator()) { + throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator"); + } + + // arbitrator verifies signature of payment sent message if given + if (trade.isArbitrator() && message.getPaymentSentMessage() != null) { + HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage()); + trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex()); + trade.setStateIfProgress(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + } + + // update multisig hex + if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + + // update peer node address + // TODO: tests can reuse the same addresses so nullify equal peer + sender.setNodeAddress(message.getSenderNodeAddress()); + + // add chat message with price info + if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); + + // add dispute + String errorMessage = null; + synchronized (disputeList) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + disputeList.add(dispute); + trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); + + // send dispute opened message to peer if arbitrator + if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); + 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 that we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } + } + + // use chat message instead of open dispute message for the ack + ObservableList messages = message.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage); + } + + // add chat message with mediation info if applicable // TODO: not applicable in haveno + addMediationResultMessage(dispute); + + requestPersistence(); + } + + // arbitrator sends dispute opened message to opener's peer + private void sendDisputeOpenedMessageToPeer(Dispute disputeFromOpener, Contract contractFromOpener, - PubKeyRing pubKeyRing) { + PubKeyRing pubKeyRing, + String updatedMultisigHex) { 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 @@ -569,13 +546,15 @@ public abstract class DisputeManager> extends Sup // from the code below. UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener, contractFromOpener, - pubKeyRing), + pubKeyRing, + updatedMultisigHex), 100, TimeUnit.MILLISECONDS); } private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, - PubKeyRing pubKeyRing) { + PubKeyRing pubKeyRing, + String updatedMultisigHex) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); @@ -638,14 +617,23 @@ public abstract class DisputeManager> extends Sup disputeList.add(dispute); } + // get trade + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } + // We mirrored dispute already! Contract contract = dispute.getContract(); PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); - PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, + DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute, p2PService.getAddress(), UUID.randomUUID().toString(), - getSupportType()); + getSupportType(), + updatedMultisigHex, + trade.getSelf().getPaymentSentMessage()); log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, @@ -701,8 +689,8 @@ public abstract class DisputeManager> extends Sup requestPersistence(); } - // arbitrator send result to trader - public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) { + // arbitrator sends result to trader when their dispute is closed + public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); @@ -720,75 +708,114 @@ public abstract class DisputeManager> extends Sup disputeResult.setChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage); - NodeAddress peersNodeAddress; - Contract contract = dispute.getContract(); - if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) - peersNodeAddress = contract.getBuyerNodeAddress(); - else - peersNodeAddress = contract.getSellerNodeAddress(); - DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult, + // get trade + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } + + // create unsigned dispute payout tx if not already published and arbitrator has trader's updated multisig info + TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing()); + if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) { + + // import multisig hex + MoneroWallet multisigWallet = trade.getWallet(); + List updatedMultisigHexes = new ArrayList(); + if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex()); + if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex()); + if (!updatedMultisigHexes.isEmpty()) { + multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually + trade.syncWallet(); + trade.saveWallet(); + } + + // create unsigned dispute payout tx + if (!trade.isPayoutPublished()) { + log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId()); + try { + MoneroTxWallet payoutTx = createDisputePayoutTx(trade, dispute, disputeResult, multisigWallet); + trade.setPayoutTx(payoutTx); + trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } catch (Exception e) { + if (!trade.isPayoutPublished()) throw e; + } + } + } + + // create dispute closed message + String unsignedPayoutTxHex = receiver.getUpdatedMultisigHex() == null ? null : trade.getPayoutTxHex(); + TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); + boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ; + DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, p2PService.getAddress(), UUID.randomUUID().toString(), - getSupportType()); - log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(), - disputeResultMessage.getUid(), chatMessage.getUid()); - mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, + getSupportType(), + trade.getSelf().getUpdatedMultisigHex(), + trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info + deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently + + // send dispute closed message + log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), + disputeClosedMessage.getUid(), chatMessage.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), dispute.getTraderPubKeyRing(), - disputeResultMessage, + disputeClosedMessage, new SendMailboxMessageListener() { @Override public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " + + log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " + "chatMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), chatMessage.getUid()); - // TODO: hack to sync wallet after dispute message received in order to detect payout published - Trade trade = tradeManager.getTrade(dispute.getTradeId()); - long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); - for (int i = 0; i < 3; i++) { - UserThread.runAfter(() -> { - if (!trade.isPayoutUnlocked()) trade.syncWallet(); - }, defaultRefreshPeriod / 1000 * (i + 1)); - } - - // We use the chatMessage wrapped inside the disputeResultMessage for + // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setArrived(true); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); + trade.syncWalletNormallyForMs(30000); requestPersistence(); + resultHandler.handleResult(); } @Override public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " + + log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + "chatMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), chatMessage.getUid()); - // We use the chatMessage wrapped inside the disputeResultMessage for + // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setStoredInMailbox(true); + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); requestPersistence(); + resultHandler.handleResult(); } @Override public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " + + log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + "chatMessage.uid={}, errorMessage={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), chatMessage.getUid(), errorMessage); - // We use the chatMessage wrapped inside the disputeResultMessage for + // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setSendMessageError(errorMessage); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); requestPersistence(); + resultHandler.handleResult(); } } ); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); requestPersistence(); } @@ -796,6 +823,52 @@ public abstract class DisputeManager> extends Sup // Utils /////////////////////////////////////////////////////////////////////////////////////////// + private MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) { + + // multisig wallet must be synced + if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId()); + + // collect winner and loser payout address and amounts + Contract contract = dispute.getContract(); + String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ? + (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : + (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); + String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); + BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); + BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); + + // create transaction to get fee estimate + MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); + if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx + if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); + MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig); + + // create payout tx by increasing estimated fee until successful + MoneroTxWallet payoutTx = null; + int numAttempts = 0; + while (payoutTx == null && numAttempts < 50) { + BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful + txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); + if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 + if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) { + if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee"); + if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee + } + numAttempts++; + try { + payoutTx = multisigWallet.createTx(txConfig); + } catch (MoneroError e) { + // exception expected // TODO: better way of estimating fee? + } + } + if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts"); + log.info("Dispute payout transaction generated on attempt {}", numAttempts); + + // save updated multisig hex + trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); + return payoutTx; + } + private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { PubKeyRing receiverPubKeyRing = null; NodeAddress peerNodeAddress = null; @@ -878,15 +951,15 @@ public abstract class DisputeManager> extends Sup // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. if (dispute.getMediatorsDisputeResult() != null) { String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); - ChatMessage mediatorsDisputeResultMessage = new ChatMessage( + ChatMessage mediatorsDisputeClosedMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), pubKeyRing.hashCode(), false, mediatorsDisputeResult, p2PService.getAddress()); - mediatorsDisputeResultMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); + mediatorsDisputeClosedMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(mediatorsDisputeClosedMessage); requestPersistence(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java index bea02ecabb..21ae3ccc80 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -23,8 +23,6 @@ import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkPayload; import bisq.common.util.Utilities; -import com.google.protobuf.ByteString; - import org.bitcoinj.core.Coin; import javafx.beans.property.BooleanProperty; @@ -89,16 +87,6 @@ public final class DisputeResult implements NetworkPayload { @Nullable private byte[] arbitratorPubKey; private long closeDate; - @Setter - private boolean isLoserPublisher; - - // added for XMR integration - @Nullable - @Setter - String arbitratorSignedPayoutTxHex; - @Nullable - @Setter - String arbitratorUpdatedMultisigHex; public DisputeResult(String tradeId, int traderId) { this.tradeId = tradeId; @@ -115,13 +103,10 @@ public final class DisputeResult implements NetworkPayload { String summaryNotes, @Nullable ChatMessage chatMessage, @Nullable byte[] arbitratorSignature, - @Nullable String arbitratorPayoutTxSigned, - @Nullable String arbitratorUpdatedMultisigHex, long buyerPayoutAmount, long sellerPayoutAmount, @Nullable byte[] arbitratorPubKey, - long closeDate, - boolean isLoserPublisher) { + long closeDate) { this.tradeId = tradeId; this.traderId = traderId; this.winner = winner; @@ -132,13 +117,10 @@ public final class DisputeResult implements NetworkPayload { this.summaryNotesProperty.set(summaryNotes); this.chatMessage = chatMessage; this.arbitratorSignature = arbitratorSignature; - this.arbitratorSignedPayoutTxHex = arbitratorPayoutTxSigned; - this.arbitratorUpdatedMultisigHex = arbitratorUpdatedMultisigHex; this.buyerPayoutAmount = buyerPayoutAmount; this.sellerPayoutAmount = sellerPayoutAmount; this.arbitratorPubKey = arbitratorPubKey; this.closeDate = closeDate; - this.isLoserPublisher = isLoserPublisher; } @@ -157,13 +139,10 @@ public final class DisputeResult implements NetworkPayload { proto.getSummaryNotes(), proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getArbitratorSignature().toByteArray(), - ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignedPayoutTxHex()), - ProtoUtil.stringOrNullFromProto(proto.getArbitratorUpdatedMultisigHex()), proto.getBuyerPayoutAmount(), proto.getSellerPayoutAmount(), proto.getArbitratorPubKey().toByteArray(), - proto.getCloseDate(), - proto.getIsLoserPublisher()); + proto.getCloseDate()); } @Override @@ -178,13 +157,8 @@ public final class DisputeResult implements NetworkPayload { .setSummaryNotes(summaryNotesProperty.get()) .setBuyerPayoutAmount(buyerPayoutAmount) .setSellerPayoutAmount(sellerPayoutAmount) - .setCloseDate(closeDate) - .setIsLoserPublisher(isLoserPublisher); + .setCloseDate(closeDate); - Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); - Optional.ofNullable(arbitratorSignedPayoutTxHex).ifPresent(arbitratorPayoutTxSigned -> builder.setArbitratorSignedPayoutTxHex(arbitratorPayoutTxSigned)); - Optional.ofNullable(arbitratorUpdatedMultisigHex).ifPresent(arbitratorUpdatedMultisigHex -> builder.setArbitratorUpdatedMultisigHex(arbitratorUpdatedMultisigHex)); - Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); Optional.ofNullable(chatMessage).ifPresent(chatMessage -> builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); @@ -265,13 +239,10 @@ public final class DisputeResult implements NetworkPayload { ",\n summaryNotesProperty=" + summaryNotesProperty + ",\n chatMessage=" + chatMessage + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + - ",\n arbitratorPayoutTxSigned=" + arbitratorSignedPayoutTxHex + - ",\n arbitratorUpdatedMultisigHex=" + arbitratorUpdatedMultisigHex + ",\n buyerPayoutAmount=" + buyerPayoutAmount + ",\n sellerPayoutAmount=" + sellerPayoutAmount + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n closeDate=" + closeDate + - ",\n isLoserPublisher=" + isLoserPublisher + "\n}"; } } 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 f60c67aaae..c70bebe66a 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 @@ -22,7 +22,6 @@ import bisq.core.api.CoreNotificationService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; -import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; @@ -30,17 +29,12 @@ import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult.Winner; -import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; -import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest; -import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse; -import bisq.core.support.dispute.messages.DisputeResultMessage; -import bisq.core.support.dispute.messages.OpenNewDisputeMessage; -import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.messages.DisputeClosedMessage; +import bisq.core.support.dispute.messages.DisputeOpenedMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Contract; -import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.util.ParsingUtils; @@ -48,24 +42,20 @@ import bisq.core.util.ParsingUtils; import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; -import bisq.network.p2p.SendDirectMessageListener; -import bisq.network.p2p.SendMailboxMessageListener; - +import common.utils.GenUtils; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import bisq.common.crypto.PubKeyRing; import com.google.inject.Inject; import com.google.inject.Singleton; import java.math.BigInteger; - +import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.UUID; import lombok.extern.slf4j.Slf4j; @@ -73,11 +63,9 @@ import static com.google.common.base.Preconditions.checkNotNull; -import monero.common.MoneroError; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; -import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxSet; import monero.wallet.model.MoneroTxWallet; @@ -122,20 +110,12 @@ public final class ArbitrationManager extends DisputeManager tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED)); + // no action } @Override @@ -185,43 +160,52 @@ public final class ArbitrationManager extends DisputeManager tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId()); - String tradeId = disputeResult.getTradeId(); - log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId()); + + // get trade + Trade trade = tradeManager.getTrade(tradeId); + if (trade == null) { + log.warn("Dispute trade {} does not exist", tradeId); + return; + } + + log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId()); + + // get dispute Optional disputeOptional = findDispute(disputeResult); - String uid = disputeResultMessage.getUid(); + String uid = disputeClosedMessage.getUid(); if (!disputeOptional.isPresent()) { - log.warn("We got a dispute result msg but we don't have a matching dispute. " + - "That might happen when we get the disputeResultMessage before the dispute was created. " + - "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + log.warn("We got a dispute closed msg but we don't have a matching dispute. " + + "That might happen when we get the DisputeClosedMessage before the dispute was created. " + + "We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeClosedMessage), 2); delayMsgMap.put(uid, timer); } else { - log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " + "That should never happen. TradeId = " + tradeId); } return; } Dispute dispute = disputeOptional.get(); - // verify that arbitrator does not get DisputeResultMessage + // verify that arbitrator does not get DisputeClosedMessage if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) { - log.error("Arbitrator received disputeResultMessage. That must never happen."); - return; + log.error("Arbitrator received disputeResultMessage. That should never happen."); + return; } + // set dispute state cleanupRetryMap(uid); if (!dispute.getChatMessages().contains(chatMessage)) { dispute.addAndPersistChatMessage(chatMessage); @@ -229,492 +213,139 @@ 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); - } - 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 trade wallet - MoneroWallet wallet = trade.getWallet(); - if (wallet != null) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion? - trade.syncWallet(); - wallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()); - trade.saveWallet(); - MoneroTxWallet parsedPayoutTx = wallet.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(); - } + }).start(); } - // Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published - // TODO: this should be invoked from mailbox message and send mailbox message response to support offline arbitrator - private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) { - log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName()); - String tradeId = request.getTradeId(); - 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(); + private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) { - // 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()); - - 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 - trade.syncWallet(); - MoneroWallet multisigWallet = trade.getWallet(); - try { - multisigWallet.importMultisigHex(request.getUpdatedMultisigHex()); - trade.saveWallet(); - } 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()); - - // TODO: hack to sync wallet after dispute message received in order to detect payout published - Trade trade = tradeManager.getTrade(dispute.getTradeId()); - long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); - for (int i = 0; i < 3; i++) { - UserThread.runAfter(() -> { - if (!trade.isPayoutUnlocked()) trade.syncWallet(); - }, defaultRefreshPeriod / 1000 * (i + 1)); - } - } - - @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()); - - // 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); - multisigWallet.sync(); - 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(); + // gather trade info + MoneroWallet multisigWallet = trade.getWallet(); + Optional disputeOptional = findDispute(trade.getId()); + if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + trade.getId()); + 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); + // parse arbitrator-signed payout tx + MoneroTxSet signedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); + if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack + MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0); - // 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"); + // 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); + // 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 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 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"); + // 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) + // 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); + // 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(disputeResult.getArbitratorUpdatedMultisigHex()); - xmrWalletService.saveWallet(multisigWallet); + // 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(); + signedTxSet.setMultisigTxHex(signedMultisigTxHex); - // 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; - } + // submit fully signed payout tx to the network + List txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex()); + signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed - private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) { - - // gather trade info - Optional disputeOptional = findDispute(tradeId); - if (!disputeOptional.isPresent()) { - log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); - return; - } - Dispute dispute = disputeOptional.get(); - Contract contract = dispute.getContract(); - Trade trade = tradeManager.getOpenTrade(tradeId).get(); - - // submit fully signed payout tx to the network - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // closed when trade completed (TradeManager.onTradeCompleted()) - List 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? - trade.setPayoutTxId(txSet.getTxs().get(0).getHash()); - trade.setPayoutState(Trade.PayoutState.PUBLISHED); - dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash()); - sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract); - closeTradeOrOffer(tradeId); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Send messages - /////////////////////////////////////////////////////////////////////////////////////////// - - // winner (or buyer in case of 50/50) sends tx to other peer - private void sendPeerPublishedPayoutTxMessage(String updatedMultisigHex, String payoutTxHex, Dispute dispute, Contract contract) { - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress(); - log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress); - PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(updatedMultisigHex, - payoutTxHex, - dispute.getTradeId(), - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType()); - log.info("Send {} to peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, - peersPubKeyRing, - message, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - } - } - ); - } - - public void closeTradeOrOffer(String tradeId) { - // set state after payout as we call swapTradeEntryToAvailableEntry - if (tradeManager.getOpenTrade(tradeId).isPresent()) { - tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED); - } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); - } - } - // dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx - private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) { - ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest( - dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - SupportType.ARBITRATION, - updatedMultisigHex); - log.info("Send {} to peer {}. tradeId={}, uid={}", - request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid()); - p2PService.sendEncryptedDirectMessage(contract.getArbitratorNodeAddress(), - dispute.getAgentPubKeyRing(), - request, - new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid()); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid(), errorMessage); - } - } - ); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Disputed payout tx signing - /////////////////////////////////////////////////////////////////////////////////////////// - - public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) { - - // multisig wallet must be synced - if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId()); - - // collect winner and loser payout address and amounts - String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ? - (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : - (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); - String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); - BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); - BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); - - // create transaction to get fee estimate - // TODO (woodser): include arbitration fee - MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx - if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); - MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig); - - // create payout tx by increasing estimated fee until successful - MoneroTxWallet payoutTx = null; - int numAttempts = 0; - while (payoutTx == null && numAttempts < 50) { - BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful - txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 - if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) { - if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee"); - if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee - } - numAttempts++; - try { - payoutTx = multisigWallet.createTx(txConfig); - } catch (MoneroError e) { - // exception expected // TODO: better way of estimating fee? - } - } - if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts"); - log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx); - return payoutTx; + // update state + trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? + trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash()); + trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED); + dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash()); + return signedTxSet; } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java deleted file mode 100644 index 6aed9cef8c..0000000000 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.support.dispute.arbitration.messages; - -import bisq.core.support.SupportType; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.Version; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@Value -@EqualsAndHashCode(callSuper = true) -public final class PeerPublishedDisputePayoutTxMessage extends ArbitrationMessage { - private final String updatedMultisigHex; - private final String payoutTxHex; - private final String tradeId; - private final NodeAddress senderNodeAddress; - - public PeerPublishedDisputePayoutTxMessage(String updatedMultisigHex, - String payoutTxHex, - String tradeId, - NodeAddress senderNodeAddress, - String uid, - SupportType supportType) { - this(updatedMultisigHex, - payoutTxHex, - tradeId, - senderNodeAddress, - uid, - Version.getP2PMessageVersion(), - supportType); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - private PeerPublishedDisputePayoutTxMessage(String updatedMultisigHex, - String payoutTxHex, - String tradeId, - NodeAddress senderNodeAddress, - String uid, - String messageVersion, - SupportType supportType) { - super(messageVersion, uid, supportType); - this.updatedMultisigHex = updatedMultisigHex; - this.payoutTxHex = payoutTxHex; - this.tradeId = tradeId; - this.senderNodeAddress = senderNodeAddress; - } - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setPeerPublishedDisputePayoutTxMessage(protobuf.PeerPublishedDisputePayoutTxMessage.newBuilder() - .setUpdatedMultisigHex(updatedMultisigHex) - .setPayoutTxHex(payoutTxHex) - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid) - .setType(SupportType.toProtoMessage(supportType))) - .build(); - } - - public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto, - String messageVersion) { - return new PeerPublishedDisputePayoutTxMessage(proto.getUpdatedMultisigHex(), - proto.getPayoutTxHex(), - proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getUid(), - messageVersion, - SupportType.fromProto(proto.getType())); - } - - @Override - public String getTradeId() { - return tradeId; - } - - @Override - public String toString() { - return "PeerPublishedDisputePayoutTxMessage{" + - "\n updatedMultisigHex=" + updatedMultisigHex + - "\n payoutTxHex=" + payoutTxHex + - ",\n tradeId='" + tradeId + '\'' + - ",\n senderNodeAddress=" + senderNodeAddress + - ",\n PeerPublishedDisputePayoutTxMessage.uid='" + uid + '\'' + - ",\n messageVersion=" + messageVersion + - ",\n supportType=" + supportType + - "\n} " + super.toString(); - } -} 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 746788639e..13f03c1606 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 @@ -29,9 +29,8 @@ import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; -import bisq.core.support.dispute.messages.DisputeResultMessage; -import bisq.core.support.dispute.messages.OpenNewDisputeMessage; -import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.messages.DisputeClosedMessage; +import bisq.core.support.dispute.messages.DisputeOpenedMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; import bisq.core.trade.ClosedTradableManager; @@ -107,25 +106,18 @@ public final class MediationManager extends DisputeManager log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - if (message instanceof OpenNewDisputeMessage) { - onOpenNewDisputeMessage((OpenNewDisputeMessage) message); - } else if (message instanceof PeerOpenedDisputeMessage) { - onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + if (message instanceof DisputeOpenedMessage) { + handleDisputeOpenedMessage((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { - onChatMessage((ChatMessage) message); - } else if (message instanceof DisputeResultMessage) { - onDisputeResultMessage((DisputeResultMessage) message); + handleChatMessage((ChatMessage) message); + } else if (message instanceof DisputeClosedMessage) { + handleDisputeClosedMessage((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } - @Override - protected Trade.DisputeState getDisputeStateStartedByPeer() { - return Trade.DisputeState.MEDIATION_STARTED_BY_PEER; - } - @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.MEDIATION_MESSAGE; @@ -164,7 +156,7 @@ public final class MediationManager extends DisputeManager @Override // We get that message at both peers. The dispute object is in context of the trader - public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); @@ -177,7 +169,7 @@ public final class MediationManager extends DisputeManager "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + diff --git a/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxRequest.java b/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxRequest.java deleted file mode 100644 index d770daebab..0000000000 --- a/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxRequest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.support.dispute.messages; - -import bisq.core.proto.CoreProtoResolver; -import bisq.core.support.SupportType; -import bisq.core.support.dispute.Dispute; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.Version; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@EqualsAndHashCode(callSuper = true) -@Value -public final class ArbitratorPayoutTxRequest extends DisputeMessage { - private final Dispute dispute; - private final NodeAddress senderNodeAddress; - private final String updatedMultisigHex; - - public ArbitratorPayoutTxRequest(Dispute dispute, - NodeAddress senderNodeAddress, - String uid, - SupportType supportType, - String updatedMultisigHex) { - this(dispute, - senderNodeAddress, - uid, - Version.getP2PMessageVersion(), - supportType, - updatedMultisigHex); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - private ArbitratorPayoutTxRequest(Dispute dispute, - NodeAddress senderNodeAddress, - String uid, - String messageVersion, - SupportType supportType, - String updatedMultisigHex) { - super(messageVersion, uid, supportType); - this.dispute = dispute; - this.senderNodeAddress = senderNodeAddress; - this.updatedMultisigHex = updatedMultisigHex; - } - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setArbitratorPayoutTxRequest(protobuf.ArbitratorPayoutTxRequest.newBuilder() - .setUid(uid) - .setDispute(dispute.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex)) - .build(); - } - - public static ArbitratorPayoutTxRequest fromProto(protobuf.ArbitratorPayoutTxRequest proto, - CoreProtoResolver coreProtoResolver, - String messageVersion) { - return new ArbitratorPayoutTxRequest(Dispute.fromProto(proto.getDispute(), coreProtoResolver), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getUid(), - messageVersion, - SupportType.fromProto(proto.getType()), - proto.getUpdatedMultisigHex()); - } - - @Override - public String getTradeId() { - return dispute.getTradeId(); - } - - @Override - public String toString() { - return "ArbitratorPayoutTxRequest{" + - "\n dispute=" + dispute + - ",\n senderNodeAddress=" + senderNodeAddress + - ",\n ArbitratorPayoutTxRequest.uid='" + uid + '\'' + - ",\n messageVersion=" + messageVersion + - ",\n supportType=" + supportType + - ",\n updatedMultisigHex=" + updatedMultisigHex + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxResponse.java b/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxResponse.java deleted file mode 100644 index e048b1579e..0000000000 --- a/core/src/main/java/bisq/core/support/dispute/messages/ArbitratorPayoutTxResponse.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.support.dispute.messages; - -import bisq.core.proto.CoreProtoResolver; -import bisq.core.support.SupportType; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.Version; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@EqualsAndHashCode(callSuper = true) -@Value -public final class ArbitratorPayoutTxResponse extends DisputeMessage { - private final String tradeId; - private final NodeAddress senderNodeAddress; - private final String arbitratorSignedPayoutTxHex; - - public ArbitratorPayoutTxResponse(String tradeId, - NodeAddress senderNodeAddress, - String uid, - SupportType supportType, - String arbitratorSignedPayoutTxHex) { - this(tradeId, - senderNodeAddress, - uid, - Version.getP2PMessageVersion(), - supportType, - arbitratorSignedPayoutTxHex); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - private ArbitratorPayoutTxResponse(String tradeId, - NodeAddress senderNodeAddress, - String uid, - String messageVersion, - SupportType supportType, - String arbitratorSignedPayoutTxHex) { - super(messageVersion, uid, supportType); - this.tradeId = tradeId; - this.senderNodeAddress = senderNodeAddress; - this.arbitratorSignedPayoutTxHex = arbitratorSignedPayoutTxHex; - } - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setArbitratorPayoutTxResponse(protobuf.ArbitratorPayoutTxResponse.newBuilder() - .setUid(uid) - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setType(SupportType.toProtoMessage(supportType)) - .setArbitratorSignedPayoutTxHex(arbitratorSignedPayoutTxHex)) - .build(); - } - - public static ArbitratorPayoutTxResponse fromProto(protobuf.ArbitratorPayoutTxResponse proto, - CoreProtoResolver coreProtoResolver, - String messageVersion) { - return new ArbitratorPayoutTxResponse(proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getUid(), - messageVersion, - SupportType.fromProto(proto.getType()), - proto.getArbitratorSignedPayoutTxHex()); - } - - @Override - public String toString() { - return "ArbitratorPayoutTxResponse{" + - "\n tradeId=" + tradeId + - ",\n senderNodeAddress=" + senderNodeAddress + - ",\n ArbitratorPayoutTxResponse.uid='" + uid + '\'' + - ",\n messageVersion=" + messageVersion + - ",\n supportType=" + supportType + - ",\n updatedMultisigHex=" + arbitratorSignedPayoutTxHex + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeClosedMessage.java similarity index 52% rename from core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java rename to core/src/main/java/bisq/core/support/dispute/messages/DisputeClosedMessage.java index fb82b03f99..153349b783 100644 --- a/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeClosedMessage.java @@ -23,27 +23,41 @@ import bisq.core.support.dispute.DisputeResult; import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; - +import bisq.common.proto.ProtoUtil; import lombok.EqualsAndHashCode; import lombok.Value; import static com.google.common.base.Preconditions.checkArgument; +import java.util.Optional; + +import javax.annotation.Nullable; + @Value @EqualsAndHashCode(callSuper = true) -public final class DisputeResultMessage extends DisputeMessage { +public final class DisputeClosedMessage extends DisputeMessage { private final DisputeResult disputeResult; private final NodeAddress senderNodeAddress; + private final String updatedMultisigHex; + @Nullable + private final String unsignedPayoutTxHex; + private final boolean deferPublishPayout; - public DisputeResultMessage(DisputeResult disputeResult, + public DisputeClosedMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, String uid, - SupportType supportType) { + SupportType supportType, + String updatedMultisigHex, + @Nullable String unsignedPayoutTxHex, + boolean deferPublishPayout) { this(disputeResult, senderNodeAddress, uid, Version.getP2PMessageVersion(), - supportType); + supportType, + updatedMultisigHex, + unsignedPayoutTxHex, + deferPublishPayout); } @@ -51,34 +65,45 @@ public final class DisputeResultMessage extends DisputeMessage { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private DisputeResultMessage(DisputeResult disputeResult, + private DisputeClosedMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, String uid, String messageVersion, - SupportType supportType) { + SupportType supportType, + String updatedMultisigHex, + String unsignedPayoutTxHex, + boolean deferPublishPayout) { super(messageVersion, uid, supportType); this.disputeResult = disputeResult; this.senderNodeAddress = senderNodeAddress; + this.updatedMultisigHex = updatedMultisigHex; + this.unsignedPayoutTxHex = unsignedPayoutTxHex; + this.deferPublishPayout = deferPublishPayout; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder() - .setDisputeResult(disputeResult.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid) - .setType(SupportType.toProtoMessage(supportType))) - .build(); + protobuf.DisputeClosedMessage.Builder builder = protobuf.DisputeClosedMessage.newBuilder() + .setDisputeResult(disputeResult.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid) + .setType(SupportType.toProtoMessage(supportType)) + .setUpdatedMultisigHex(updatedMultisigHex) + .setDeferPublishPayout(deferPublishPayout); + Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); + return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build(); } - public static DisputeResultMessage fromProto(protobuf.DisputeResultMessage proto, String messageVersion) { + public static DisputeClosedMessage fromProto(protobuf.DisputeClosedMessage proto, String messageVersion) { checkArgument(proto.hasDisputeResult(), "DisputeResult must be set"); - return new DisputeResultMessage(DisputeResult.fromProto(proto.getDisputeResult()), + return new DisputeClosedMessage(DisputeResult.fromProto(proto.getDisputeResult()), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion, - SupportType.fromProto(proto.getType())); + SupportType.fromProto(proto.getType()), + proto.getUpdatedMultisigHex(), + ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), + proto.getDeferPublishPayout()); } @Override @@ -88,12 +113,13 @@ public final class DisputeResultMessage extends DisputeMessage { @Override public String toString() { - return "DisputeResultMessage{" + + return "DisputeClosedMessage{" + "\n disputeResult=" + disputeResult + ",\n senderNodeAddress=" + senderNodeAddress + - ",\n DisputeResultMessage.uid='" + uid + '\'' + + ",\n DisputeClosedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + + ",\n deferPublishPayout=" + deferPublishPayout + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeOpenedMessage.java similarity index 61% rename from core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java rename to core/src/main/java/bisq/core/support/dispute/messages/DisputeOpenedMessage.java index 120f8616f4..ff6ab28b36 100644 --- a/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeOpenedMessage.java @@ -20,9 +20,11 @@ package bisq.core.support.dispute.messages; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; - +import bisq.core.trade.messages.PaymentSentMessage; import bisq.network.p2p.NodeAddress; +import java.util.Optional; + import bisq.common.app.Version; import lombok.EqualsAndHashCode; @@ -30,22 +32,25 @@ import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value -public final class OpenNewDisputeMessage extends DisputeMessage { +public final class DisputeOpenedMessage extends DisputeMessage { private final Dispute dispute; private final NodeAddress senderNodeAddress; private final String updatedMultisigHex; + private final PaymentSentMessage paymentSentMessage; - public OpenNewDisputeMessage(Dispute dispute, + public DisputeOpenedMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, SupportType supportType, - String updatedMultisigHex) { + String updatedMultisigHex, + PaymentSentMessage paymentSentMessage) { this(dispute, senderNodeAddress, uid, Version.getP2PMessageVersion(), supportType, - updatedMultisigHex); + updatedMultisigHex, + paymentSentMessage); } @@ -53,39 +58,42 @@ public final class OpenNewDisputeMessage extends DisputeMessage { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private OpenNewDisputeMessage(Dispute dispute, + private DisputeOpenedMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, String messageVersion, SupportType supportType, - String updatedMultisigHex) { + String updatedMultisigHex, + PaymentSentMessage paymentSentMessage) { super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; this.updatedMultisigHex = updatedMultisigHex; + this.paymentSentMessage = paymentSentMessage; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder() - .setUid(uid) - .setDispute(dispute.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex)) - .build(); + protobuf.DisputeOpenedMessage.Builder builder = protobuf.DisputeOpenedMessage.newBuilder() + .setUid(uid) + .setDispute(dispute.toProtoMessage()) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setType(SupportType.toProtoMessage(supportType)) + .setUpdatedMultisigHex(updatedMultisigHex); + Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); + return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); } - public static OpenNewDisputeMessage fromProto(protobuf.OpenNewDisputeMessage proto, + public static DisputeOpenedMessage fromProto(protobuf.DisputeOpenedMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) { - return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), + return new DisputeOpenedMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - proto.getUpdatedMultisigHex()); + proto.getUpdatedMultisigHex(), + proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); } @Override @@ -95,13 +103,14 @@ public final class OpenNewDisputeMessage extends DisputeMessage { @Override public String toString() { - return "OpenNewDisputeMessage{" + + return "DisputeOpenedMessage{" + "\n dispute=" + dispute + ",\n senderNodeAddress=" + senderNodeAddress + - ",\n OpenNewDisputeMessage.uid='" + uid + '\'' + + ",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + ",\n updatedMultisigHex=" + updatedMultisigHex + + ",\n paymentSentMessage=" + paymentSentMessage + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java deleted file mode 100644 index 1f6abf8755..0000000000 --- a/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.support.dispute.messages; - -import bisq.core.proto.CoreProtoResolver; -import bisq.core.support.SupportType; -import bisq.core.support.dispute.Dispute; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.Version; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@Value -@EqualsAndHashCode(callSuper = true) -public final class PeerOpenedDisputeMessage extends DisputeMessage { - private final Dispute dispute; - private final NodeAddress senderNodeAddress; - - public PeerOpenedDisputeMessage(Dispute dispute, - NodeAddress senderNodeAddress, - String uid, - SupportType supportType) { - this(dispute, - senderNodeAddress, - uid, - Version.getP2PMessageVersion(), - supportType); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - private PeerOpenedDisputeMessage(Dispute dispute, - NodeAddress senderNodeAddress, - String uid, - String messageVersion, - SupportType supportType) { - super(messageVersion, uid, supportType); - this.dispute = dispute; - this.senderNodeAddress = senderNodeAddress; - } - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - return getNetworkEnvelopeBuilder() - .setPeerOpenedDisputeMessage(protobuf.PeerOpenedDisputeMessage.newBuilder() - .setUid(uid) - .setDispute(dispute.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setType(SupportType.toProtoMessage(supportType))) - .build(); - } - - public static PeerOpenedDisputeMessage fromProto(protobuf.PeerOpenedDisputeMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) { - return new PeerOpenedDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getUid(), - messageVersion, - SupportType.fromProto(proto.getType())); - } - - @Override - public String getTradeId() { - return dispute.getTradeId(); - } - - @Override - public String toString() { - return "PeerOpenedDisputeMessage{" + - "\n dispute=" + dispute + - ",\n senderNodeAddress=" + senderNodeAddress + - ",\n PeerOpenedDisputeMessage.uid='" + uid + '\'' + - ",\n messageVersion=" + messageVersion + - ",\n supportType=" + supportType + - "\n} " + super.toString(); - } -} 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 aafd088e16..5d98c3b55c 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 @@ -29,9 +29,8 @@ import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; -import bisq.core.support.dispute.messages.DisputeResultMessage; -import bisq.core.support.dispute.messages.OpenNewDisputeMessage; -import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.messages.DisputeClosedMessage; +import bisq.core.support.dispute.messages.DisputeOpenedMessage; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; import bisq.core.trade.ClosedTradableManager; @@ -101,25 +100,18 @@ public final class RefundManager extends DisputeManager { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - if (message instanceof OpenNewDisputeMessage) { - onOpenNewDisputeMessage((OpenNewDisputeMessage) message); - } else if (message instanceof PeerOpenedDisputeMessage) { - onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + if (message instanceof DisputeOpenedMessage) { + handleDisputeOpenedMessage((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { - onChatMessage((ChatMessage) message); - } else if (message instanceof DisputeResultMessage) { - onDisputeResultMessage((DisputeResultMessage) message); + handleChatMessage((ChatMessage) message); + } else if (message instanceof DisputeClosedMessage) { + handleDisputeClosedMessage((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } - @Override - protected Trade.DisputeState getDisputeStateStartedByPeer() { - return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; - } - @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.REFUND_MESSAGE; @@ -161,7 +153,7 @@ public final class RefundManager extends DisputeManager { @Override // We get that message at both peers. The dispute object is in context of the trader - public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); @@ -174,7 +166,7 @@ public final class RefundManager extends DisputeManager { "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + 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 4abcbe87f6..a14e0de073 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -154,7 +154,7 @@ public class TraderChatManager extends SupportManager { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof ChatMessage) { - onChatMessage((ChatMessage) message); + handleChatMessage((ChatMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } diff --git a/core/src/main/java/bisq/core/trade/HavenoUtils.java b/core/src/main/java/bisq/core/trade/HavenoUtils.java index 266e064e69..c5c8849767 100644 --- a/core/src/main/java/bisq/core/trade/HavenoUtils.java +++ b/core/src/main/java/bisq/core/trade/HavenoUtils.java @@ -24,7 +24,11 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.trade.messages.InitTradeRequest; +import bisq.core.trade.messages.PaymentReceivedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.util.JsonUtil; +import lombok.extern.slf4j.Slf4j; + import java.net.URI; import java.util.Collection; import java.util.concurrent.CountDownLatch; @@ -32,9 +36,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import com.google.common.base.Charsets; + /** * Collection of utilities. */ +@Slf4j public class HavenoUtils { public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node @@ -73,10 +80,10 @@ public class HavenoUtils { } /** - * Check if the arbitrator signature for an offer is valid. + * Check if the arbitrator signature is valid for an offer. * * @param offer is a signed offer with payload - * @param arbitrator is the possible original arbitrator + * @param arbitrator is the original signing arbitrator * @return true if the arbitrator's signature is valid for the offer */ public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) { @@ -92,15 +99,11 @@ public class HavenoUtils { String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy); // verify arbitrator signature - boolean isValid = true; try { - isValid = Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature); + return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature); } catch (Exception e) { - isValid = false; + return false; } - - // return result - return isValid; } /** @@ -149,6 +152,71 @@ public class HavenoUtils { } } + /** + * Verify the buyer signature for a PaymentSentMessage. + * + * @param trade - the trade to verify + * @param message - signed payment sent message to verify + * @return true if the buyer's signature is valid for the message + */ + public static void verifyPaymentSentMessage(Trade trade, PaymentSentMessage message) { + + // remove signature from message + byte[] signature = message.getBuyerSignature(); + message.setBuyerSignature(null); + + // get unsigned message as json string + String unsignedMessageAsJson = JsonUtil.objectToJson(message); + + // replace signature + message.setBuyerSignature(signature); + + // verify signature + String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId(); + try { + if (!Sig.verify(trade.getBuyer().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage); + } catch (Exception e) { + throw new RuntimeException(errMessage); + } + + // verify trade id + if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId()); + } + + /** + * Verify the seller signature for a PaymentReceivedMessage. + * + * @param trade - the trade to verify + * @param message - signed payment received message to verify + * @return true if the seller's signature is valid for the message + */ + public static void verifyPaymentReceivedMessage(Trade trade, PaymentReceivedMessage message) { + + // remove signature from message + byte[] signature = message.getSellerSignature(); + message.setSellerSignature(null); + + // get unsigned message as json string + String unsignedMessageAsJson = JsonUtil.objectToJson(message); + + // replace signature + message.setSellerSignature(signature); + + // verify signature + String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId(); + try { + if (!Sig.verify(trade.getSeller().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage); + } catch (Exception e) { + throw new RuntimeException(errMessage); + } + + // verify trade id + if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId()); + + // verify buyer signature of payment sent message + verifyPaymentSentMessage(trade, message.getPaymentSentMessage()); + } + public static void awaitLatch(CountDownLatch latch) { try { latch.await(); @@ -156,14 +224,18 @@ public class HavenoUtils { throw new RuntimeException(e); } } - - public static void awaitTasks(Collection tasks) { + + public static void executeTasks(Collection tasks) { + executeTasks(tasks, tasks.size()); + } + + public static void executeTasks(Collection tasks, int poolSize) { if (tasks.isEmpty()) return; - ExecutorService pool = Executors.newFixedThreadPool(tasks.size()); + ExecutorService pool = Executors.newFixedThreadPool(poolSize); for (Runnable task : tasks) pool.submit(task); pool.shutdown(); try { - if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); + if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow(); } catch (InterruptedException e) { pool.shutdownNow(); throw new RuntimeException(e); diff --git a/core/src/main/java/bisq/core/trade/SellerTrade.java b/core/src/main/java/bisq/core/trade/SellerTrade.java index a6f873094e..e33d57f284 100644 --- a/core/src/main/java/bisq/core/trade/SellerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerTrade.java @@ -73,7 +73,11 @@ public abstract class SellerTrade extends Trade { return true; case DISPUTE_REQUESTED: - case DISPUTE_STARTED_BY_PEER: + case DISPUTE_OPENED: + case ARBITRATOR_SENT_DISPUTE_CLOSED_MSG: + case ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG: + case ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG: + case ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG: case DISPUTE_CLOSED: case MEDIATION_REQUESTED: case MEDIATION_STARTED_BY_PEER: diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 71ff3e0110..22034f01e6 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -35,6 +35,7 @@ import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.ProcessModelServiceProvider; import bisq.core.trade.protocol.TradeListener; +import bisq.core.trade.protocol.TradeProtocol; import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.util.ParsingUtils; @@ -44,6 +45,7 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.common.UserThread; import bisq.common.crypto.Encryption; +import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.taskrunner.Model; import bisq.common.util.Utilities; @@ -72,6 +74,7 @@ import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import lombok.Getter; @@ -214,10 +217,10 @@ public abstract class Trade implements Tradable, Model { } public enum PayoutState { - UNPUBLISHED, - PUBLISHED, - CONFIRMED, - UNLOCKED; + PAYOUT_UNPUBLISHED, + PAYOUT_PUBLISHED, + PAYOUT_CONFIRMED, + PAYOUT_UNLOCKED; public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) { return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name()); @@ -234,9 +237,12 @@ public abstract class Trade implements Tradable, Model { public enum DisputeState { NO_DISPUTE, - // arbitration - DISPUTE_REQUESTED, - DISPUTE_STARTED_BY_PEER, + DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager + DISPUTE_OPENED, + ARBITRATOR_SENT_DISPUTE_CLOSED_MSG, + ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG, + ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG, + ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG, DISPUTE_CLOSED, // mediation @@ -268,12 +274,12 @@ public abstract class Trade implements Tradable, Model { } public boolean isArbitrated() { - return this == Trade.DisputeState.DISPUTE_REQUESTED || - this == Trade.DisputeState.DISPUTE_STARTED_BY_PEER || - this == Trade.DisputeState.DISPUTE_CLOSED || - this == Trade.DisputeState.REFUND_REQUESTED || - this == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER || - this == Trade.DisputeState.REFUND_REQUEST_CLOSED; + if (isMediated()) return false; // TODO: remove mediation? + return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal(); + } + + public boolean isClosed() { + return this == DisputeState.DISPUTE_CLOSED; } } @@ -324,7 +330,7 @@ public abstract class Trade implements Tradable, Model { @Getter private State state = State.PREPARATION; @Getter - private PayoutState payoutState = PayoutState.UNPUBLISHED; + private PayoutState payoutState = PayoutState.PAYOUT_UNPUBLISHED; @Getter private DisputeState disputeState = DisputeState.NO_DISPUTE; @Getter @@ -365,11 +371,13 @@ public abstract class Trade implements Tradable, Model { transient final private ObjectProperty disputeStateProperty = new SimpleObjectProperty<>(disputeState); transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); - transient private Subscription tradePhaseSubscription = null; - transient private Subscription payoutStateSubscription = null; + transient private Subscription tradePhaseSubscription; + transient private Subscription payoutStateSubscription; transient private TaskLooper tradeTxsLooper; - transient private Long lastWalletRefreshPeriod; + transient private Long walletRefreshPeriod; + transient private Long syncNormalStartTime; private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour + public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds // Mutable @Getter @@ -435,8 +443,9 @@ public abstract class Trade implements Tradable, Model { private String payoutTxKey; private Long startTime; // cache + /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, initialization + // Constructors /////////////////////////////////////////////////////////////////////////////////////////// // maker @@ -530,96 +539,56 @@ public abstract class Trade implements Tradable, Model { setAmount(tradeAmount); } + /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER + // Listeners /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public Message toProtoMessage() { - protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() - .setOffer(offer.toProtoMessage()) - .setTxFeeAsLong(txFeeAsLong) - .setTakerFeeAsLong(takerFeeAsLong) - .setTakeOfferDate(takeOfferDate) - .setProcessModel(processModel.toProtoMessage()) - .setAmountAsLong(amountAsLong) - .setPrice(price) - .setState(Trade.State.toProtoMessage(state)) - .setPayoutState(Trade.PayoutState.toProtoMessage(payoutState)) - .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) - .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) - .addAllChatMessage(chatMessages.stream() - .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) - .collect(Collectors.toList())) - .setLockTime(lockTime) - .setUid(uid); - - Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); - Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage())); - Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); - Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash))); - Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); - Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); - Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); - Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); - Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); - Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey)); - Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); - Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name())); - return builder.build(); + public void addListener(TradeListener listener) { + tradeListeners.add(listener); } - - public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { - trade.setTakeOfferDate(proto.getTakeOfferDate()); - trade.setState(State.fromProto(proto.getState())); - trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState())); - trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); - trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); - trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); - trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex())); - trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey())); - trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); - trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); - trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); - trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); - trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); - trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); - trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState())); - trade.setLockTime(proto.getLockTime()); - trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData())); - - AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult()); - // We do not want to show the user the last pending state when he starts up the app again, so we clear it. - if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) { - persistedAssetTxProofResult = null; + + public void removeListener(TradeListener listener) { + if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered"); + } + + // notified from TradeProtocol of verified trade messages + public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { + for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception + listener.onVerifiedTradeMessage(message, sender); } - trade.setAssetTxProofResult(persistedAssetTxProofResult); - - trade.chatMessages.addAll(proto.getChatMessageList().stream() - .map(ChatMessage::fromPayloadProto) - .collect(Collectors.toList())); - - return trade; } + + // notified from TradeProtocol of ack messages + public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception + listener.onAckMessage(ackMessage, sender); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(ProcessModelServiceProvider serviceProvider) { serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> { getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); }); - isInitialized = true; // TODO: move to end? - // listen to daemon connection xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection)); - // done if payout unlocked + // check if done if (isPayoutUnlocked()) return; // handle trade state events - if (isDepositPublished()) listenToTradeTxs(); tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { - updateTxListenerRefreshPeriod(); - if (isDepositPublished()) listenToTradeTxs(); + if (!isInitialized) return; + if (isDepositPublished() && !isPayoutUnlocked()) { + updateWalletRefreshPeriod(); + listenToTradeTxs(); + } if (isCompleted()) { UserThread.execute(() -> { if (tradePhaseSubscription != null) { @@ -632,17 +601,23 @@ public abstract class Trade implements Tradable, Model { // handle payout state events payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> { - updateTxListenerRefreshPeriod(); + if (!isInitialized) return; + if (isPayoutPublished()) updateWalletRefreshPeriod(); // cleanup when payout published - if (isPayoutPublished()) { + if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) { log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); - if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published + + // complete disputed trade + if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) processModel.getTradeManager().closeDisputedTrade(getId(), Trade.DisputeState.DISPUTE_CLOSED); + + // complete arbitrator trade + if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId()); } // cleanup when payout unlocks - if (isPayoutUnlocked()) { + if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) { log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time? deleteWallet(); if (tradeTxsLooper != null) { @@ -657,12 +632,24 @@ public abstract class Trade implements Tradable, Model { }); } }); + + isInitialized = true; + + // start listening to trade wallet + if (isDepositPublished()) { + updateWalletRefreshPeriod(); + listenToTradeTxs(); + + // allow state notifications to process before returning + CountDownLatch latch = new CountDownLatch(1); + UserThread.execute(() -> latch.countDown()); + HavenoUtils.awaitLatch(latch); + } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// + public TradeProtocol getProtocol() { + return processModel.getTradeManager().getTradeProtocol(this); + } public void setMyNodeAddress() { getSelf().setNodeAddress(P2PService.getMyNodeAddress()); @@ -755,9 +742,11 @@ public abstract class Trade implements Tradable, Model { // exception expected } } - if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts"); - log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx); + log.info("Payout transaction generated on attempt {}", numAttempts); + + // save updated multisig hex + getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); return payoutTx; } @@ -827,7 +816,7 @@ public abstract class Trade implements Tradable, Model { // submit payout tx if (publish) { multisigWallet.submitMultisigTxHex(payoutTxHex); - setPayoutState(Trade.PayoutState.PUBLISHED); + setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED); } } @@ -921,10 +910,22 @@ public abstract class Trade implements Tradable, Model { } public void syncWallet() { + if (getWallet() == null) { + log.warn("Cannot sync multisig wallet because it doesn't exist for {}, {}", getClass().getSimpleName(), getId()); + return; + } log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); getWallet().sync(); - log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); pollWallet(); + log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); + } + + public void syncWalletNormallyForMs(long syncNormalDuration) { + syncNormalStartTime = System.currentTimeMillis(); + setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs()); + UserThread.runAfter(() -> { + if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod(); + }, syncNormalDuration); } public void saveWallet() { @@ -938,6 +939,12 @@ public abstract class Trade implements Tradable, Model { public void shutDown() { isInitialized = false; + if (tradeTxsLooper != null) { + tradeTxsLooper.stop(); + tradeTxsLooper = null; + } + if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); + if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -958,32 +965,6 @@ public abstract class Trade implements Tradable, Model { public abstract boolean confirmPermitted(); - /////////////////////////////////////////////////////////////////////////////////////////// - // Listeners - /////////////////////////////////////////////////////////////////////////////////////////// - - public void addListener(TradeListener listener) { - tradeListeners.add(listener); - } - - public void removeListener(TradeListener listener) { - if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered"); - } - - // notified from TradeProtocol of verified trade messages - public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { - for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception - listener.onVerifiedTradeMessage(message, sender); - } - } - - // notified from TradeProtocol of ack messages - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception - listener.onAckMessage(ackMessage, sender); - } - } - /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// @@ -1031,7 +1012,7 @@ public abstract class Trade implements Tradable, Model { public void setPayoutState(PayoutState payoutState) { if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades - log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState); + log.info("Set new payout state for {} {}: {}", this.getClass().getSimpleName(), getId(), payoutState); } if (payoutState.ordinal() < this.payoutState.ordinal()) { String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" + @@ -1046,8 +1027,24 @@ public abstract class Trade implements Tradable, Model { } public void setDisputeState(DisputeState disputeState) { + if (isInitialized) { + // We don't want to log at startup the setState calls from all persisted trades + log.info("Set new dispute state for {} {}: {}", this.getClass().getSimpleName(), getShortId(), disputeState); + } + if (disputeState.ordinal() < this.disputeState.ordinal()) { + String message = "We got a dispute state change to a previous state (id=" + getShortId() + ").\n" + + "Old dispute state is: " + this.disputeState + ". New dispute state is: " + disputeState; + log.warn(message); + } + this.disputeState = disputeState; - disputeStateProperty.set(disputeState); + UserThread.execute(() -> { + disputeStateProperty.set(disputeState); + }); + } + + public void setDisputeStateIfProgress(DisputeState disputeState) { + if (disputeState.ordinal() > getDisputeState().ordinal()) setDisputeState(disputeState); } public void setMediationResultState(MediationResultState mediationResultState) { @@ -1140,31 +1137,27 @@ public abstract class Trade implements Tradable, Model { return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker(); } - /** - * Get the taker if maker, maker if taker, null if arbitrator. - * - * @return the trade peer - */ + // get the taker if maker, maker if taker, null if arbitrator public TradingPeer getTradingPeer() { - if (this instanceof MakerTrade) return processModel.getTaker(); - else if (this instanceof TakerTrade) return processModel.getMaker(); - else if (this instanceof ArbitratorTrade) return null; - else throw new RuntimeException("Unknown trade type: " + getClass().getName()); + if (this instanceof MakerTrade) return processModel.getTaker(); + else if (this instanceof TakerTrade) return processModel.getMaker(); + else if (this instanceof ArbitratorTrade) return null; + else throw new RuntimeException("Unknown trade type: " + getClass().getName()); } - /** - * Get the peer with the given address which can be self. - * - * TODO (woodser): this naming convention is confusing - * - * @param address is the address of the peer to get - * @return the trade peer - */ + // TODO (woodser): this naming convention is confusing public TradingPeer getTradingPeer(NodeAddress address) { if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker(); if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker(); if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator(); - throw new RuntimeException("No trade participant with the given address. Their address might have changed: " + address); + return null; + } + + public TradingPeer getTradingPeer(PubKeyRing pubKeyRing) { + if (getMaker() != null && getMaker().getPubKeyRing().equals(pubKeyRing)) return getMaker(); + if (getTaker() != null && getTaker().getPubKeyRing().equals(pubKeyRing)) return getTaker(); + if (getArbitrator() != null && getArbitrator().getPubKeyRing().equals(pubKeyRing)) return getArbitrator(); + return null; } public Date getTakeOfferDate() { @@ -1210,12 +1203,10 @@ public abstract class Trade implements Tradable, Model { private long getStartTime() { if (startTime != null) return startTime; long now = System.currentTimeMillis(); - final MoneroTx takerDepositTx = getTakerDepositTx(); - final MoneroTx makerDepositTx = getMakerDepositTx(); - if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) { + if (isDepositConfirmed() && getTakeOfferDate() != null) { if (isDepositUnlocked()) { final long tradeTime = getTakeOfferDate().getTime(); - long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight()); + long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight()); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); @@ -1233,7 +1224,7 @@ public abstract class Trade implements Tradable, Model { log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", new Date(startTime), new Date(tradeTime), new Date(blockTime)); } else { - log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", makerDepositTx.getHash(), takerDepositTx.getHash()); + log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash()); startTime = now; } } else { @@ -1259,34 +1250,7 @@ public abstract class Trade implements Tradable, Model { } public boolean isFundsLockedIn() { - // If no deposit tx was published we have no funds locked in - if (!isDepositPublished()) { - return false; - } - - // If we have the payout tx published (non disputed case) we have no funds locked in. Here we might have more - // complex cases where users open a mediation but continue the trade to finalize it without mediated payout. - // The trade state handles that but does not handle mediated payouts or refund agents payouts. - if (isPayoutPublished()) { - return false; - } - - // check for closed disputed case - if (disputeState == DisputeState.DISPUTE_CLOSED) return false; - - // In mediation case we check for the mediationResultState. As there are multiple sub-states we use ordinal. - if (disputeState == DisputeState.MEDIATION_CLOSED) { - if (mediationResultState != null && - mediationResultState.ordinal() >= MediationResultState.PAYOUT_TX_PUBLISHED.ordinal()) { - return false; - } - } - - // In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as - // locked in funds. - return disputeState != DisputeState.REFUND_REQUESTED && - disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER && - disputeState != DisputeState.REFUND_REQUEST_CLOSED; + return isDepositPublished() && !isPayoutPublished(); } public boolean isDepositConfirmed() { @@ -1310,15 +1274,15 @@ public abstract class Trade implements Tradable, Model { } public boolean isPayoutPublished() { - return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal(); + return getPayoutState().ordinal() >= PayoutState.PAYOUT_PUBLISHED.ordinal(); } public boolean isPayoutConfirmed() { - return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal(); + return getPayoutState().ordinal() >= PayoutState.PAYOUT_CONFIRMED.ordinal(); } public boolean isPayoutUnlocked() { - return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal(); + return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal(); } public ReadOnlyObjectProperty stateProperty() { @@ -1439,80 +1403,81 @@ public abstract class Trade implements Tradable, Model { // poll wallet for tx state pollWallet(); - tradeTxsLooper = new TaskLooper(() -> { - try { - pollWallet(); - } catch (Exception e) { - if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage()); - } - }); - tradeTxsLooper.start(getWalletRefreshPeriod()); + tradeTxsLooper = new TaskLooper(() -> { pollWallet(); }); + tradeTxsLooper.start(walletRefreshPeriod); } private void pollWallet() { + try { - // skip if payout unlocked - if (isPayoutUnlocked()) return; + // skip if payout unlocked + if (isPayoutUnlocked()) return; - // rescan spent if deposits unlocked - if (isDepositUnlocked()) getWallet().rescanSpent(); + // rescan spent if deposits unlocked + if (isDepositUnlocked()) getWallet().rescanSpent(); - // get txs with outputs - List txs = getWallet().getTxs(new MoneroTxQuery() - .setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())) - .setIncludeOutputs(true)); + // get txs with outputs + List txs = getWallet().getTxs(new MoneroTxQuery() + .setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())) + .setIncludeOutputs(true)); - // check deposit txs - if (!isDepositUnlocked()) { - if (txs.size() == 2) { - setStateDepositsPublished(); - boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); - getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); - getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); + // check deposit txs + if (!isDepositUnlocked()) { + if (txs.size() == 2) { + setStateDepositsPublished(); + boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); + getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); + getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); - // check if deposit txs confirmed - if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); - if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); - } - } - - // check payout tx - else { - - // check if deposit txs spent (appears on payout published) - for (MoneroTxWallet tx : txs) { - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) { - setPayoutStatePublished(); - } + // check if deposit txs confirmed + if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); + if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); } } - // check for outgoing txs (appears on payout confirmed) - List outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true)); - if (!outgoingTxs.isEmpty()) { - MoneroTxWallet payoutTx = outgoingTxs.get(0); - setPayoutTx(payoutTx); - setPayoutStatePublished(); - if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); - if (!payoutTx.isLocked()) setPayoutStateUnlocked(); + // check payout tx + else { + + // check if deposit txs spent (appears on payout published) + for (MoneroTxWallet tx : txs) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (Boolean.TRUE.equals(output.isSpent())) { + setPayoutStatePublished(); + } + } + } + + // check for outgoing txs (appears on payout confirmed) + List outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true)); + if (!outgoingTxs.isEmpty()) { + MoneroTxWallet payoutTx = outgoingTxs.get(0); + setPayoutTx(payoutTx); + setPayoutStatePublished(); + if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); + if (!payoutTx.isLocked()) setPayoutStateUnlocked(); + } } + } catch (Exception e) { + if (isInitialized) log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); // TODO (monero-java): poller.isPolling() and then don't need to use isInitialized here as shutdown flag } } private void setDaemonConnection(MoneroRpcConnection connection) { + if (getWallet() == null) return; log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri()); - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId()); - multisigWallet.setDaemonConnection(connection); - multisigWallet.startSyncing(getWalletRefreshPeriod()); - updateTxListenerRefreshPeriod(); + getWallet().setDaemonConnection(connection); + updateWalletRefreshPeriod(); } - private void updateTxListenerRefreshPeriod() { - long walletRefreshPeriod = getWalletRefreshPeriod(); - if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return; - log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod); - lastWalletRefreshPeriod = walletRefreshPeriod; + private void updateWalletRefreshPeriod() { + setWalletRefreshPeriod(getWalletRefreshPeriod()); + } + + private void setWalletRefreshPeriod(long walletRefreshPeriod) { + if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return; + log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod); + this.walletRefreshPeriod = walletRefreshPeriod; + getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period if (tradeTxsLooper != null) { tradeTxsLooper.stop(); tradeTxsLooper = null; @@ -1521,8 +1486,8 @@ public abstract class Trade implements Tradable, Model { } private long getWalletRefreshPeriod() { - if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup - return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate + if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // slow arbitrator trade wallet after deposits confirm + return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // else sync at default rate } private void setStateDepositsPublished() { @@ -1538,15 +1503,87 @@ public abstract class Trade implements Tradable, Model { } private void setPayoutStatePublished() { - if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED); + if (!isPayoutPublished()) setPayoutState(PayoutState.PAYOUT_PUBLISHED); } private void setPayoutStateConfirmed() { - if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED); + if (!isPayoutConfirmed()) setPayoutState(PayoutState.PAYOUT_CONFIRMED); } private void setPayoutStateUnlocked() { - if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED); + if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() + .setOffer(offer.toProtoMessage()) + .setTxFeeAsLong(txFeeAsLong) + .setTakerFeeAsLong(takerFeeAsLong) + .setTakeOfferDate(takeOfferDate) + .setProcessModel(processModel.toProtoMessage()) + .setAmountAsLong(amountAsLong) + .setPrice(price) + .setState(Trade.State.toProtoMessage(state)) + .setPayoutState(Trade.PayoutState.toProtoMessage(payoutState)) + .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) + .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) + .addAllChatMessage(chatMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) + .collect(Collectors.toList())) + .setLockTime(lockTime) + .setUid(uid); + + Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); + Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage())); + Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); + Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash))); + Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); + Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); + Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); + Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); + Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); + Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey)); + Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name())); + return builder.build(); + } + + public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { + trade.setTakeOfferDate(proto.getTakeOfferDate()); + trade.setState(State.fromProto(proto.getState())); + trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState())); + trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); + trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); + trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); + trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex())); + trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey())); + trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); + trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); + trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); + trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); + trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); + trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); + trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState())); + trade.setLockTime(proto.getLockTime()); + trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData())); + + AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult()); + // We do not want to show the user the last pending state when he starts up the app again, so we clear it. + if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) { + persistedAssetTxProofResult = null; + } + trade.setAssetTxProofResult(persistedAssetTxProofResult); + + trade.chatMessages.addAll(proto.getChatMessageList().stream() + .map(ChatMessage::fromPayloadProto) + .collect(Collectors.toList())); + + return trade; } @Override diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 329df241a4..b35c18a853 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -34,6 +34,7 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.trade.Trade.DisputeState; import bisq.core.trade.Trade.Phase; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.handlers.TradeResultHandler; @@ -75,7 +76,6 @@ import bisq.common.proto.persistable.PersistedDataHost; import org.bitcoinj.core.Coin; import javax.inject.Inject; -import javax.inject.Named; import javafx.beans.property.BooleanProperty; import javafx.beans.property.LongProperty; @@ -96,9 +96,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -252,7 +249,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public void onAllServicesInitialized() { if (p2PService.isBootstrapped()) { - initPersistedTrades(); + new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override @@ -266,12 +263,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi onTradesChanged(); xmrWalletService.setTradeManager(this); - xmrWalletService.getAddressEntriesForAvailableBalanceStream() - .filter(addressEntry -> addressEntry.getOfferId() != null) - .forEach(addressEntry -> { - log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); - xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), XmrAddressEntry.Context.OFFER_FUNDING); - }); // thaw unreserved outputs thawUnreservedOutputs(); @@ -292,9 +283,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi trade.shutDown(); } catch (Exception e) { log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?"); + e.printStackTrace(); } }); - HavenoUtils.awaitTasks(tasks); + HavenoUtils.executeTasks(tasks); } private void thawUnreservedOutputs() { @@ -346,35 +338,41 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void initPersistedTrades() { + // get all trades // TODO: getAllTrades() + List trades = new ArrayList(); + trades.addAll(tradableList.getList()); + trades.addAll(closedTradableManager.getClosedTrades()); + trades.addAll(failedTradesManager.getObservableList()); + // open trades in parallel since each may open a multisig wallet - List trades = tradableList.getList(); - if (!trades.isEmpty()) { - ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, trades.size())); - for (Trade trade : trades) { - pool.submit(new Runnable() { - @Override - public void run() { - initPersistedTrade(trade); - } + int threadPoolSize = 10; + Set tasks = new HashSet(); + for (Trade trade : trades) { + tasks.add(new Runnable() { + @Override + public void run() { + initPersistedTrade(trade); + } + }); + }; + HavenoUtils.executeTasks(tasks, threadPoolSize); + + // reset any available address entries + xmrWalletService.getAddressEntriesForAvailableBalanceStream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> { + log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); + xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext()); }); - } - pool.shutdown(); - try { - if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); - } catch (InterruptedException e) { - pool.shutdownNow(); - throw new RuntimeException(e); - } - } persistedTradesInitialized.set(true); // We do not include failed trades as they should not be counted anyway in the trade statistics - Set allTrades = new HashSet<>(closedTradableManager.getClosedTrades()); - allTrades.addAll(tradableList.getList()); + Set nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades()); + nonFailedTrades.addAll(tradableList.getList()); String referralId = referralIdService.getOptionalReferralId().orElse(null); boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; - tradeStatisticsManager.maybeRepublishTradeStatistics(allTrades, referralId, isTorNetworkNode); + tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); } private void initPersistedTrade(Trade trade) { @@ -485,6 +483,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); removeTrade(trade); + if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); requestPersistence(); @@ -568,8 +567,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Maker error during trade initialization: " + errorMessage); - openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig removeTrade(trade); + openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); @@ -589,7 +588,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi Optional tradeOptional = getOpenTrade(request.getTradeId()); if (!tradeOptional.isPresent()) { - log.warn("No trade with id " + request.getTradeId()); + log.warn("No trade with id " + request.getTradeId() + " at node " + P2PService.getMyNodeAddress()); return; } Trade trade = tradeOptional.get(); @@ -751,8 +750,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); - errorMessageHandler.handleErrorMessage(errorMessage); removeTrade(trade); + errorMessageHandler.handleErrorMessage(errorMessage); + if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); requestPersistence(); } @@ -804,6 +804,7 @@ 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) { + if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed"); closedTradableManager.add(trade); trade.setState(Trade.State.TRADE_COMPLETED); removeTrade(trade); @@ -818,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // Dispute /////////////////////////////////////////////////////////////////////////////////////////// - public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) { + public void closeDisputedTrade(String tradeId, DisputeState disputeState) { Optional tradeOptional = getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); @@ -911,9 +912,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi 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. " + - "That should never happen. trade ID=" + trade.getId()); + "That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } } else { + log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } @@ -923,9 +925,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); } else { log.warn("We found a closed trade with locked up funds. " + - "That should never happen. trade ID=" + trade.getId()); + "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } } else { + log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } return trade.getId(); @@ -1026,7 +1029,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private synchronized void removeTrade(Trade trade) { - log.info("TradeManager.removeTrade()"); + log.info("TradeManager.removeTrade() " + trade.getId()); synchronized(tradableList) { if (!tradableList.contains(trade)) return; diff --git a/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java b/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java index e3c313aae8..9d81305a40 100644 --- a/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java @@ -19,7 +19,6 @@ package bisq.core.trade.messages; import bisq.core.proto.CoreProtoResolver; -import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; import com.google.protobuf.ByteString; import java.util.Optional; @@ -33,7 +32,7 @@ import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value -public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage { +public final class DepositsConfirmedMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; private final PubKeyRing pubKeyRing; @Nullable diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java index 57c80594f9..c255517532 100644 --- a/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java @@ -29,14 +29,18 @@ import java.util.Optional; import java.util.UUID; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import com.google.protobuf.ByteString; + @Slf4j @EqualsAndHashCode(callSuper = true) -@Value +@Getter public final class PaymentReceivedMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; @Nullable @@ -44,7 +48,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { @Nullable private final String signedPayoutTxHex; private final String updatedMultisigHex; - private final boolean sawArrivedPaymentReceivedMsg; + private final boolean deferPublishPayout; + private final PaymentSentMessage paymentSentMessage; + @Setter + @Nullable + private byte[] sellerSignature; // Added in v1.4.0 @Nullable @@ -56,7 +64,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { String unsignedPayoutTxHex, String signedPayoutTxHex, String updatedMultisigHex, - boolean sawArrivedPaymentReceivedMsg) { + boolean deferPublishPayout, + PaymentSentMessage paymentSentMessage) { this(tradeId, senderNodeAddress, signedWitness, @@ -65,7 +74,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { unsignedPayoutTxHex, signedPayoutTxHex, updatedMultisigHex, - sawArrivedPaymentReceivedMsg); + deferPublishPayout, + paymentSentMessage); } @@ -81,14 +91,16 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { String unsignedPayoutTxHex, String signedPayoutTxHex, String updatedMultisigHex, - boolean sawArrivedPaymentReceivedMsg) { + boolean deferPublishPayout, + PaymentSentMessage paymentSentMessage) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.signedWitness = signedWitness; this.unsignedPayoutTxHex = unsignedPayoutTxHex; this.signedPayoutTxHex = signedPayoutTxHex; this.updatedMultisigHex = updatedMultisigHex; - this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg; + this.deferPublishPayout = deferPublishPayout; + this.paymentSentMessage = paymentSentMessage; } @Override @@ -97,11 +109,13 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { .setTradeId(tradeId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) - .setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg); + .setDeferPublishPayout(deferPublishPayout); Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex)); + Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); + Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e))); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); } @@ -112,7 +126,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ? SignedWitness.fromProto(protoSignedWitness) : null; - return new PaymentReceivedMessage(proto.getTradeId(), + PaymentReceivedMessage message = new PaymentReceivedMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), signedWitness, proto.getUid(), @@ -120,18 +134,23 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), - proto.getSawArrivedPaymentReceivedMsg()); + proto.getDeferPublishPayout(), + proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); + message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature())); + return message; } @Override public String toString() { - return "SellerReceivedPaymentMessage{" + + return "PaymentReceivedMessage{" + "\n senderNodeAddress=" + senderNodeAddress + ",\n signedWitness=" + signedWitness + ",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex + ",\n signedPayoutTxHex=" + signedPayoutTxHex + ",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + - ",\n sawArrivedPaymentReceivedMsg=" + sawArrivedPaymentReceivedMsg + + ",\n deferPublishPayout=" + deferPublishPayout + + ",\n paymentSentMessage=" + paymentSentMessage + + ",\n sellerSignature=" + sellerSignature + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java index b482a4aa20..8e6d6e908e 100644 --- a/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java @@ -25,12 +25,13 @@ import bisq.common.proto.ProtoUtil; import java.util.Optional; import lombok.EqualsAndHashCode; -import lombok.Value; +import lombok.Getter; +import lombok.Setter; import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) -@Value +@Getter public final class PaymentSentMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; @Nullable @@ -41,6 +42,9 @@ public final class PaymentSentMessage extends TradeMailboxMessage { private final String updatedMultisigHex; @Nullable private final byte[] paymentAccountKey; + @Setter + @Nullable + private byte[] buyerSignature; // Added after v1.3.7 // We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets. @@ -101,13 +105,14 @@ public final class PaymentSentMessage extends TradeMailboxMessage { Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); + Optional.ofNullable(buyerSignature).ifPresent(e -> builder.setBuyerSignature(ByteString.copyFrom(e))); return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build(); } public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto, String messageVersion) { - return new PaymentSentMessage(proto.getTradeId(), + PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()), @@ -117,6 +122,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage { ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()) ); + message.setBuyerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getBuyerSignature())); + return message; } @@ -130,6 +137,7 @@ public final class PaymentSentMessage extends TradeMailboxMessage { ",\n payoutTxHex=" + payoutTxHex + ",\n updatedMultisigHex=" + updatedMultisigHex + ",\n paymentAccountKey=" + paymentAccountKey + + ",\n buyerSignature=" + buyerSignature + "\n} " + super.toString(); } } 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 9bdc8fa952..e6f70305e1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java @@ -113,7 +113,7 @@ public class ArbitratorProtocol extends DisputeProtocol { @SuppressWarnings("unchecked") @Override - public Class[] getDepsitsConfirmedTasks() { + public Class[] getDepositsConfirmedTasks() { return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class }; } } 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 fbd8a0c11d..77b7e60bf9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -19,19 +19,11 @@ package bisq.core.trade.protocol; import bisq.core.trade.BuyerAsMakerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.DepositResponse; -import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.DepositsConfirmedMessage; -import bisq.core.trade.messages.PaymentReceivedMessage; -import bisq.core.trade.messages.SignContractRequest; -import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; -import bisq.common.handlers.ResultHandler; -import bisq.common.taskrunner.Task; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -45,10 +37,6 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol super(trade); } - /////////////////////////////////////////////////////////////////////////////////////////// - // MakerProtocol - /////////////////////////////////////////////////////////////////////////////////////////// - @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, @@ -80,49 +68,4 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol } }).start(); } - - @Override - public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - super.handleInitMultisigRequest(request, sender); - } - - @Override - public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - super.handleSignContractRequest(message, sender); - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - super.handleSignContractResponse(message, sender); - } - - @Override - public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - super.handleDepositResponse(response, sender); - } - - @Override - public void handle(DepositsConfirmedMessage request, NodeAddress sender) { - super.handle(request, sender); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which events we expect - @Override - public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - super.onPaymentStarted(resultHandler, errorMessageHandler); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message Payout tx - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which messages we expect - @Override - protected void handle(PaymentReceivedMessage message, NodeAddress peer) { - super.handle(message, peer); - } } 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 a23fd1bf78..f55a971111 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -92,60 +92,6 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol }).start(); } - @Override - public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - super.handleInitMultisigRequest(request, sender); - } - - @Override - public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - super.handleSignContractRequest(message, sender); - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - super.handleSignContractResponse(message, sender); - } - - @Override - public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - super.handleDepositResponse(response, sender); - } - - @Override - public void handle(DepositsConfirmedMessage request, NodeAddress sender) { - super.handle(request, sender); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which events we expect - @Override - public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - super.onPaymentStarted(resultHandler, errorMessageHandler); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message Payout tx - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which messages we expect - @Override - protected void handle(PaymentReceivedMessage message, NodeAddress peer) { - super.handle(message, peer); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Message dispatcher - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void onTradeMessage(TradeMessage message, NodeAddress peer) { - super.onTradeMessage(message, peer); - } - @Override protected void handleError(String errorMessage) { trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId()); 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 fe900e5d31..c7722f9324 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -25,7 +25,8 @@ import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; -import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; +import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator; +import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller; import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; @@ -58,7 +59,9 @@ public class BuyerProtocol extends DisputeProtocol { given(anyPhase(Trade.Phase.PAYMENT_SENT) .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) .with(BuyerEvent.STARTUP)) - .setup(tasks(BuyerSendPaymentSentMessage.class)) + .setup(tasks( + BuyerSendPaymentSentMessageToSeller.class, + BuyerSendPaymentSentMessageToArbitrator.class)) .executeTasks(); } @@ -93,10 +96,9 @@ public class BuyerProtocol extends DisputeProtocol { .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks(ApplyFilter.class, - //UpdateMultisigWithTradingPeer.class, // TODO (woodser): can use this to test protocol with updated multisig from peer. peer should attempt to send updated multisig hex earlier as part of protocol. cannot use with countdown latch because response comes back in a separate thread and blocks on trade BuyerPreparePaymentSentMessage.class, - //BuyerSetupPayoutTxListener.class, - BuyerSendPaymentSentMessage.class) // don't latch trade because this blocks and runs in background + BuyerSendPaymentSentMessageToSeller.class, + BuyerSendPaymentSentMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { this.errorMessageHandler = null; @@ -119,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol { @SuppressWarnings("unchecked") @Override - public Class[] getDepsitsConfirmedTasks() { + public Class[] getDepositsConfirmedTasks() { return new Class[] { SendDepositsConfirmedMessageToArbitrator.class }; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index 3debcc3d96..5ae14d3516 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -20,18 +20,11 @@ package bisq.core.trade.protocol; import bisq.core.trade.SellerAsMakerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PaymentSentMessage; -import bisq.core.trade.messages.SignContractRequest; -import bisq.core.trade.messages.SignContractResponse; -import bisq.core.trade.messages.DepositResponse; -import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; -import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; @@ -81,53 +74,4 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc } }).start(); } - - @Override - public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - super.handleInitMultisigRequest(request, sender); - } - - @Override - public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - super.handleSignContractRequest(message, sender); - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - super.handleSignContractResponse(message, sender); - } - - @Override - public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - super.handleDepositResponse(response, sender); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which events we expect - @Override - public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - super.onPaymentReceived(resultHandler, errorMessageHandler); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Massage dispatcher - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void onTradeMessage(TradeMessage message, NodeAddress peer) { - super.onTradeMessage(message, peer); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message when buyer has clicked payment started button - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which messages we expect - @Override - protected void handle(PaymentSentMessage message, NodeAddress peer) { - super.handle(message, peer); - } } 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 3131216574..4745cdec5c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -22,18 +22,10 @@ import bisq.core.offer.Offer; import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.handlers.TradeResultHandler; -import bisq.core.trade.messages.PaymentSentMessage; -import bisq.core.trade.messages.SignContractRequest; -import bisq.core.trade.messages.SignContractResponse; -import bisq.core.trade.messages.DepositResponse; -import bisq.core.trade.messages.InitMultisigRequest; -import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds; import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; -import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; -import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; @@ -90,55 +82,6 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc }).start(); } - @Override - public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - super.handleInitMultisigRequest(request, sender); - } - - @Override - public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - super.handleSignContractRequest(message, sender); - } - - @Override - public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - super.handleSignContractResponse(message, sender); - } - - @Override - public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - super.handleDepositResponse(response, sender); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message when buyer has clicked payment started button - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which messages we expect - @Override - protected void handle(PaymentSentMessage message, NodeAddress peer) { - super.handle(message, peer); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // User interaction - /////////////////////////////////////////////////////////////////////////////////////////// - - // We keep the handler here in as well to make it more transparent which events we expect - @Override - public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - super.onPaymentReceived(resultHandler, errorMessageHandler); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Massage dispatcher - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected void onTradeMessage(TradeMessage message, NodeAddress peer) { - super.onTradeMessage(message, peer); - } - @Override protected void handleError(String errorMessage) { trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId()); 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 e0104b12ba..2fe57c5ad3 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -24,10 +24,10 @@ import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; -import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator; import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; +import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; @@ -54,18 +54,11 @@ public class SellerProtocol extends DisputeProtocol { @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); - if (message instanceof PaymentSentMessage) { - handle((PaymentSentMessage) message, peer); - } } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { super.onMailboxMessage(message, peerNodeAddress); - - if (message instanceof PaymentSentMessage) { - handle((PaymentSentMessage) message, peerNodeAddress); - } } @Override @@ -74,52 +67,6 @@ public class SellerProtocol extends DisputeProtocol { } - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message when buyer has clicked payment started button - /////////////////////////////////////////////////////////////////////////////////////////// - - protected void handle(PaymentSentMessage message, NodeAddress peer) { - log.info("SellerProtocol.handle(PaymentSentMessage)"); - new Thread(() -> { - // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case - // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received - // a mailbox message with PaymentSentMessage. - // 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. - synchronized (trade) { - if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Ignoring PaymentSentMessage which was already processed"); - return; - } - latchTrade(); - expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) - .with(message) - .from(peer) - .preCondition(trade.getPayoutTx() == null, - () -> { - log.warn("We received a PaymentSentMessage 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( - ApplyFilter.class, - SellerProcessPaymentSentMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - (errorMessage) -> { - stopTimeout(); - handleTaskRunnerFault(peer, message, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }).start(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // User interaction /////////////////////////////////////////////////////////////////////////////////////////// @@ -160,7 +107,7 @@ public class SellerProtocol extends DisputeProtocol { @SuppressWarnings("unchecked") @Override - public Class[] getDepsitsConfirmedTasks() { - return new Class[] { SendDepositsConfirmedMessageToBuyer.class }; + public Class[] getDepositsConfirmedTasks() { + return new Class[] { SendDepositsConfirmedMessageToArbitrator.class, SendDepositsConfirmedMessageToBuyer.class }; } } 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 fe798b6778..0273859e94 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -23,6 +23,7 @@ import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.HavenoUtils; +import bisq.core.trade.SellerTrade; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.DepositResponse; @@ -33,8 +34,10 @@ import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.RemoveOffer; +import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.FluentProtocol.Condition; +import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest; import bisq.core.trade.protocol.tasks.ProcessDepositResponse; import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; @@ -92,13 +95,15 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D /////////////////////////////////////////////////////////////////////////////////////////// - // Dispatcher + // Message dispatching /////////////////////////////////////////////////////////////////////////////////////////// protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); if (message instanceof DepositsConfirmedMessage) { handle((DepositsConfirmedMessage) message, peerNodeAddress); + } else if (message instanceof PaymentSentMessage) { + handle((PaymentSentMessage) message, peerNodeAddress); } else if (message instanceof PaymentReceivedMessage) { handle((PaymentReceivedMessage) message, peerNodeAddress); } @@ -108,49 +113,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); if (message instanceof DepositsConfirmedMessage) { handle((DepositsConfirmedMessage) message, peerNodeAddress); + } else if (message instanceof PaymentSentMessage) { + handle((PaymentSentMessage) message, peerNodeAddress); } else if (message instanceof PaymentReceivedMessage) { handle((PaymentReceivedMessage) message, peerNodeAddress); } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) { - processModel.applyTransient(serviceProvider, tradeManager, offer); - onInitialized(); - } - - protected void onInitialized() { - if (!trade.isCompleted()) { - processModel.getP2PService().addDecryptedDirectMessageListener(this); - } - - // handle trade events - EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage(); - }); - - // initialize trade - trade.initialize(processModel.getProvider()); - - // process mailbox messages - MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); - mailboxMessageService.addDecryptedMailboxListener(this); - handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); - } - - public void onWithdrawCompleted() { - log.info("Withdraw completed"); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // DecryptedDirectMessageListener - /////////////////////////////////////////////////////////////////////////////////////////// - @Override public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); @@ -176,11 +145,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // DecryptedMailboxListener - /////////////////////////////////////////////////////////////////////////////////////////// - @Override public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return; @@ -240,10 +204,34 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D /////////////////////////////////////////////////////////////////////////////////////////// - // Abstract + // API /////////////////////////////////////////////////////////////////////////////////////////// - public abstract Class[] getDepsitsConfirmedTasks(); + public abstract Class[] getDepositsConfirmedTasks(); + + public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) { + processModel.applyTransient(serviceProvider, tradeManager, offer); + onInitialized(); + } + + protected void onInitialized() { + if (!trade.isCompleted()) { + processModel.getP2PService().addDecryptedDirectMessageListener(this); + } + + // handle trade events + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage(); + }); + + // initialize trade + trade.initialize(processModel.getProvider()); + + // process mailbox messages + MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); + mailboxMessageService.addDecryptedMailboxListener(this); + handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); + } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()"); @@ -398,6 +386,53 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }).start(); } + // received by seller and arbitrator + protected void handle(PaymentSentMessage message, NodeAddress peer) { + System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage)"); + if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); + return; + } + new Thread(() -> { + // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case + // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received + // a mailbox message with PaymentSentMessage. + // 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. + synchronized (trade) { + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { + log.warn("Ignoring PaymentSentMessage which was already processed"); + return; + } + latchTrade(); + expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) + .with(message) + .from(peer) + .preCondition(trade.getPayoutTx() == null, + () -> { + log.warn("We received a PaymentSentMessage 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( + ApplyFilter.class, + ProcessPaymentSentMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(peer, message); + }, + (errorMessage) -> { + stopTimeout(); + handleTaskRunnerFault(peer, message, errorMessage); + }))) + .executeTasks(true); + awaitTradeLatch(); + } + }).start(); + } + // received by buyer and arbitrator protected void handle(PaymentReceivedMessage message, NodeAddress peer) { System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)"); @@ -410,7 +445,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); - expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED }) + expect(anyPhase(trade.isBuyer() ? new Trade.Phase[] {Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED} : new Trade.Phase[] {Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT}) .with(message) .from(peer)) .setup(tasks( @@ -427,6 +462,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } + public void onWithdrawCompleted() { + log.info("Withdraw completed"); + } + /////////////////////////////////////////////////////////////////////////////////////////// // FluentProtocol /////////////////////////////////////////////////////////////////////////////////////////// @@ -590,15 +629,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // Validation /////////////////////////////////////////////////////////////////////////////////////////// - private PubKeyRing getPeersPubKeyRing(NodeAddress peer) { + private PubKeyRing getPeersPubKeyRing(NodeAddress address) { trade.setMyNodeAddress(); // TODO: this is a hack to update my node address before verifying the message - if (peer.equals(trade.getArbitrator().getNodeAddress())) return trade.getArbitrator().getPubKeyRing(); - else if (peer.equals(trade.getMaker().getNodeAddress())) return trade.getMaker().getPubKeyRing(); - else if (peer.equals(trade.getTaker().getNodeAddress())) return trade.getTaker().getPubKeyRing(); - else { + TradingPeer peer = trade.getTradingPeer(address); + if (peer == null) { log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + peer); return null; } + return peer.getPubKeyRing(); } private boolean isPubKeyValid(DecryptedMessageWithPubKey message) { @@ -707,7 +745,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D synchronized (trade) { latchTrade(); expect(new Condition(trade)) - .setup(tasks(getDepsitsConfirmedTasks()) + .setup(tasks(getDepositsConfirmedTasks()) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages"); diff --git a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java index 531acd3073..1ece9cc237 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -20,7 +20,9 @@ package bisq.core.trade.protocol; import bisq.core.btc.model.RawTransactionInput; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.network.p2p.NodeAddress; +import bisq.common.app.Version; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistablePayload; @@ -124,6 +126,8 @@ public final class TradingPeer implements PersistablePayload { private String depositTxKey; @Nullable private String updatedMultisigHex; + @Nullable + private PaymentSentMessage paymentSentMessage; public TradingPeer() { } @@ -163,6 +167,7 @@ public final class TradingPeer implements PersistablePayload { Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); + Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); builder.setCurrentDate(currentDate); return builder.build(); @@ -211,6 +216,7 @@ public final class TradingPeer implements PersistablePayload { tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); + tradingPeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null); return tradingPeer; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index c2fc12abbe..1a39a5ea0a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -48,83 +48,87 @@ public class ArbitratorProcessDepositRequest extends TradeTask { @Override protected void run() { try { - runInterceptHook(); + runInterceptHook(); + + // get contract and signature + String contractAsJson = trade.getContractAsJson(); + DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response + String signature = request.getContractSignature(); + + // get peer info + TradingPeer peer = trade.getTradingPeer(request.getSenderNodeAddress()); + if (peer == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); + PubKeyRing peerPubKeyRing = peer.getPubKeyRing(); + + // verify signature + if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid"); - // get contract and signature - String contractAsJson = trade.getContractAsJson(); - DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response - String signature = request.getContractSignature(); + // set peer's signature + peer.setContractSignature(signature); - // get peer info - TradingPeer peer = trade.getTradingPeer(request.getSenderNodeAddress()); - if (peer == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); - PubKeyRing peerPubKeyRing = peer.getPubKeyRing(); + // collect expected values of deposit tx + Offer offer = trade.getOffer(); + boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress()); + boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY; + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); + String depositAddress = processModel.getMultisigAddress(); + BigInteger tradeFee; + TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress()); + if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee()); + else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); + else throw new RuntimeException("DepositRequest is not from maker or taker"); - // verify signature - if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid"); + // verify deposit tx + try { + trade.getXmrWalletService().verifyTradeTx(depositAddress, + depositAmount, + tradeFee, + trader.getDepositTxHash(), + request.getDepositTxHex(), + request.getDepositTxKey(), + null, + false); + } catch (Exception e) { + throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } - // set peer's signature - peer.setContractSignature(signature); + // set deposit info + trader.setDepositTxHex(request.getDepositTxHex()); + trader.setDepositTxKey(request.getDepositTxKey()); + if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); - // collect expected values of deposit tx - Offer offer = trade.getOffer(); - boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress()); - boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY; - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); - String depositAddress = processModel.getMultisigAddress(); - BigInteger tradeFee; - TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress()); - if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee()); - else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); - else throw new RuntimeException("DepositRequest is not from maker or taker"); + // 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) { - // verify deposit tx - trade.getXmrWalletService().verifyTradeTx(depositAddress, - depositAmount, - tradeFee, - trader.getDepositTxHash(), - request.getDepositTxHex(), - request.getDepositTxKey(), - null, - false); - - // set deposit info - trader.setDepositTxHex(request.getDepositTxHex()); - trader.setDepositTxKey(request.getDepositTxKey()); - if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); - - // 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 - MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - 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 txs + MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); + 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()); - // update trade state - log.info("Arbitrator submitted deposit txs for trade " + trade.getId()); - trade.setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); + // update trade state + log.info("Arbitrator submitted deposit txs for trade " + trade.getId()); + trade.setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); - // create deposit response - DepositResponse response = new DepositResponse( - trade.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime()); + // 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.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response); - sendDepositResponse(trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), response); - } else { - if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); - if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); - } + // send deposit response to maker and taker + sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response); + sendDepositResponse(trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), response); + } else { + if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); + if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); + } - // TODO (woodser): request persistence? - complete(); + // TODO (woodser): request persistence? + complete(); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index 957839efac..6c1c7349ff 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -54,7 +54,8 @@ public class ArbitratorProcessReserveTx extends TradeTask { // process reserve tx with expected terms BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee()); BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); - trade.getXmrWalletService().verifyTradeTx( + try { + trade.getXmrWalletService().verifyTradeTx( request.getPayoutAddress(), depositAmount, tradeFee, @@ -63,6 +64,9 @@ public class ArbitratorProcessReserveTx extends TradeTask { request.getReserveTxKey(), null, true); + } catch (Exception e) { + throw new RuntimeException("Error processing reserve tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } // save reserve tx to model TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index b6c9f8cee0..b955791824 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -75,7 +75,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { // create payout tx if we have seller's updated multisig hex if (trade.getSeller().getUpdatedMultisigHex() != null) { - // create payout tx + // create payout tx log.info("Buyer creating unsigned payout tx"); MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index e15b6a1b08..f00d079615 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -21,11 +21,18 @@ import bisq.core.network.MessageState; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.util.JsonUtil; +import bisq.network.p2p.NodeAddress; + +import com.google.common.base.Charsets; + import bisq.common.Timer; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; import javafx.beans.value.ChangeListener; - +import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; /** @@ -38,8 +45,8 @@ import lombok.extern.slf4j.Slf4j; * online he will process it. */ @Slf4j -public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { - private PaymentSentMessage message; +@EqualsAndHashCode(callSuper = true) +public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; @@ -47,16 +54,34 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { super(taskHandler, trade); } + protected abstract NodeAddress getReceiverNodeAddress(); + + protected abstract PubKeyRing getReceiverPubKeyRing(); + + @Override + protected void run() { + try { + runInterceptHook(); + super.run(); + } catch (Throwable t) { + failed(t); + } finally { + cleanup(); + } + } + @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { - if (message == null) { + if (trade.getSelf().getPaymentSentMessage() == null) { // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress(); - message = new PaymentSentMessage( + + // create payment sent message + PaymentSentMessage message = new PaymentSentMessage( tradeId, processModel.getMyNodeAddress(), trade.getCounterCurrencyTxId(), @@ -66,8 +91,18 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getPaymentAccountKey() ); + + // sign message + try { + String messageAsJson = JsonUtil.objectToJson(message); + byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8)); + message.setBuyerSignature(sig); + trade.getSelf().setPaymentSentMessage(message); + } catch (Exception e) { + throw new RuntimeException (e); + } } - return message; + return trade.getSelf().getPaymentSentMessage(); } @Override @@ -96,18 +131,6 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { processModel.getTradeManager().requestPersistence(); } - @Override - protected void run() { - try { - runInterceptHook(); - super.run(); - } catch (Throwable t) { - failed(t); - } finally { - cleanup(); - } - } - private void cleanup() { if (timer != null) { timer.stop(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java new file mode 100644 index 0000000000..1e8214faf5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java @@ -0,0 +1,63 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSentMessage { + + public BuyerSendPaymentSentMessageToArbitrator(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected NodeAddress getReceiverNodeAddress() { + return trade.getArbitrator().getNodeAddress(); + } + + protected PubKeyRing getReceiverPubKeyRing() { + return trade.getArbitrator().getPubKeyRing(); + } + + @Override + protected void setStateSent() { + complete(); // don't wait for message to arbitrator + } + + @Override + protected void setStateFault() { + // state only updated on seller message + } + + @Override + protected void setStateStoredInMailbox() { + // state only updated on seller message + } + + @Override + protected void setStateArrived() { + // state only updated on seller message + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java new file mode 100644 index 0000000000..236fd86b8e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java @@ -0,0 +1,52 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMessage { + + public BuyerSendPaymentSentMessageToSeller(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected NodeAddress getReceiverNodeAddress() { + return trade.getSeller().getNodeAddress(); + } + + protected PubKeyRing getReceiverPubKeyRing() { + return trade.getSeller().getPubKeyRing(); + } + + // continue execution on fault so payment sent message is sent to arbitrator + @Override + protected void onFault(String errorMessage, TradeMessage message) { + setStateFault(); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + complete(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/MakerSendInitTradeRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/MakerSendInitTradeRequest.java index 253a1909a3..834f84792c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/MakerSendInitTradeRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/MakerSendInitTradeRequest.java @@ -53,7 +53,7 @@ public class MakerSendInitTradeRequest extends TradeTask { checkNotNull(makerRequest); checkTradeId(processModel.getOfferId(), makerRequest); - // maker signs offer id as nonce to avoid challenge protocol // TODO (woodser): is this necessary? + // maker signs offer id as nonce to avoid challenge protocol // TODO: how is this used? Offer offer = processModel.getOffer(); byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8)); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index bd64221d48..ac55b6b466 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -35,34 +35,33 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { @Override protected void run() { try { - runInterceptHook(); + runInterceptHook(); - // get sender based on the pub key - // TODO: trade.getTradingPeer(PubKeyRing) - DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); - TradingPeer sender; - if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator(); - else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer(); - else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller(); - else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); - - // update peer node address - sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + // get peer + DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); + TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing()); + if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); + + // update peer node address + sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + if (sender.getNodeAddress().equals(trade.getBuyer().getNodeAddress()) && sender != trade.getBuyer()) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses + if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null); + if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null); - // decrypt seller payment account payload if key given - if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) { - log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey()); - trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); + // store updated multisig hex for processing on payment sent + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); + + // decrypt seller payment account payload if key given + if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) { + log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey()); + trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); + } + + // persist and complete + processModel.getTradeManager().requestPersistence(); + complete(); + } catch (Throwable t) { + failed(t); } - - // store updated multisig hex for processing on payment sent - sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - - // persist and complete - processModel.getTradeManager().requestPersistence(); - complete(); - } catch (Throwable t) { - failed(t); - } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java index 2672ea0bc8..58e3f512c6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -55,9 +55,6 @@ public class ProcessInitTradeRequest extends TradeTask { checkNotNull(request); checkTradeId(processModel.getOfferId(), request); - System.out.println("PROCESS INIT TRADE REQUEST"); - System.out.println(request); - // handle request as arbitrator TradingPeer multisigParticipant; if (trade instanceof ArbitratorTrade) { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 94b7f2cab4..1c980fcd7a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -18,8 +18,8 @@ package bisq.core.trade.protocol.tasks; import bisq.core.account.sign.SignedWitness; -import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.ArbitratorTrade; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.util.Validator; @@ -27,11 +27,13 @@ import common.utils.GenUtils; import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; -import monero.wallet.MoneroWallet; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import java.util.ArrayList; +import java.util.List; + @Slf4j public class ProcessPaymentReceivedMessage extends TradeTask { public ProcessPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { @@ -48,26 +50,34 @@ public class ProcessPaymentReceivedMessage extends TradeTask { checkNotNull(message); checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided"); + // verify signature of payment received message + HavenoUtils.verifyPaymentReceivedMessage(trade, message); + trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex()); + // update to the latest peer address of our peer if the message is correct trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); - if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses + if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses + + // import multisig hex + List updatedMultisigHexes = new ArrayList(); + if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex()); + if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex()); + if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually + + // sync and save wallet + trade.syncWallet(); + trade.saveWallet(); // handle if payout tx not published if (!trade.isPayoutPublished()) { - // import multisig hex - MoneroWallet multisigWallet = trade.getWallet(); - if (message.getUpdatedMultisigHex() != null) { - multisigWallet.importMultisigHex(message.getUpdatedMultisigHex()); - trade.saveWallet(); - } - - // arbitrator waits for buyer to sign and broadcast payout tx if message arrived + // wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer) boolean isSigned = message.getSignedPayoutTxHex() != null; - if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) { - log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName()); - GenUtils.waitFor(30000); - multisigWallet.rescanSpent(); + if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) { + log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); + GenUtils.waitFor(Trade.DEFER_PUBLISH_MS); + trade.syncWallet(); } // verify and publish payout tx @@ -77,11 +87,16 @@ public class ProcessPaymentReceivedMessage extends TradeTask { trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); } else { log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName()); - trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); + try { + trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); + } catch (Exception e) { + if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); + else throw e; + } } } } else { - log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId()); + log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); } SignedWitness signedWitness = message.getSignedWitness(); @@ -93,7 +108,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { } // complete - if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published + trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentSentMessage.java similarity index 70% rename from core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 6526f64a72..3498887474 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -20,14 +20,15 @@ package bisq.core.trade.protocol.tasks; import static com.google.common.base.Preconditions.checkNotNull; import bisq.common.taskrunner.TaskRunner; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.util.Validator; import lombok.extern.slf4j.Slf4j; @Slf4j -public class SellerProcessPaymentSentMessage extends TradeTask { - public SellerProcessPaymentSentMessage(TaskRunner taskHandler, Trade trade) { +public class ProcessPaymentSentMessage extends TradeTask { + public ProcessPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -40,28 +41,26 @@ public class SellerProcessPaymentSentMessage extends TradeTask { Validator.checkTradeId(processModel.getOfferId(), message); checkNotNull(message); - // store buyer info + // verify signature of payment sent message + HavenoUtils.verifyPaymentSentMessage(trade, message); + + // update buyer info trade.setPayoutTxHex(message.getPayoutTxHex()); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + trade.getBuyer().setPaymentSentMessage(message); - // decrypt buyer's payment account payload - trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); + // if seller, decrypt buyer's payment account payload + if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + // set state String counterCurrencyTxId = message.getCounterCurrencyTxId(); - if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) { - trade.setCounterCurrencyTxId(counterCurrencyTxId); - } - + if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId); String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); - if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) { - trade.setCounterCurrencyExtraData(counterCurrencyExtraData); - } - - trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - + if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData); + trade.setStateIfProgress(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { 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 f3f9fd768a..fdad9d61bc 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 @@ -107,7 +107,7 @@ public class ProcessSignContractResponse extends TradeTask { trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST); processModel.getTradeManager().requestPersistence(); } else { - log.info("Waiting for more contract signatures to send deposit request"); + log.info("Waiting for another contract signatures to send deposit request"); complete(); // does not yet have needed signatures } } catch (Throwable t) { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index 9a8907db42..1a5b27db91 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -63,11 +63,6 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - - // export multisig hex once - if (trade.getSelf().getUpdatedMultisigHex() == null) { - trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); - } } processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index 85b4d6f2b2..62bdfedf1a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -21,8 +21,10 @@ import bisq.core.account.sign.SignedWitness; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.core.util.JsonUtil; import bisq.network.p2p.NodeAddress; import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; import lombok.EqualsAndHashCode; @@ -30,10 +32,13 @@ import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; -@EqualsAndHashCode(callSuper = true) +import com.google.common.base.Charsets; + @Slf4j +@EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { SignedWitness signedWitness = null; + PaymentReceivedMessage message = null; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -47,13 +52,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag protected void run() { try { runInterceptHook(); - - if (trade.getPayoutTxHex() == null) { - log.error("Payout tx is null"); - failed("Payout tx is null"); - return; - } - super.run(); } catch (Throwable t) { failed(t); @@ -63,23 +61,37 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); + if (message == null) { - // TODO: sign witness - // AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); - // if (accountAgeWitnessService.isSignWitnessTrade(trade)) { - // // Broadcast is done in accountAgeWitness domain. - // accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); - // } + // TODO: sign witness + // AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); + // if (accountAgeWitnessService.isSignWitnessTrade(trade)) { + // // Broadcast is done in accountAgeWitness domain. + // accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); + // } - return new PaymentReceivedMessage( - tradeId, - processModel.getMyNodeAddress(), - signedWitness, - trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned - trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed - trade.getSelf().getUpdatedMultisigHex(), - trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout - ); + // TODO: create with deterministic id like BuyerSendPaymentSentMessage + message = new PaymentReceivedMessage( + tradeId, + processModel.getMyNodeAddress(), + signedWitness, + trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned + trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed + trade.getSelf().getUpdatedMultisigHex(), + trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout + trade.getBuyer().getPaymentSentMessage() + ); + + // sign message + try { + String messageAsJson = JsonUtil.objectToJson(message); + byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8)); + message.setSellerSignature(sig); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return message; } @Override diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index 15235760e4..cc3bce8aa9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -65,6 +65,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId); trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); + processModel.getTradeManager().requestPersistence(); } // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java index 252316af0b..847958fe5f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendMailboxMessageTask.java @@ -63,6 +63,7 @@ public abstract class SendMailboxMessageTask extends TradeTask { log.info("Send {} to peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + TradeTask task = this; processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( peersNodeAddress, getReceiverPubKeyRing(), @@ -72,7 +73,7 @@ public abstract class SendMailboxMessageTask extends TradeTask { public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); setStateArrived(); - complete(); + if (!task.isCompleted()) complete(); } @Override @@ -95,7 +96,7 @@ public abstract class SendMailboxMessageTask extends TradeTask { protected void onStoredInMailbox() { setStateStoredInMailbox(); - complete(); + if (!isCompleted()) complete(); } protected void onFault(String errorMessage, TradeMessage message) { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java index 0ff741fdf0..037ea61006 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java @@ -19,11 +19,8 @@ package bisq.core.trade.protocol.tasks; import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; -import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; -import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.trade.Trade; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendDirectMessageListener; import java.util.HashSet; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 1d0aace4e6..9626ff820b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1842,7 +1842,7 @@ disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\ Open trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ -No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions +A dispute has been opened with the arbitrator. You can chat with the arbitrator in the "Support" tab to resolve the dispute. disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 210bb0ec9d..6153b1dfc7 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -212,13 +212,10 @@ public class AccountAgeWitnessServiceTest { "summary", null, null, - null, - null, 100000, 0, null, - now - 1, - false)); + now - 1)); // Filtermanager says nothing is filtered when(filterManager.isNodeAddressBanned(any())).thenReturn(false); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java index 7d28c26676..deaca5053a 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java @@ -65,7 +65,7 @@ public class GrpcDisputesService extends DisputesImplBase { }, (errorMessage, throwable) -> { log.info("Error in openDispute" + errorMessage); - exceptionHandler.handleException(log, throwable, responseObserver); + exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); @@ -82,7 +82,7 @@ public class GrpcDisputesService extends DisputesImplBase { responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { - exceptionHandler.handleException(log, cause, responseObserver); + exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".getDispute", cause, responseObserver); } } @@ -115,7 +115,7 @@ public class GrpcDisputesService extends DisputesImplBase { responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { - exceptionHandler.handleException(log, cause, responseObserver); + exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".resolveDispute", cause, responseObserver); } } @@ -149,7 +149,7 @@ public class GrpcDisputesService extends DisputesImplBase { put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); }} ))); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index c16c444ce6..307247a3cb 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -206,7 +206,7 @@ class GrpcOffersService extends OffersImplBase { put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); - put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); }} ))); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 415ed0c28c..a2d7182d85 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -244,9 +244,9 @@ class GrpcTradesService extends TradesImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(30, SECONDS)); put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 21c3b2e39e..3e009eda47 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -189,9 +189,7 @@ class GrpcWalletsService extends WalletsImplBase { .stream() .map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount()))) .collect(Collectors.toList())); - log.info("Successfully created XMR tx: hash {}, metadata {}", - tx.getHash(), - tx.getMetadata()); + log.info("Successfully created XMR tx: hash {}", tx.getHash()); var reply = CreateXmrTxReply.newBuilder() .setTx(toXmrTx(tx).toProtoMessage()) .build(); diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index 2a4c3d616e..539084c063 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; import bisq.core.trade.protocol.tasks.MakerSetLockTime; import bisq.core.trade.protocol.tasks.RemoveOffer; import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; -import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; +import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.SellerPublishDepositTx; import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; @@ -100,7 +100,7 @@ public class DebugView extends InitializableView { SellerPublishDepositTx.class, SellerPublishTradeStatistics.class, - SellerProcessPaymentSentMessage.class, + ProcessPaymentSentMessage.class, ApplyFilter.class, TakerVerifyMakerFeePayment.class, @@ -157,7 +157,7 @@ public class DebugView extends InitializableView { SellerPublishDepositTx.class, SellerPublishTradeStatistics.class, - SellerProcessPaymentSentMessage.class, + ProcessPaymentSentMessage.class, ApplyFilter.class, ApplyFilter.class, diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 4f932cc098..064cdb975d 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -92,10 +92,7 @@ public class DisputeSummaryWindow extends Overlay { private final CoinFormatter formatter; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; - private final XmrWalletService walletService; - private final TradeWalletService tradeWalletService; // TODO (woodser): remove for xmr or adapt to get/create multisig wallets for tx creation utils - private final CoreDisputesService disputesService; - private Dispute dispute; + private final CoreDisputesService disputesService; private Dispute dispute; private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private DisputeResult disputeResult; private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, @@ -115,7 +112,6 @@ public class DisputeSummaryWindow extends Overlay { private ChangeListener reasonToggleSelectionListener; private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField; private ChangeListener buyerPayoutAmountListener, sellerPayoutAmountListener; - private CheckBox isLoserPublisherCheckBox; private ChangeListener tradeAmountToggleGroupListener; @@ -134,8 +130,6 @@ public class DisputeSummaryWindow extends Overlay { this.formatter = formatter; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; - this.walletService = walletService; - this.tradeWalletService = tradeWalletService; this.disputesService = disputesService; type = Type.Confirmation; @@ -220,7 +214,6 @@ public class DisputeSummaryWindow extends Overlay { disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); disputeResult.setWinner(peersDisputeResult.getWinner()); - disputeResult.setLoserPublisher(peersDisputeResult.isLoserPublisher()); disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); @@ -248,13 +241,8 @@ public class DisputeSummaryWindow extends Overlay { reasonWasPeerWasLateRadioButton.setDisable(true); reasonWasTradeAlreadySettledRadioButton.setDisable(true); - isLoserPublisherCheckBox.setDisable(true); - isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher()); - applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get()); applyTradeAmountRadioButtonStates(); - } else { - isLoserPublisherCheckBox.setSelected(false); } setReasonRadioButtonState(); @@ -426,11 +414,9 @@ public class DisputeSummaryWindow extends Overlay { sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setEditable(false); - isLoserPublisherCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.payoutAmount.invert")); - VBox vBox = new VBox(); vBox.setSpacing(15); - vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField, isLoserPublisherCheckBox); + vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField); GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); GridPane.setRowIndex(vBox, rowIndex); GridPane.setColumnIndex(vBox, 1); @@ -590,7 +576,6 @@ public class DisputeSummaryWindow extends Overlay { Button cancelButton = tuple.second; closeTicketButton.setOnAction(e -> { - disputesService.applyDisputePayout(dispute, disputeResult, contract); doClose(closeTicketButton); // if (dispute.getDepositTxSerialized() == null) { @@ -763,19 +748,14 @@ public class DisputeSummaryWindow extends Overlay { summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); - boolean isRefundAgent = disputeManager instanceof RefundManager; - disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setCloseDate(new Date()); - disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent); + disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> { + if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { + new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show(); + } + disputeManager.requestPersistence(); + }); - if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { - UserThread.runAfter(() -> new Popup() - .attention(Res.get("disputeSummaryWindow.close.closePeer")) - .show(), - 200, TimeUnit.MILLISECONDS); - } - - disputeManager.requestPersistence(); closeTicketButton.disableProperty().unbind(); hide(); } 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 77578fe345..efcfe41885 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 @@ -465,7 +465,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { byte[] payoutTxSerialized = null; String payoutTxHashAsString = null; MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); - String updatedMultisigHex = multisigWallet.exportMultisigHex(); if (trade.getPayoutTxId() != null) { // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxHashAsString = payoutTx.getHashAsString(); @@ -477,9 +476,9 @@ public class PendingTradesDataModel extends ActivatableDataModel { // If mediation is not activated we use arbitration if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) { // In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or - useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED; + useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED; // in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED - useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED; + useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED; } else { useMediation = false; useArbitration = true; @@ -549,27 +548,27 @@ public class PendingTradesDataModel extends ActivatableDataModel { dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); - sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); + sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex()); tradeManager.requestPersistence(); } else if (useArbitration) { // Only if we have completed mediation we allow arbitration disputeManager = arbitrationManager; Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); - sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); + sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex()); tradeManager.requestPersistence(); } else { log.warn("Invalid dispute state {}", disputeState.name()); } } - private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager> disputeManager, String senderMultisigHex) { - disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, senderMultisigHex, + private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager> disputeManager, String senderMultisigHex) { + disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex, () -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> { if ((throwable instanceof DisputeAlreadyOpenException)) { errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); new Popup().warning(errorMessage) .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) - .onAction(() -> sendOpenNewDisputeMessage(dispute, true, disputeManager, senderMultisigHex)) + .onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex)) .closeButtonText(Res.get("shared.cancel")).show(); } else { new Popup().warning(errorMessage).show(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 2c60a0e12b..c226d5ecff 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -511,7 +511,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { - if (tradeStepInfo != null) - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); - }); - - break; - case DISPUTE_STARTED_BY_PEER: - if (tradeStepInfo != null) { - tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); - } - applyOnDisputeOpened(); - - ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId()); - ownDispute.ifPresent(dispute -> { - if (tradeStepInfo != null) - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); + if (tradeStepInfo != null) { + boolean isOpener = dispute.isDisputeOpenerIsBuyer() ? trade.isBuyer() : trade.isSeller(); + tradeStepInfo.setState(isOpener ? TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED : TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); + } }); break; + case DISPUTE_CLOSED: break; case MEDIATION_REQUESTED: 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 e4fcc2310e..233282515d 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 @@ -190,7 +190,7 @@ public class BuyerStep2View extends TradeStepView { model.setMessageStateProperty(MessageState.FAILED); break; default: - log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId()); + log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId()); busyAnimation.stop(); statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); break; @@ -608,12 +608,6 @@ public class BuyerStep2View extends TradeStepView { busyAnimation.play(); statusLabel.setText(Res.get("shared.sendingConfirmation")); - //TODO seems this was a hack to enable repeated confirm??? - if (trade.isPaymentSent()) { - trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); - model.dataModel.getTradeManager().requestPersistence(); - } - model.dataModel.onPaymentStarted(() -> { }, errorMessage -> { busyAnimation.stop(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index d1cb5850eb..52486d61b0 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -145,6 +145,11 @@ public class SellerStep3View extends TradeStepView { busyAnimation.stop(); statusLabel.setText(""); break; + case TRADE_COMPLETED: + if (!trade.isPayoutPublished()) log.warn("Payout is expected to be published for {} {} state {}", trade.getClass().getSimpleName(), trade.getId(), trade.getState()); + busyAnimation.stop(); + statusLabel.setText(""); + break; default: log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId()); busyAnimation.stop(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 8b7663c601..8a7a524df4 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -838,15 +838,19 @@ message TradeInfo { string phase = 17; string period_state = 18; string payout_state = 19; - bool is_deposit_published = 20; - bool is_deposit_unlocked = 21; - bool is_payment_sent = 22; - bool is_payment_received = 23; - bool is_payout_published = 24; - bool is_completed = 25; - string contract_as_json = 26; - ContractInfo contract = 27; - string trade_volume = 28; + string dispute_state = 20; + bool is_deposit_published = 21; + bool is_deposit_confirmed = 22; + bool is_deposit_unlocked = 23; + bool is_payment_sent = 24; + bool is_payment_received = 25; + bool is_payout_published = 26; + bool is_payout_confirmed = 27; + bool is_payout_unlocked = 28; + bool is_completed = 29; + string contract_as_json = 30; + ContractInfo contract = 31; + string trade_volume = 32; string maker_deposit_tx_id = 100; string taker_deposit_tx_id = 101; diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index a0e0261a99..e6d21960a0 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -40,31 +40,29 @@ message NetworkEnvelope { InputsForDepositTxResponse inputs_for_deposit_tx_response = 18; DepositTxMessage deposit_tx_message = 19; - OpenNewDisputeMessage open_new_dispute_message = 20; - PeerOpenedDisputeMessage peer_opened_dispute_message = 21; + DisputeOpenedMessage dispute_opened_message = 20; + DisputeClosedMessage dispute_closed_message = 21; ChatMessage chat_message = 22; - DisputeResultMessage dispute_result_message = 23; - PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 24; - PrivateNotificationMessage private_notification_message = 25; + PrivateNotificationMessage private_notification_message = 23; - AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 26; - AckMessage ack_message = 27; + AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 24; + AckMessage ack_message = 25; - BundleOfEnvelopes bundle_of_envelopes = 28; - MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 29; - MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 30; + BundleOfEnvelopes bundle_of_envelopes = 26; + MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 27; + MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 28; - DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 31; - DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 32; - DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 33; - PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 34; + DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 29; + DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 30; + DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 31; + PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 32; - RefreshTradeStateRequest refresh_trade_state_request = 35 [deprecated = true]; - TraderSignedWitnessMessage trader_signed_witness_message = 36 [deprecated = true]; + RefreshTradeStateRequest refresh_trade_state_request = 33 [deprecated = true]; + TraderSignedWitnessMessage trader_signed_witness_message = 34 [deprecated = true]; - GetInventoryRequest get_inventory_request = 37; - GetInventoryResponse get_inventory_response = 38; + GetInventoryRequest get_inventory_request = 35; + GetInventoryResponse get_inventory_response = 36; SignOfferRequest sign_offer_request = 1001; SignOfferResponse sign_offer_response = 1002; @@ -77,8 +75,6 @@ message NetworkEnvelope { DepositsConfirmedMessage deposits_confirmed_message = 1009; PaymentSentMessage payment_sent_message = 1010; PaymentReceivedMessage payment_received_message = 1011; - ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012; - ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013; } } @@ -399,14 +395,6 @@ message PeerPublishedDelayedPayoutTxMessage { NodeAddress sender_node_address = 3; } -message FinalizePayoutTxRequest { - string trade_id = 1; - bytes seller_signature = 2; - string seller_payout_address = 3; - NodeAddress sender_node_address = 4; - string uid = 5; -} - message PaymentSentMessage { string trade_id = 1; NodeAddress sender_node_address = 2; @@ -416,6 +404,7 @@ message PaymentSentMessage { string payout_tx_hex = 6; string updated_multisig_hex = 7; bytes payment_account_key = 8; + bytes buyer_signature = 9; } message PaymentReceivedMessage { @@ -426,23 +415,9 @@ message PaymentReceivedMessage { string unsigned_payout_tx_hex = 5; string signed_payout_tx_hex = 6; string updated_multisig_hex = 7; - bool saw_arrived_payment_received_msg = 8; -} - -message ArbitratorPayoutTxRequest { - Dispute dispute = 1; // TODO (woodser): replace with trade id - NodeAddress sender_node_address = 2; - string uid = 3; - SupportType type = 4; - string updated_multisig_hex = 5; -} - -message ArbitratorPayoutTxResponse { - string trade_id = 1; - NodeAddress sender_node_address = 2; - string uid = 3; - SupportType type = 4; - string arbitrator_signed_payout_tx_hex = 5; + bool defer_publish_payout = 8; + PaymentSentMessage payment_sent_message = 9; + bytes seller_signature = 10; } message MediatedPayoutTxPublishedMessage { @@ -474,30 +449,6 @@ message TraderSignedWitnessMessage { SignedWitness signed_witness = 4 [deprecated = true]; } -// dispute - -enum SupportType { - ARBITRATION = 0; - MEDIATION = 1; - TRADE = 2; - REFUND = 3; -} - -message OpenNewDisputeMessage { - Dispute dispute = 1; - NodeAddress sender_node_address = 2; - string uid = 3; - SupportType type = 4; - string updated_multisig_hex = 5; -} - -message PeerOpenedDisputeMessage { - Dispute dispute = 1; - NodeAddress sender_node_address = 2; - string uid = 3; - SupportType type = 4; -} - message ChatMessage { int64 date = 1; string trade_id = 2; @@ -517,21 +468,32 @@ message ChatMessage { bool was_displayed = 16; } -message DisputeResultMessage { +// dispute + +enum SupportType { + ARBITRATION = 0; + MEDIATION = 1; + TRADE = 2; + REFUND = 3; +} + +message DisputeOpenedMessage { + Dispute dispute = 1; + NodeAddress sender_node_address = 2; + string uid = 3; + SupportType type = 4; + string updated_multisig_hex = 5; + PaymentSentMessage payment_sent_message = 6; +} + +message DisputeClosedMessage { string uid = 1; DisputeResult dispute_result = 2; NodeAddress sender_node_address = 3; SupportType type = 4; -} - -message PeerPublishedDisputePayoutTxMessage { - string uid = 1; - reserved 2; // was bytes transaction = 2; - string trade_id = 3; - NodeAddress sender_node_address = 4; - SupportType type = 5; - string updated_multisig_hex = 6; - string payout_tx_hex = 7; + string updated_multisig_hex = 5; + string unsigned_payout_tx_hex = 6; + bool defer_publish_payout = 7; } message PrivateNotificationMessage { @@ -944,8 +906,6 @@ message DisputeResult { bytes arbitrator_pub_key = 13; int64 close_date = 14; bool is_loser_publisher = 15; - string arbitrator_signed_payout_tx_hex = 16; - string arbitrator_updated_multisig_hex = 17; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1640,24 +1600,28 @@ message Trade { } enum PayoutState { - UNPUBLISHED = 0; - PUBLISHED = 1; - CONFIRMED = 2; - UNLOCKED = 3; + PAYOUT_UNPUBLISHED = 0; + PAYOUT_PUBLISHED = 1; + PAYOUT_CONFIRMED = 2; + PAYOUT_UNLOCKED = 3; } enum DisputeState { PB_ERROR_DISPUTE_STATE = 0; NO_DISPUTE = 1; - DISPUTE_REQUESTED = 2; // arbitration We use the enum name for resolving enums so it cannot be renamed - DISPUTE_STARTED_BY_PEER = 3; // arbitration We use the enum name for resolving enums so it cannot be renamed - DISPUTE_CLOSED = 4; // arbitration We use the enum name for resolving enums so it cannot be renamed - MEDIATION_REQUESTED = 5; - MEDIATION_STARTED_BY_PEER = 6; - MEDIATION_CLOSED = 7; - REFUND_REQUESTED = 8; - REFUND_REQUEST_STARTED_BY_PEER = 9; - REFUND_REQUEST_CLOSED = 10; + DISPUTE_REQUESTED = 2; + DISPUTE_OPENED = 3; + ARBITRATOR_SENT_DISPUTE_CLOSED_MSG = 4; + ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG = 5; + ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG = 6; + ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG = 7; + DISPUTE_CLOSED = 8; + MEDIATION_REQUESTED = 9; + MEDIATION_STARTED_BY_PEER = 10; + MEDIATION_CLOSED = 11; + REFUND_REQUESTED = 12; + REFUND_REQUEST_STARTED_BY_PEER = 13; + REFUND_REQUEST_CLOSED = 14; } enum TradePeriodState { @@ -1782,6 +1746,7 @@ message TradingPeer { string deposit_tx_hex = 1009; string deposit_tx_key = 1010; string updated_multisig_hex = 1011; + PaymentSentMessage payment_sent_message = 1012; } ///////////////////////////////////////////////////////////////////////////////////////////