refactor arbitration protocol

add dispute states and open/close messages routed through arbitrator
both traders publish dispute payout tx, winner is default
verify signatures of payment sent and received messages
seller sends deposit confirmed message to arbitrator
buyer sends payment sent message to arbitrator
arbitrator slows trade wallet sync rate after deposits confirmed
various refactoring, fixes, and cleanup
This commit is contained in:
woodser 2022-11-04 15:56:53 -04:00
parent 363f783f30
commit 247087ef46
79 changed files with 1770 additions and 2480 deletions

View file

@ -17,8 +17,6 @@
package bisq.common.taskrunner; package bisq.common.taskrunner;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -60,6 +58,10 @@ public abstract class Task<T extends Model> {
taskHandler.handleComplete(); taskHandler.handleComplete();
} }
public boolean isCompleted() {
return completed;
}
protected void failed(String message) { protected void failed(String message) {
appendToErrorMessage(message); appendToErrorMessage(message);
failed(); failed();

View file

@ -23,7 +23,6 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.FaultHandler; import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.inject.name.Named; 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 com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format; import static java.lang.String.format;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@Singleton @Singleton
@Slf4j @Slf4j
@ -101,9 +98,7 @@ public class CoreDisputesService {
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler);
String updatedMultisigHex = multisigWallet.exportMultisigHex();
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} }
} }
@ -141,26 +136,26 @@ public class CoreDisputesService {
isSupportTicket, isSupportTicket,
SupportType.ARBITRATION); SupportType.ARBITRATION);
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
return dispute; return dispute;
} }
} }
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try { try {
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
// 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 -> tradeId.equals(d.getTradeId()))
.filter(d -> trade.getTradingPeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
.findFirst(); .findFirst();
Dispute dispute; if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) { synchronized (trade) {
var closeDate = new Date(); var closeDate = new Date();
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
var contract = dispute.getContract();
DisputePayout payout; DisputePayout payout;
if (customWinnerAmount > 0) { if (customWinnerAmount > 0) {
@ -172,30 +167,28 @@ public class CoreDisputesService {
} else { } else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
} }
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount);
// apply dispute payout
applyDisputePayout(dispute, disputeResult, contract);
// close dispute ticket // close dispute ticket
closeDispute(arbitrationManager, dispute, disputeResult, false); closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
arbitrationManager.requestPersistence();
// close dispute ticket for peer // close peer's dispute ticket
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId())
.findFirst(); .findFirst();
if (peersDisputeOptional.isPresent()) { if (peersDisputeOptional.isPresent()) {
var peerDispute = peersDisputeOptional.get(); var peerDispute = peersDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> {
applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); arbitrationManager.requestPersistence();
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); });
} else { } else {
throw new IllegalStateException("could not find peer dispute"); throw new IllegalStateException("could not find peer dispute");
} }
arbitrationManager.requestPersistence(); });
} }
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException(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 // 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.setDisputeResult(disputeResult);
dispute.setIsClosed(); dispute.setIsClosed();
DisputeResult.Reason reason = disputeResult.getReason(); 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(); String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
Contract contract = dispute.getContract(); Contract contract = dispute.getContract();
String currencyCode = contract.getOfferPayload().getCurrencyCode(); String currencyCode = contract.getOfferPayload().getCurrencyCode();
@ -314,13 +271,8 @@ public class CoreDisputesService {
} }
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
if (isRefundAgent) {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
} else { disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler);
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
}
disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
} }
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) { public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {

View file

@ -241,7 +241,6 @@ public final class CoreMoneroConnectionsService {
else { else {
boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri()); boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
if (isLocal) { 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 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 return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
} else { } else {
@ -410,6 +409,7 @@ public final class CoreMoneroConnectionsService {
private void startPollingDaemon() { private void startPollingDaemon() {
if (updateDaemonLooper != null) updateDaemonLooper.stop(); if (updateDaemonLooper != null) updateDaemonLooper.stop();
updateDaemonInfo();
updateDaemonLooper = new TaskLooper(() -> { updateDaemonLooper = new TaskLooper(() -> {
updateDaemonInfo(); updateDaemonInfo();
}); });

View file

@ -80,12 +80,16 @@ public class TradeInfo implements Payload {
private final String phase; private final String phase;
private final String periodState; private final String periodState;
private final String payoutState; private final String payoutState;
private final String disputeState;
private final boolean isDepositPublished; private final boolean isDepositPublished;
private final boolean isDepositConfirmed;
private final boolean isDepositUnlocked; private final boolean isDepositUnlocked;
private final boolean isPaymentSent; private final boolean isPaymentSent;
private final boolean isPaymentReceived; private final boolean isPaymentReceived;
private final boolean isCompleted;
private final boolean isPayoutPublished; private final boolean isPayoutPublished;
private final boolean isPayoutConfirmed;
private final boolean isPayoutUnlocked;
private final boolean isCompleted;
private final String contractAsJson; private final String contractAsJson;
private final ContractInfo contract; private final ContractInfo contract;
@ -109,11 +113,15 @@ public class TradeInfo implements Payload {
this.phase = builder.getPhase(); this.phase = builder.getPhase();
this.periodState = builder.getPeriodState(); this.periodState = builder.getPeriodState();
this.payoutState = builder.getPayoutState(); this.payoutState = builder.getPayoutState();
this.disputeState = builder.getDisputeState();
this.isDepositPublished = builder.isDepositPublished(); this.isDepositPublished = builder.isDepositPublished();
this.isDepositConfirmed = builder.isDepositConfirmed();
this.isDepositUnlocked = builder.isDepositUnlocked(); this.isDepositUnlocked = builder.isDepositUnlocked();
this.isPaymentSent = builder.isPaymentSent(); this.isPaymentSent = builder.isPaymentSent();
this.isPaymentReceived = builder.isPaymentReceived(); this.isPaymentReceived = builder.isPaymentReceived();
this.isPayoutPublished = builder.isPayoutPublished(); this.isPayoutPublished = builder.isPayoutPublished();
this.isPayoutConfirmed = builder.isPayoutConfirmed();
this.isPayoutUnlocked = builder.isPayoutUnlocked();
this.isCompleted = builder.isCompleted(); this.isCompleted = builder.isCompleted();
this.contractAsJson = builder.getContractAsJson(); this.contractAsJson = builder.getContractAsJson();
this.contract = builder.getContract(); this.contract = builder.getContract();
@ -161,11 +169,15 @@ public class TradeInfo implements Payload {
.withPhase(trade.getPhase().name()) .withPhase(trade.getPhase().name())
.withPeriodState(trade.getPeriodState().name()) .withPeriodState(trade.getPeriodState().name())
.withPayoutState(trade.getPayoutState().name()) .withPayoutState(trade.getPayoutState().name())
.withDisputeState(trade.getDisputeState().name())
.withIsDepositPublished(trade.isDepositPublished()) .withIsDepositPublished(trade.isDepositPublished())
.withIsDepositConfirmed(trade.isDepositConfirmed())
.withIsDepositUnlocked(trade.isDepositUnlocked()) .withIsDepositUnlocked(trade.isDepositUnlocked())
.withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentSent(trade.isPaymentSent())
.withIsPaymentReceived(trade.isPaymentReceived()) .withIsPaymentReceived(trade.isPaymentReceived())
.withIsPayoutPublished(trade.isPayoutPublished()) .withIsPayoutPublished(trade.isPayoutPublished())
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
.withIsCompleted(trade.isCompleted()) .withIsCompleted(trade.isCompleted())
.withContractAsJson(trade.getContractAsJson()) .withContractAsJson(trade.getContractAsJson())
.withContract(contractInfo) .withContract(contractInfo)
@ -199,12 +211,16 @@ public class TradeInfo implements Payload {
.setPhase(phase) .setPhase(phase)
.setPeriodState(periodState) .setPeriodState(periodState)
.setPayoutState(payoutState) .setPayoutState(payoutState)
.setDisputeState(disputeState)
.setIsDepositPublished(isDepositPublished) .setIsDepositPublished(isDepositPublished)
.setIsDepositConfirmed(isDepositConfirmed)
.setIsDepositUnlocked(isDepositUnlocked) .setIsDepositUnlocked(isDepositUnlocked)
.setIsPaymentSent(isPaymentSent) .setIsPaymentSent(isPaymentSent)
.setIsPaymentReceived(isPaymentReceived) .setIsPaymentReceived(isPaymentReceived)
.setIsCompleted(isCompleted) .setIsCompleted(isCompleted)
.setIsPayoutPublished(isPayoutPublished) .setIsPayoutPublished(isPayoutPublished)
.setIsPayoutConfirmed(isPayoutConfirmed)
.setIsPayoutUnlocked(isPayoutUnlocked)
.setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContractAsJson(contractAsJson == null ? "" : contractAsJson)
.setContract(contract.toProtoMessage()) .setContract(contract.toProtoMessage())
.build(); .build();
@ -227,16 +243,20 @@ public class TradeInfo implements Payload {
.withVolume(proto.getTradeVolume()) .withVolume(proto.getTradeVolume())
.withPeriodState(proto.getPeriodState()) .withPeriodState(proto.getPeriodState())
.withPayoutState(proto.getPayoutState()) .withPayoutState(proto.getPayoutState())
.withDisputeState(proto.getDisputeState())
.withState(proto.getState()) .withState(proto.getState())
.withPhase(proto.getPhase()) .withPhase(proto.getPhase())
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress()) .withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress()) .withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
.withIsDepositPublished(proto.getIsDepositPublished()) .withIsDepositPublished(proto.getIsDepositPublished())
.withIsDepositConfirmed(proto.getIsDepositConfirmed())
.withIsDepositUnlocked(proto.getIsDepositUnlocked()) .withIsDepositUnlocked(proto.getIsDepositUnlocked())
.withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentSent(proto.getIsPaymentSent())
.withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsPaymentReceived(proto.getIsPaymentReceived())
.withIsCompleted(proto.getIsCompleted()) .withIsCompleted(proto.getIsCompleted())
.withIsPayoutPublished(proto.getIsPayoutPublished()) .withIsPayoutPublished(proto.getIsPayoutPublished())
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
.withContractAsJson(proto.getContractAsJson()) .withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract()))) .withContract((ContractInfo.fromProto(proto.getContract())))
.build(); .build();
@ -262,12 +282,16 @@ public class TradeInfo implements Payload {
", phase='" + phase + '\'' + "\n" + ", phase='" + phase + '\'' + "\n" +
", periodState='" + periodState + '\'' + "\n" + ", periodState='" + periodState + '\'' + "\n" +
", payoutState='" + payoutState + '\'' + "\n" + ", payoutState='" + payoutState + '\'' + "\n" +
", disputeState='" + disputeState + '\'' + "\n" +
", isDepositPublished=" + isDepositPublished + "\n" + ", isDepositPublished=" + isDepositPublished + "\n" +
", isDepositConfirmed=" + isDepositUnlocked + "\n" + ", isDepositConfirmed=" + isDepositConfirmed + "\n" +
", isDepositUnlocked=" + isDepositUnlocked + "\n" +
", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" +
", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" +
", isCompleted=" + isCompleted + "\n" +
", isPayoutPublished=" + isPayoutPublished + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" +
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
", isCompleted=" + isCompleted + "\n" +
", offer=" + offer + "\n" + ", offer=" + offer + "\n" +
", contractAsJson=" + contractAsJson + "\n" + ", contractAsJson=" + contractAsJson + "\n" +
", contract=" + contract + "\n" + ", contract=" + contract + "\n" +

View file

@ -52,11 +52,15 @@ public final class TradeInfoV1Builder {
private String phase; private String phase;
private String periodState; private String periodState;
private String payoutState; private String payoutState;
private String disputeState;
private boolean isDepositPublished; private boolean isDepositPublished;
private boolean isDepositConfirmed;
private boolean isDepositUnlocked; private boolean isDepositUnlocked;
private boolean isPaymentSent; private boolean isPaymentSent;
private boolean isPaymentReceived; private boolean isPaymentReceived;
private boolean isPayoutPublished; private boolean isPayoutPublished;
private boolean isPayoutConfirmed;
private boolean isPayoutUnlocked;
private boolean isCompleted; private boolean isCompleted;
private String contractAsJson; private String contractAsJson;
private ContractInfo contract; private ContractInfo contract;
@ -152,6 +156,11 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withDisputeState(String disputeState) {
this.disputeState = disputeState;
return this;
}
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) { public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
this.arbitratorNodeAddress = arbitratorNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress;
return this; return this;
@ -167,6 +176,11 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withIsDepositConfirmed(boolean isDepositConfirmed) {
this.isDepositConfirmed = isDepositConfirmed;
return this;
}
public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) { public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) {
this.isDepositUnlocked = isDepositUnlocked; this.isDepositUnlocked = isDepositUnlocked;
return this; return this;
@ -187,6 +201,16 @@ public final class TradeInfoV1Builder {
return this; 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) { public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
this.isCompleted = isCompleted; this.isCompleted = isCompleted;
return this; return this;

View file

@ -95,17 +95,14 @@ public class Balances {
} }
private void updatedBalances() { 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(); updateAvailableBalance();
updatePendingBalance(); updatePendingBalance();
updateReservedOfferBalance(); updateReservedOfferBalance();
updateReservedTradeBalance(); updateReservedTradeBalance();
updateReservedBalance(); 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() { private void updateAvailableBalance() {
availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact())); availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact()));

View file

@ -68,9 +68,21 @@ public class MoneroWalletRpcManager {
int numAttempts = 0; int numAttempts = 0;
while (numAttempts < NUM_ALLOWED_ATTEMPTS) { while (numAttempts < NUM_ALLOWED_ATTEMPTS) {
int port = -1; int port = -1;
ServerSocket socket = null;
try { try {
numAttempts++; 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<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd List<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd
cmdCopy.add(RPC_BIND_PORT_ARGUMENT); cmdCopy.add(RPC_BIND_PORT_ARGUMENT);
cmdCopy.add("" + port); cmdCopy.add("" + port);
@ -84,6 +96,8 @@ public class MoneroWalletRpcManager {
log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS);
throw e; 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 throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here
@ -121,24 +135,13 @@ public class MoneroWalletRpcManager {
walletRpc.stopProcess(); walletRpc.stopProcess();
} }
private int registerPort() throws IOException { private int registerNextPort() throws IOException {
synchronized (registeredPorts) { synchronized (registeredPorts) {
// register next consecutive port
if (startPort != null) {
int port = startPort; int port = startPort;
while (registeredPorts.containsKey(port)) port++; while (registeredPorts.containsKey(port)) port++;
registeredPorts.put(port, null); registeredPorts.put(port, null);
return port; return port;
} }
// register auto-assigned port
else {
int port = getLocalPort();
registeredPorts.put(port, null);
return port;
}
}
} }
private void unregisterPort(int port) { private void unregisterPort(int port) {
@ -146,11 +149,4 @@ public class MoneroWalletRpcManager {
registeredPorts.remove(port); 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;
}
} }

View file

@ -86,7 +86,7 @@ public class XmrWalletService {
private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_"; 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 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 CoreAccountService accountService;
private final CoreMoneroConnectionsService connectionsService; private final CoreMoneroConnectionsService connectionsService;
@ -103,6 +103,7 @@ public class XmrWalletService {
private Map<String, MoneroWallet> multisigWallets; private Map<String, MoneroWallet> multisigWallets;
private Map<String, Object> walletLocks = new HashMap<String, Object>(); private Map<String, Object> walletLocks = new HashMap<String, Object>();
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>(); private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isShutDown = false;
@Inject @Inject
XmrWalletService(CoreAccountService accountService, XmrWalletService(CoreAccountService accountService,
@ -199,9 +200,9 @@ public class XmrWalletService {
} }
public MoneroWallet createMultisigWallet(String tradeId) { public MoneroWallet createMultisigWallet(String tradeId) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId); initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) { synchronized (walletLocks.get(tradeId)) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port
@ -212,6 +213,7 @@ public class XmrWalletService {
// TODO (woodser): provide progress notifications during open? // TODO (woodser): provide progress notifications during open?
public MoneroWallet getMultisigWallet(String tradeId) { public MoneroWallet getMultisigWallet(String tradeId) {
if (isShutDown) throw new RuntimeException(getClass().getName() + " is shut down");
initWalletLock(tradeId); initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) { synchronized (walletLocks.get(tradeId)) {
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
@ -229,9 +231,9 @@ public class XmrWalletService {
} }
public void closeMultisigWallet(String tradeId) { public void closeMultisigWallet(String tradeId) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId); initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) { synchronized (walletLocks.get(tradeId)) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
MoneroWallet wallet = multisigWallets.remove(tradeId); MoneroWallet wallet = multisigWallets.remove(tradeId);
closeWallet(wallet, true); closeWallet(wallet, true);
@ -239,9 +241,9 @@ public class XmrWalletService {
} }
public boolean deleteMultisigWallet(String tradeId) { public boolean deleteMultisigWallet(String tradeId) {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId); initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) { synchronized (walletLocks.get(tradeId)) {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
if (!walletExists(walletName)) return false; if (!walletExists(walletName)) return false;
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
@ -253,7 +255,7 @@ public class XmrWalletService {
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
try { try {
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
printTxs("XmrWalletService.createTx", tx); //printTxs("XmrWalletService.createTx", tx);
return tx; return tx;
} catch (Exception e) { } catch (Exception e) {
throw e; throw e;
@ -268,7 +270,7 @@ public class XmrWalletService {
* @param tradeFee - trade fee * @param tradeFee - trade fee
* @param depositAmount - amount needed for the trade minus the trade fee * @param depositAmount - amount needed for the trade minus the trade fee
* @param returnAddress - return address for deposit amount * @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 * @return a transaction to reserve a trade
*/ */
public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount, boolean addPadding) { 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 // add miner fee padding to deposit amount
if (addPadding) { if (addPadding) {
// get expected mining fee // get estimated mining fee with deposit amount
MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig() MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount)); .addDestination(returnAddress, depositAmount));
BigInteger feeEstimate = feeEstimateTx.getFee(); 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)); BigInteger minerFeePadding = feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER));
depositAmount = depositAmount.add(minerFeePadding); depositAmount = depositAmount.add(minerFeePadding);
} }
@ -295,11 +309,11 @@ public class XmrWalletService {
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount)); .addDestination(returnAddress, depositAmount));
log.info("Reserve tx weight={}, fee={}, depositAmount={}", reserveTx.getWeight(), reserveTx.getFee(), depositAmount);
// freeze inputs // freeze inputs
for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
wallet.save(); wallet.save();
return reserveTx; return reserveTx;
} }
} }
@ -343,12 +357,13 @@ public class XmrWalletService {
* @param txHex is the transaction hex * @param txHex is the transaction hex
* @param txKey is the transaction key * @param txKey is the transaction key
* @param keyImages are expected key images of inputs, ignored if null * @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<String> keyImages, boolean miningFeePadding) { public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean addPadding) {
MoneroDaemonRpc daemon = getDaemon(); MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
try { try {
log.info("Verifying trade tx with deposit amount={}", depositAmount);
// verify tx not submitted to pool // verify tx not submitted to pool
MoneroTx tx = daemon.getTx(txHash); MoneroTx tx = daemon.getTx(txHash);
@ -379,12 +394,18 @@ public class XmrWalletService {
BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); 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()); 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 // verify deposit amount
check = wallet.checkTxKey(txHash, txKey, depositAddress); check = wallet.checkTxKey(txHash, txKey, depositAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); 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 (addPadding) {
if (check.getReceivedAmount().compareTo(depositAmount) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount()); 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 { } finally {
try { try {
daemon.flushTxPool(txHash); // flush tx from pool daemon.flushTxPool(txHash); // flush tx from pool
@ -405,11 +426,12 @@ public class XmrWalletService {
// get fee estimates per kB from daemon // get fee estimates per kB from daemon
MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); 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(); BigInteger qmask = feeEstimates.getQuantizationMask();
log.info("Monero base fee estimate={}, qmask={}: " + baseFeeEstimate, qmask);
// get tx base fee // 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 // round up to multiple of quantization mask
BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask); BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask);
@ -468,6 +490,7 @@ public class XmrWalletService {
} }
public void shutDown() { public void shutDown() {
this.isShutDown = true;
closeAllWallets(); closeAllWallets();
} }
@ -573,7 +596,7 @@ public class XmrWalletService {
// start syncing wallet in background // start syncing wallet in background
new Thread(() -> { new Thread(() -> {
log.info("Syncing wallet " + config.getPath() + " in background"); log.info("Starting background syncing for wallet " + config.getPath());
walletRpc.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); walletRpc.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
log.info("Done starting background sync for wallet " + config.getPath()); log.info("Done starting background sync for wallet " + config.getPath());
}).start(); }).start();
@ -645,11 +668,10 @@ public class XmrWalletService {
} }
private void changeWalletPasswords(String oldPassword, String newPassword) { private void changeWalletPasswords(String oldPassword, String newPassword) {
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size())); // create task to change main wallet password
pool.submit(new Runnable() { List<Runnable> tasks = new ArrayList<Runnable>();
@Override tasks.add(() -> {
public void run() {
try { try {
wallet.changePassword(oldPassword, newPassword); wallet.changePassword(oldPassword, newPassword);
saveWallet(wallet); saveWallet(wallet);
@ -657,35 +679,30 @@ public class XmrWalletService {
e.printStackTrace(); e.printStackTrace();
throw e; throw e;
} }
}
}); });
// create tasks to change multisig wallet passwords
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
for (String tradeId : tradeIds) { for (String tradeId : tradeIds) {
pool.submit(new Runnable() { tasks.add(() -> {
@Override
public void run() {
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
if (multisigWallet == null) return; if (multisigWallet == null) return;
multisigWallet.changePassword(oldPassword, newPassword); multisigWallet.changePassword(oldPassword, newPassword);
saveWallet(multisigWallet); saveWallet(multisigWallet);
}
}); });
} }
pool.shutdown();
try { // excute tasks in parallel
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.size()));
} catch (InterruptedException e) {
try { pool.shutdownNow(); }
catch (Exception e2) { }
throw new RuntimeException(e);
}
} }
private void closeWallet(MoneroWallet walletRpc, boolean save) { private void closeWallet(MoneroWallet walletRpc, boolean save) {
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save); log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
MoneroError err = null; MoneroError err = null;
try { try {
if (save) saveWallet(walletRpc); String path = walletRpc.getPath();
walletRpc.close(); walletRpc.close(save);
if (save) backupWallet(path);
} catch (MoneroError e) { } catch (MoneroError e) {
err = 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?"); log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
} }
}); });
HavenoUtils.awaitTasks(tasks); HavenoUtils.executeTasks(tasks);
// clear wallets // clear wallets
wallet = null; wallet = null;

View file

@ -43,6 +43,7 @@ public class TradeEvents {
private final PubKeyRingProvider pubKeyRingProvider; private final PubKeyRingProvider pubKeyRingProvider;
private final TradeManager tradeManager; private final TradeManager tradeManager;
private final MobileNotificationService mobileNotificationService; private final MobileNotificationService mobileNotificationService;
private boolean isInitialized = false;
@Inject @Inject
public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) { public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) {
@ -59,10 +60,11 @@ public class TradeEvents {
} }
}); });
tradeManager.getObservableList().forEach(this::setTradePhaseListener); tradeManager.getObservableList().forEach(this::setTradePhaseListener);
isInitialized = true;
} }
private void setTradePhaseListener(Trade trade) { 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()) { if (!trade.isPayoutPublished()) {
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
String msg = null; String msg = null;

View file

@ -117,14 +117,14 @@ public class CreateOfferService {
double buyerSecurityDepositAsDouble, double buyerSecurityDepositAsDouble,
PaymentAccount paymentAccount) { PaymentAccount paymentAccount) {
log.info("create and get offer with offerId={}, \n" + log.info("create and get offer with offerId={}, " +
"currencyCode={}, \n" + "currencyCode={}, " +
"direction={}, \n" + "direction={}, " +
"price={}, \n" + "price={}, " +
"useMarketBasedPrice={}, \n" + "useMarketBasedPrice={}, " +
"marketPriceMargin={}, \n" + "marketPriceMargin={}, " +
"amount={}, \n" + "amount={}, " +
"minAmount={}, \n" + "minAmount={}, " +
"buyerSecurityDeposit={}", "buyerSecurityDeposit={}",
offerId, offerId,
currencyCode, currencyCode,

View file

@ -675,6 +675,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle unscheduled offer // handle unscheduled offer
if (openOffer.getScheduledTxHashes() == null) { if (openOffer.getScheduledTxHashes() == null) {
log.info("Scheduling offer " + openOffer.getId());
// check for sufficient balance - scheduled offers amount // check for sufficient balance - scheduled offers amount
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount()).compareTo(offerReserveAmount) < 0) { 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 Coin offerReserveAmount, // TODO: switch to BigInteger
boolean useSavingsWallet, // TODO: remove this boolean useSavingsWallet, // TODO: remove this
TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("Signing and posting offer " + openOffer.getId());
// create model // create model
PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(), PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(),

View file

@ -99,7 +99,6 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
.setTakersTradePrice(takersTradePrice) .setTakersTradePrice(takersTradePrice)
.setIsTakerApiUser(isTakerApiUser) .setIsTakerApiUser(isTakerApiUser)
.setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest()); .setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest());
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));

View file

@ -62,7 +62,6 @@ public class PlaceOfferProtocol {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void placeOffer() { public void placeOffer() {
log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId());
timeoutTimer = UserThread.runAfter(() -> { timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing")); handleError(Res.get("createOffer.timeoutAtPublishing"));
@ -96,10 +95,12 @@ public class PlaceOfferProtocol {
// ignore if timer already stopped // ignore if timer already stopped
if (timeoutTimer == null) { 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; return;
} }
// reset timer
stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> { timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing")); handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_TIMEOUT); }, TradeProtocol.TRADE_TIMEOUT);

View file

@ -23,12 +23,15 @@ import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@Slf4j
public class MakerReserveOfferFunds extends Task<PlaceOfferModel> { public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) { public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) {
@ -47,6 +50,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); 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); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount, true);
// collect reserved key images // TODO (woodser): switch to proof of reserve? // collect reserved key images // TODO (woodser): switch to proof of reserve?

View file

@ -79,8 +79,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
sendSignOfferRequests(request, () -> { sendSignOfferRequests(request, () -> {
complete(); complete();
}, (errorMessage) -> { }, (errorMessage) -> {
log.warn("Error signing offer: " + errorMessage); appendToErrorMessage("Error signing offer " + request.getOfferId() + ": " + errorMessage);
appendToErrorMessage("Error signing offer: " + errorMessage);
failed(errorMessage); failed(errorMessage);
}); });
} catch (Throwable t) { } catch (Throwable t) {
@ -94,7 +93,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager()); Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager());
if (leastUsedArbitrator == null) { 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; return;
} }
sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler); sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler);
@ -102,7 +101,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// complete on successful ack message // complete on successful ack message, fail on first nack
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() { DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
@Override @Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) { public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
@ -117,8 +116,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED); model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
resultHandler.handleResult(); resultHandler.handleResult();
} else { } else {
log.warn("Arbitrator nacked request: {}", errorMessage); errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
} }
} }
}; };
@ -135,7 +133,14 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
@Override @Override
public void onFault(String errorMessage) { public void onFault(String errorMessage) {
log.warn("Arbitrator unavailable: {}", 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<PlaceOfferModel> {
listener listener
); );
} }
private void handleArbitratorFailure(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> 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);
}
} }

View file

@ -47,7 +47,7 @@ public class PaymentAccountUtil {
public static boolean isAnyPaymentAccountValidForOffer(Offer offer, public static boolean isAnyPaymentAccountValidForOffer(Offer offer,
Collection<PaymentAccount> paymentAccounts) { Collection<PaymentAccount> paymentAccounts) {
for (PaymentAccount paymentAccount : paymentAccounts) { for (PaymentAccount paymentAccount : new ArrayList<PaymentAccount>(paymentAccounts)) {
if (isPaymentAccountValidForOffer(offer, paymentAccount)) if (isPaymentAccountValidForOffer(offer, paymentAccount))
return true; return true;
} }

View file

@ -17,6 +17,7 @@
package bisq.core.presentation; package bisq.core.presentation;
import bisq.common.UserThread;
import bisq.core.btc.Balances; import bisq.core.btc.Balances;
import javax.inject.Inject; import javax.inject.Inject;
@ -43,13 +44,13 @@ public class BalancePresentation {
@Inject @Inject
public BalancePresentation(Balances balances) { public BalancePresentation(Balances balances) {
balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> { balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> {
availableBalance.set(longToXmr(newValue.value)); UserThread.execute(() -> availableBalance.set(longToXmr(newValue.value)));
}); });
balances.getPendingBalance().addListener((observable, oldValue, newValue) -> { balances.getPendingBalance().addListener((observable, oldValue, newValue) -> {
pendingBalance.set(longToXmr(newValue.value)); UserThread.execute(() -> pendingBalance.set(longToXmr(newValue.value)));
}); });
balances.getReservedBalance().addListener((observable, oldValue, newValue) -> { balances.getReservedBalance().addListener((observable, oldValue, newValue) -> {
reservedBalance.set(longToXmr(newValue.value)); UserThread.execute(() -> reservedBalance.set(longToXmr(newValue.value)));
}); });
} }

View file

@ -29,13 +29,9 @@ import bisq.core.offer.messages.SignOfferRequest;
import bisq.core.offer.messages.SignOfferResponse; import bisq.core.offer.messages.SignOfferResponse;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; 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.mediation.mediator.Mediator;
import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest; import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse; import bisq.core.support.dispute.messages.DisputeOpenedMessage;
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.refund.refundagent.RefundAgent; import bisq.core.support.dispute.refund.refundagent.RefundAgent;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
@ -170,20 +166,12 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE:
return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion); return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion);
case OPEN_NEW_DISPUTE_MESSAGE: case DISPUTE_OPENED_MESSAGE:
return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion); return DisputeOpenedMessage.fromProto(proto.getDisputeOpenedMessage(), this, messageVersion);
case PEER_OPENED_DISPUTE_MESSAGE: case DISPUTE_CLOSED_MESSAGE:
return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion); return DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), messageVersion);
case CHAT_MESSAGE: case CHAT_MESSAGE:
return ChatMessage.fromProto(proto.getChatMessage(), messageVersion); 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: case PRIVATE_NOTIFICATION_MESSAGE:
return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion); return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion);

View file

@ -144,7 +144,7 @@ public abstract class SupportManager {
// Message handler // Message handler
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
protected void onChatMessage(ChatMessage chatMessage) { protected void handleChatMessage(ChatMessage chatMessage) {
final String tradeId = chatMessage.getTradeId(); final String tradeId = chatMessage.getTradeId();
final String uid = chatMessage.getUid(); final String uid = chatMessage.getUid();
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid); log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
@ -152,7 +152,7 @@ public abstract class SupportManager {
if (!channelOpen) { if (!channelOpen) {
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
Timer timer = UserThread.runAfter(() -> onChatMessage(chatMessage), 1); Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1);
delayMsgMap.put(uid, timer); delayMsgMap.put(uid, timer);
} else { } else {
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;

View file

@ -31,15 +31,19 @@ import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService; import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportManager; import bisq.core.support.SupportManager;
import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.DisputeResult.Winner;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.dispute.messages.DisputeOpenedMessage;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; 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.BootstrapListener;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
@ -63,8 +67,9 @@ import javafx.beans.property.IntegerProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.math.BigInteger;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -75,6 +80,10 @@ import java.util.stream.Collectors;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; 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; import javax.annotation.Nullable;
@ -82,8 +91,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
@Slf4j @Slf4j
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager { public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
protected final TradeWalletService tradeWalletService; protected final TradeWalletService tradeWalletService;
@ -197,12 +204,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// We get that message at both peers. The dispute object is in context of the trader // 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); public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
protected abstract Trade.DisputeState getDisputeStateStartedByPeer();
public abstract void cleanupDisputes(); public abstract void cleanupDisputes();
protected abstract String getDisputeInfo(Dispute dispute); protected abstract String getDisputeInfo(Dispute dispute);
@ -299,157 +304,26 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Message handler // Dispute handling
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// arbitrator receives that from trader who opens dispute // trader sends message to arbitrator to open dispute
protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { public void sendDisputeOpenedMessage(Dispute dispute,
T disputeList = getDisputeList(); boolean reOpen,
if (disputeList == null) { String updatedMultisigHex,
log.warn("disputes is null"); ResultHandler resultHandler,
return; 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()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) { if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId()); log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return; return;
} }
synchronized (trade) { log.info("Sending {} for {} {}, dispute {}",
DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(),
String errorMessage = null; dispute.getTradeId(), dispute.getId());
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<Dispute> 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<ChatMessage> 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<Trade> 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<Dispute> 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<ChatMessage> 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());
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
@ -469,8 +343,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (!storedDisputeOptional.isPresent() || reOpen) { if (!storedDisputeOptional.isPresent() || reOpen) {
String disputeInfo = getDisputeInfo(dispute); String disputeInfo = getDisputeInfo(dispute);
String sysMsg = dispute.isSupportTicket() ? String sysMsg = dispute.isSupportTicket() ?
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
ChatMessage chatMessage = new ChatMessage( ChatMessage chatMessage = new ChatMessage(
getSupportType(), getSupportType(),
@ -486,31 +360,33 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
getSupportType(), getSupportType(),
updatedMultisigHex); updatedMultisigHex,
trade.getBuyer().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}", "chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid()); chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
dispute.getAgentPubKeyRing(), dispute.getAgentPubKeyRing(),
openNewDisputeMessage, disputeOpenedMessage,
new SendMailboxMessageListener() { new SendMailboxMessageListener() {
@Override @Override
public void onArrived() { public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}", "chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid()); chatMessage.getUid());
// We use the chatMessage wrapped inside the openNewDisputeMessage for // We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true); chatMessage.setArrived(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence(); requestPersistence();
resultHandler.handleResult(); resultHandler.handleResult();
} }
@ -519,13 +395,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
public void onStoredInMailbox() { public void onStoredInMailbox() {
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}", "chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid()); chatMessage.getUid());
// We use the chatMessage wrapped inside the openNewDisputeMessage for // We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true); chatMessage.setStoredInMailbox(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence(); requestPersistence();
resultHandler.handleResult(); resultHandler.handleResult();
} }
@ -534,8 +411,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
public void onFault(String errorMessage) { public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}", "chatMessage.uid={}, errorMessage={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid(), errorMessage); chatMessage.getUid(), errorMessage);
// We use the chatMessage wrapped inside the openNewDisputeMessage for // We use the chatMessage wrapped inside the openNewDisputeMessage for
@ -545,8 +422,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
faultHandler.handleFault("Sending dispute message failed: " + faultHandler.handleFault("Sending dispute message failed: " +
errorMessage, new DisputeMessageDeliveryFailedException()); errorMessage, new DisputeMessageDeliveryFailedException());
} }
} });
);
} else { } else {
String msg = "We got a dispute already open for that trade and trading peer.\n" + String msg = "We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId(); "TradeId = " + dispute.getTradeId();
@ -558,10 +434,111 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
requestPersistence(); requestPersistence();
} }
// Dispute agent sends that to trading peer when he received openDispute request // arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, 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<Dispute> 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<ChatMessage> 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, Contract contractFromOpener,
PubKeyRing pubKeyRing) { PubKeyRing pubKeyRing,
String updatedMultisigHex) {
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId()); 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 // 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 // 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<T extends DisputeList<Dispute>> extends Sup
// from the code below. // from the code below.
UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener, UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener,
contractFromOpener, contractFromOpener,
pubKeyRing), pubKeyRing,
updatedMultisigHex),
100, TimeUnit.MILLISECONDS); 100, TimeUnit.MILLISECONDS);
} }
private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
Contract contractFromOpener, Contract contractFromOpener,
PubKeyRing pubKeyRing) { PubKeyRing pubKeyRing,
String updatedMultisigHex) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");
@ -638,14 +617,23 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeList.add(dispute); 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! // We mirrored dispute already!
Contract contract = dispute.getContract(); Contract contract = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
getSupportType()); getSupportType(),
updatedMultisigHex,
trade.getSelf().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
@ -701,8 +689,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
requestPersistence(); requestPersistence();
} }
// arbitrator send result to trader // arbitrator sends result to trader when their dispute is closed
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) { public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");
@ -720,75 +708,114 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.setChatMessage(chatMessage); disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
NodeAddress peersNodeAddress; // get trade
Contract contract = dispute.getContract(); Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) if (trade == null) {
peersNodeAddress = contract.getBuyerNodeAddress(); log.warn("Dispute trade {} does not exist", dispute.getTradeId());
else return;
peersNodeAddress = contract.getSellerNodeAddress(); }
DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult,
// 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<String> updatedMultisigHexes = new ArrayList<String>();
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(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
getSupportType()); getSupportType(),
log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}", trade.getSelf().getUpdatedMultisigHex(),
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(), trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info
disputeResultMessage.getUid(), chatMessage.getUid()); deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
// 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(), dispute.getTraderPubKeyRing(),
disputeResultMessage, disputeClosedMessage,
new SendMailboxMessageListener() { new SendMailboxMessageListener() {
@Override @Override
public void onArrived() { public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " + log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " +
"chatMessage.uid={}", "chatMessage.uid={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid()); chatMessage.getUid());
// TODO: hack to sync wallet after dispute message received in order to detect payout published // We use the chatMessage wrapped inside the DisputeClosedMessage for
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
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true); chatMessage.setArrived(true);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.syncWalletNormallyForMs(30000);
requestPersistence(); requestPersistence();
resultHandler.handleResult();
} }
@Override @Override
public void onStoredInMailbox() { 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={}", "chatMessage.uid={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.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 // the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true); chatMessage.setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence();
resultHandler.handleResult();
} }
@Override @Override
public void onFault(String errorMessage) { public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " + log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}", "chatMessage.uid={}, errorMessage={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid(), errorMessage); 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 // the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage); chatMessage.setSendMessageError(errorMessage);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence();
resultHandler.handleResult();
} }
} }
); );
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence();
} }
@ -796,6 +823,52 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// Utils // 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<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) { private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
PubKeyRing receiverPubKeyRing = null; PubKeyRing receiverPubKeyRing = null;
NodeAddress peerNodeAddress = null; NodeAddress peerNodeAddress = null;
@ -878,15 +951,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
if (dispute.getMediatorsDisputeResult() != null) { if (dispute.getMediatorsDisputeResult() != null) {
String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult());
ChatMessage mediatorsDisputeResultMessage = new ChatMessage( ChatMessage mediatorsDisputeClosedMessage = new ChatMessage(
getSupportType(), getSupportType(),
dispute.getTradeId(), dispute.getTradeId(),
pubKeyRing.hashCode(), pubKeyRing.hashCode(),
false, false,
mediatorsDisputeResult, mediatorsDisputeResult,
p2PService.getAddress()); p2PService.getAddress());
mediatorsDisputeResultMessage.setSystemMessage(true); mediatorsDisputeClosedMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); dispute.addAndPersistChatMessage(mediatorsDisputeClosedMessage);
requestPersistence(); requestPersistence();
} }
} }

View file

@ -23,8 +23,6 @@ import bisq.common.proto.ProtoUtil;
import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.network.NetworkPayload;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
import com.google.protobuf.ByteString;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -89,16 +87,6 @@ public final class DisputeResult implements NetworkPayload {
@Nullable @Nullable
private byte[] arbitratorPubKey; private byte[] arbitratorPubKey;
private long closeDate; 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) { public DisputeResult(String tradeId, int traderId) {
this.tradeId = tradeId; this.tradeId = tradeId;
@ -115,13 +103,10 @@ public final class DisputeResult implements NetworkPayload {
String summaryNotes, String summaryNotes,
@Nullable ChatMessage chatMessage, @Nullable ChatMessage chatMessage,
@Nullable byte[] arbitratorSignature, @Nullable byte[] arbitratorSignature,
@Nullable String arbitratorPayoutTxSigned,
@Nullable String arbitratorUpdatedMultisigHex,
long buyerPayoutAmount, long buyerPayoutAmount,
long sellerPayoutAmount, long sellerPayoutAmount,
@Nullable byte[] arbitratorPubKey, @Nullable byte[] arbitratorPubKey,
long closeDate, long closeDate) {
boolean isLoserPublisher) {
this.tradeId = tradeId; this.tradeId = tradeId;
this.traderId = traderId; this.traderId = traderId;
this.winner = winner; this.winner = winner;
@ -132,13 +117,10 @@ public final class DisputeResult implements NetworkPayload {
this.summaryNotesProperty.set(summaryNotes); this.summaryNotesProperty.set(summaryNotes);
this.chatMessage = chatMessage; this.chatMessage = chatMessage;
this.arbitratorSignature = arbitratorSignature; this.arbitratorSignature = arbitratorSignature;
this.arbitratorSignedPayoutTxHex = arbitratorPayoutTxSigned;
this.arbitratorUpdatedMultisigHex = arbitratorUpdatedMultisigHex;
this.buyerPayoutAmount = buyerPayoutAmount; this.buyerPayoutAmount = buyerPayoutAmount;
this.sellerPayoutAmount = sellerPayoutAmount; this.sellerPayoutAmount = sellerPayoutAmount;
this.arbitratorPubKey = arbitratorPubKey; this.arbitratorPubKey = arbitratorPubKey;
this.closeDate = closeDate; this.closeDate = closeDate;
this.isLoserPublisher = isLoserPublisher;
} }
@ -157,13 +139,10 @@ public final class DisputeResult implements NetworkPayload {
proto.getSummaryNotes(), proto.getSummaryNotes(),
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
proto.getArbitratorSignature().toByteArray(), proto.getArbitratorSignature().toByteArray(),
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getArbitratorUpdatedMultisigHex()),
proto.getBuyerPayoutAmount(), proto.getBuyerPayoutAmount(),
proto.getSellerPayoutAmount(), proto.getSellerPayoutAmount(),
proto.getArbitratorPubKey().toByteArray(), proto.getArbitratorPubKey().toByteArray(),
proto.getCloseDate(), proto.getCloseDate());
proto.getIsLoserPublisher());
} }
@Override @Override
@ -178,13 +157,8 @@ public final class DisputeResult implements NetworkPayload {
.setSummaryNotes(summaryNotesProperty.get()) .setSummaryNotes(summaryNotesProperty.get())
.setBuyerPayoutAmount(buyerPayoutAmount) .setBuyerPayoutAmount(buyerPayoutAmount)
.setSellerPayoutAmount(sellerPayoutAmount) .setSellerPayoutAmount(sellerPayoutAmount)
.setCloseDate(closeDate) .setCloseDate(closeDate);
.setIsLoserPublisher(isLoserPublisher);
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(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name())));
Optional.ofNullable(chatMessage).ifPresent(chatMessage -> Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
@ -265,13 +239,10 @@ public final class DisputeResult implements NetworkPayload {
",\n summaryNotesProperty=" + summaryNotesProperty + ",\n summaryNotesProperty=" + summaryNotesProperty +
",\n chatMessage=" + chatMessage + ",\n chatMessage=" + chatMessage +
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\n arbitratorPayoutTxSigned=" + arbitratorSignedPayoutTxHex +
",\n arbitratorUpdatedMultisigHex=" + arbitratorUpdatedMultisigHex +
",\n buyerPayoutAmount=" + buyerPayoutAmount + ",\n buyerPayoutAmount=" + buyerPayoutAmount +
",\n sellerPayoutAmount=" + sellerPayoutAmount + ",\n sellerPayoutAmount=" + sellerPayoutAmount +
",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) +
",\n closeDate=" + closeDate + ",\n closeDate=" + closeDate +
",\n isLoserPublisher=" + isLoserPublisher +
"\n}"; "\n}";
} }
} }

View file

@ -22,7 +22,6 @@ import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService; import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportType; 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.DisputeManager;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeResult.Winner; import bisq.core.support.dispute.DisputeResult.Winner;
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest; import bisq.core.support.dispute.messages.DisputeOpenedMessage;
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.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage; import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.Tradable;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
@ -48,24 +42,20 @@ import bisq.core.util.ParsingUtils;
import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.AckMessageSourceType;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener; import common.utils.GenUtils;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j; 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.MoneroWallet;
import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxSet; import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -122,20 +110,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Received {} from {} with tradeId {} and uid {}", log.info("Received {} from {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid()); message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
if (message instanceof OpenNewDisputeMessage) { if (message instanceof DisputeOpenedMessage) {
onOpenNewDisputeMessage((OpenNewDisputeMessage) message); handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof PeerOpenedDisputeMessage) {
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
} else if (message instanceof ChatMessage) { } else if (message instanceof ChatMessage) {
onChatMessage((ChatMessage) message); handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeResultMessage) { } else if (message instanceof DisputeClosedMessage) {
onDisputeResultMessage((DisputeResultMessage) message); handleDisputeClosedMessage((DisputeClosedMessage) message);
} else if (message instanceof PeerPublishedDisputePayoutTxMessage) {
onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message);
} else if (message instanceof ArbitratorPayoutTxRequest) {
onArbitratorPayoutTxRequest((ArbitratorPayoutTxRequest) message);
} else if (message instanceof ArbitratorPayoutTxResponse) {
onArbitratorPayoutTxResponse((ArbitratorPayoutTxResponse) message);
} else { } else {
log.warn("Unsupported message at dispatchMessage. message={}", message); log.warn("Unsupported message at dispatchMessage. message={}", message);
} }
@ -147,11 +127,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
return dispute.getContract().getArbitratorNodeAddress(); return dispute.getContract().getArbitratorNodeAddress();
} }
@Override
protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.DISPUTE_STARTED_BY_PEER;
}
@Override @Override
protected AckMessageSourceType getAckMessageSourceType() { protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.ARBITRATION_MESSAGE; return AckMessageSourceType.ARBITRATION_MESSAGE;
@ -159,7 +134,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
@Override @Override
public void cleanupDisputes() { public void cleanupDisputes() {
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED)); // no action
} }
@Override @Override
@ -185,43 +160,52 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Message handler // Dispute handling
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// received by both peers when arbitrator closes disputes
@Override @Override
// We get that message at both peers. The dispute object is in context of the trader public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
ChatMessage chatMessage = disputeResult.getChatMessage(); ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null"); checkNotNull(chatMessage, "chatMessage must not be null");
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId());
String tradeId = 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<Dispute> disputeOptional = findDispute(disputeResult); Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeResultMessage.getUid(); String uid = disputeClosedMessage.getUid();
if (!disputeOptional.isPresent()) { if (!disputeOptional.isPresent()) {
log.warn("We got a dispute result msg but we don't have a matching dispute. " + log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
"That might happen when we get the disputeResultMessage before the dispute was created. " + "That might happen when we get the DisputeClosedMessage before the dispute was created. " +
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); "We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first // 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); delayMsgMap.put(uid, timer);
} else { } 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); "That should never happen. TradeId = " + tradeId);
} }
return; return;
} }
Dispute dispute = disputeOptional.get(); Dispute dispute = disputeOptional.get();
// verify that arbitrator does not get DisputeResultMessage // verify that arbitrator does not get DisputeClosedMessage
if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) { if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) {
log.error("Arbitrator received disputeResultMessage. That must never happen."); log.error("Arbitrator received disputeResultMessage. That should never happen.");
return; return;
} }
// set dispute state
cleanupRetryMap(uid); cleanupRetryMap(uid);
if (!dispute.getChatMessages().contains(chatMessage)) { if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
@ -229,281 +213,76 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
} }
dispute.setIsClosed(); dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) { if (dispute.disputeResultProperty().get() != null) {
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " + log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
"again because the first close did not succeed. TradeId = " + tradeId); "again because the first close did not succeed. TradeId = " + tradeId);
} }
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);
// import multisig hex
List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().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();
// run off main thread
new Thread(() -> {
String errorMessage = null; String errorMessage = null;
boolean success = true; boolean success = true;
boolean requestUpdatedPayoutTx = false;
Contract contract = dispute.getContract(); // attempt to sign and publish dispute payout tx if given and not already published
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
// wait to sign and publish payout tx if defer flag set
if (disputeClosedMessage.isDeferPublishPayout()) {
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
trade.syncWallet();
}
// sign and publish dispute payout tx if peer still has not published
if (!trade.isPayoutPublished()) {
try { try {
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb) signAndPublishDisputePayoutTx(trade, disputeClosedMessage.getUnsignedPayoutTxHex());
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
// more BTC as he has deposited
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
DisputeResult.Winner publisher = disputeResult.getWinner();
// Sometimes the user who receives the trade amount is never online, so we might want to
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
// Default isLoserPublisher is set to false
if (disputeResult.isLoserPublisher()) {
// we invert the logic
if (publisher == DisputeResult.Winner.BUYER)
publisher = DisputeResult.Winner.SELLER;
else if (publisher == DisputeResult.Winner.SELLER)
publisher = DisputeResult.Winner.BUYER;
}
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)) {
MoneroTxWallet payoutTx = null;
if (tradeOptional.isPresent()) {
payoutTx = tradeOptional.get().getPayoutTx();
} else {
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); // TODO (woodser): payout tx is transient so won't exist after restart?
}
}
if (payoutTx == null) {
// gather relevant info
String arbitratorSignedPayoutTxHex = disputeResult.getArbitratorSignedPayoutTxHex();
if (arbitratorSignedPayoutTxHex != null) {
if (!tradeOptional.isPresent()) throw new RuntimeException("Trade must not be null when trader signs arbitrator's payout tx");
try {
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
onTraderSignedDisputePayoutTx(tradeId, txSet);
} catch (Exception e) { } catch (Exception e) {
// check if payout published again
trade.syncWallet();
if (trade.isPayoutPublished()) {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else {
e.printStackTrace(); e.printStackTrace();
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId + " SignedPayoutTx = " + arbitratorSignedPayoutTxHex; errorMessage = "Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
log.warn(errorMessage); log.warn(errorMessage);
success = false; success = false;
} }
} else {
requestUpdatedPayoutTx = true;
} }
} else { } else {
log.warn("We already got a payout tx. That might be the case if the other peer did not get the " + log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
"payout tx and opened a dispute. TradeId = " + tradeId);
} }
} else { } else {
log.trace("We don't publish the tx as we are not the winning party."); if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
// Clean up tangling trades else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getName(), trade.getId());
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
closeTradeOrOffer(tradeId);
} }
}
}
// catch (TransactionVerificationException e) {
// errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
// log.error(errorMessage, e);
// success = false;
//
// // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
// // we get a TransactionVerificationException. No reason to keep that dispute open...
// updateTradeOrOpenOfferManager(tradeId);
//
// throw new RuntimeException(errorMessage);
// }
// catch (AddressFormatException | WalletException e) {
catch (Exception e) {
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
log.error(errorMessage, e);
success = false;
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used // We use the chatMessage as we only persist those not the DisputeClosedMessage.
// we get a TransactionVerificationException. No reason to keep that dispute open... // If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
closeTradeOrOffer(tradeId); // TODO (woodser): only close in case of verification exception?
throw new RuntimeException(errorMessage);
} finally {
// We use the chatMessage as we only persist those not the disputeResultMessage.
// If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage.
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
// If dispute opener's peer is co-signer, send updated multisig hex to arbitrator to receive updated payout tx
if (requestUpdatedPayoutTx) {
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // TODO (woodser): this is closed after sending ArbitratorPayoutTxRequest to arbitrator which opens and syncs multisig and responds with signed dispute tx. more efficient way is to include with arbitrator-signed dispute tx with dispute result?
sendArbitratorPayoutTxRequest(multisigWallet.exportMultisigHex(), dispute, contract);
}
}
}
requestPersistence(); requestPersistence();
}).start();
} }
// Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) {
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
String uid = peerPublishedDisputePayoutTxMessage.getUid();
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
// get dispute and trade
Optional<Dispute> 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();
}
}
// 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();
// 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 // gather trade info
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); MoneroWallet multisigWallet = trade.getWallet();
multisigWallet.sync(); Optional<Dispute> disputeOptional = findDispute(trade.getId());
Optional<Dispute> disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + trade.getId());
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(); Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract(); Contract contract = dispute.getContract();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
@ -514,10 +293,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER); // BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
// parse arbitrator-signed payout tx // parse arbitrator-signed payout tx
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); MoneroTxSet signedTxSet = 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 if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0); MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0);
log.info("Received updated multisig hex and partially signed payout tx from arbitrator:\n" + arbitratorSignedPayoutTx);
// verify payout tx has 1 or 2 destinations // verify payout tx has 1 or 2 destinations
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size(); int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
@ -553,168 +331,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount); 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); 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 // sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
parsedTxSet.setMultisigTxHex(signedMultisigTxHex); signedTxSet.setMultisigTxHex(signedMultisigTxHex);
return parsedTxSet;
}
private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) {
// gather trade info
Optional<Dispute> 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 // submit fully signed payout tx to the network
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // closed when trade completed (TradeManager.onTradeCompleted()) List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
List<String> txHashes = multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex()); signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
txSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
// update state // update state
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
trade.setPayoutTxId(txSet.getTxs().get(0).getHash()); trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash());
trade.setPayoutState(Trade.PayoutState.PUBLISHED); trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash()); dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash());
sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract); return signedTxSet;
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<OpenOffer> 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;
} }
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -29,9 +29,8 @@ import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.DisputeOpenedMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage; import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
@ -107,25 +106,18 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
log.info("Received {} with tradeId {} and uid {}", log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof OpenNewDisputeMessage) { if (message instanceof DisputeOpenedMessage) {
onOpenNewDisputeMessage((OpenNewDisputeMessage) message); handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof PeerOpenedDisputeMessage) {
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
} else if (message instanceof ChatMessage) { } else if (message instanceof ChatMessage) {
onChatMessage((ChatMessage) message); handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeResultMessage) { } else if (message instanceof DisputeClosedMessage) {
onDisputeResultMessage((DisputeResultMessage) message); handleDisputeClosedMessage((DisputeClosedMessage) message);
} else { } else {
log.warn("Unsupported message at dispatchMessage. message={}", message); log.warn("Unsupported message at dispatchMessage. message={}", message);
} }
} }
} }
@Override
protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.MEDIATION_STARTED_BY_PEER;
}
@Override @Override
protected AckMessageSourceType getAckMessageSourceType() { protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.MEDIATION_MESSAGE; return AckMessageSourceType.MEDIATION_MESSAGE;
@ -164,7 +156,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
@Override @Override
// We get that message at both peers. The dispute object is in context of the trader // 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(); DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
String tradeId = disputeResult.getTradeId(); String tradeId = disputeResult.getTradeId();
ChatMessage chatMessage = disputeResult.getChatMessage(); ChatMessage chatMessage = disputeResult.getChatMessage();
@ -177,7 +169,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first // 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); delayMsgMap.put(uid, timer);
} else { } 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 result msg after we already repeated to apply the message after a delay. " +

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -23,27 +23,41 @@ import bisq.core.support.dispute.DisputeResult;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.proto.ProtoUtil;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import java.util.Optional;
import javax.annotation.Nullable;
@Value @Value
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class DisputeResultMessage extends DisputeMessage { public final class DisputeClosedMessage extends DisputeMessage {
private final DisputeResult disputeResult; private final DisputeResult disputeResult;
private final NodeAddress senderNodeAddress; 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, NodeAddress senderNodeAddress,
String uid, String uid,
SupportType supportType) { SupportType supportType,
String updatedMultisigHex,
@Nullable String unsignedPayoutTxHex,
boolean deferPublishPayout) {
this(disputeResult, this(disputeResult,
senderNodeAddress, senderNodeAddress,
uid, uid,
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
supportType); supportType,
updatedMultisigHex,
unsignedPayoutTxHex,
deferPublishPayout);
} }
@ -51,34 +65,45 @@ public final class DisputeResultMessage extends DisputeMessage {
// PROTO BUFFER // PROTO BUFFER
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private DisputeResultMessage(DisputeResult disputeResult, private DisputeClosedMessage(DisputeResult disputeResult,
NodeAddress senderNodeAddress, NodeAddress senderNodeAddress,
String uid, String uid,
String messageVersion, String messageVersion,
SupportType supportType) { SupportType supportType,
String updatedMultisigHex,
String unsignedPayoutTxHex,
boolean deferPublishPayout) {
super(messageVersion, uid, supportType); super(messageVersion, uid, supportType);
this.disputeResult = disputeResult; this.disputeResult = disputeResult;
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex;
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
this.deferPublishPayout = deferPublishPayout;
} }
@Override @Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder() protobuf.DisputeClosedMessage.Builder builder = protobuf.DisputeClosedMessage.newBuilder()
.setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder()
.setDisputeResult(disputeResult.toProtoMessage()) .setDisputeResult(disputeResult.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid) .setUid(uid)
.setType(SupportType.toProtoMessage(supportType))) .setType(SupportType.toProtoMessage(supportType))
.build(); .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"); 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()), NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
SupportType.fromProto(proto.getType())); SupportType.fromProto(proto.getType()),
proto.getUpdatedMultisigHex(),
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
proto.getDeferPublishPayout());
} }
@Override @Override
@ -88,12 +113,13 @@ public final class DisputeResultMessage extends DisputeMessage {
@Override @Override
public String toString() { public String toString() {
return "DisputeResultMessage{" + return "DisputeClosedMessage{" +
"\n disputeResult=" + disputeResult + "\n disputeResult=" + disputeResult +
",\n senderNodeAddress=" + senderNodeAddress + ",\n senderNodeAddress=" + senderNodeAddress +
",\n DisputeResultMessage.uid='" + uid + '\'' + ",\n DisputeClosedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion + ",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType + ",\n supportType=" + supportType +
",\n deferPublishPayout=" + deferPublishPayout +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -20,9 +20,11 @@ package bisq.core.support.dispute.messages;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import java.util.Optional;
import bisq.common.app.Version; import bisq.common.app.Version;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -30,22 +32,25 @@ import lombok.Value;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Value
public final class OpenNewDisputeMessage extends DisputeMessage { public final class DisputeOpenedMessage extends DisputeMessage {
private final Dispute dispute; private final Dispute dispute;
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex; private final String updatedMultisigHex;
private final PaymentSentMessage paymentSentMessage;
public OpenNewDisputeMessage(Dispute dispute, public DisputeOpenedMessage(Dispute dispute,
NodeAddress senderNodeAddress, NodeAddress senderNodeAddress,
String uid, String uid,
SupportType supportType, SupportType supportType,
String updatedMultisigHex) { String updatedMultisigHex,
PaymentSentMessage paymentSentMessage) {
this(dispute, this(dispute,
senderNodeAddress, senderNodeAddress,
uid, uid,
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
supportType, supportType,
updatedMultisigHex); updatedMultisigHex,
paymentSentMessage);
} }
@ -53,39 +58,42 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
// PROTO BUFFER // PROTO BUFFER
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private OpenNewDisputeMessage(Dispute dispute, private DisputeOpenedMessage(Dispute dispute,
NodeAddress senderNodeAddress, NodeAddress senderNodeAddress,
String uid, String uid,
String messageVersion, String messageVersion,
SupportType supportType, SupportType supportType,
String updatedMultisigHex) { String updatedMultisigHex,
PaymentSentMessage paymentSentMessage) {
super(messageVersion, uid, supportType); super(messageVersion, uid, supportType);
this.dispute = dispute; this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex; this.updatedMultisigHex = updatedMultisigHex;
this.paymentSentMessage = paymentSentMessage;
} }
@Override @Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder() protobuf.DisputeOpenedMessage.Builder builder = protobuf.DisputeOpenedMessage.newBuilder()
.setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder()
.setUid(uid) .setUid(uid)
.setDispute(dispute.toProtoMessage()) .setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType)) .setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex)) .setUpdatedMultisigHex(updatedMultisigHex);
.build(); 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, CoreProtoResolver coreProtoResolver,
String messageVersion) { String messageVersion) {
return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), return new DisputeOpenedMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
NodeAddress.fromProto(proto.getSenderNodeAddress()), NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
SupportType.fromProto(proto.getType()), SupportType.fromProto(proto.getType()),
proto.getUpdatedMultisigHex()); proto.getUpdatedMultisigHex(),
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
} }
@Override @Override
@ -95,13 +103,14 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
@Override @Override
public String toString() { public String toString() {
return "OpenNewDisputeMessage{" + return "DisputeOpenedMessage{" +
"\n dispute=" + dispute + "\n dispute=" + dispute +
",\n senderNodeAddress=" + senderNodeAddress + ",\n senderNodeAddress=" + senderNodeAddress +
",\n OpenNewDisputeMessage.uid='" + uid + '\'' + ",\n DisputeOpenedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion + ",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType + ",\n supportType=" + supportType +
",\n updatedMultisigHex=" + updatedMultisigHex + ",\n updatedMultisigHex=" + updatedMultisigHex +
",\n paymentSentMessage=" + paymentSentMessage +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -29,9 +29,8 @@ import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.DisputeOpenedMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage; import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
@ -101,25 +100,18 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
log.info("Received {} with tradeId {} and uid {}", log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof OpenNewDisputeMessage) { if (message instanceof DisputeOpenedMessage) {
onOpenNewDisputeMessage((OpenNewDisputeMessage) message); handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof PeerOpenedDisputeMessage) {
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
} else if (message instanceof ChatMessage) { } else if (message instanceof ChatMessage) {
onChatMessage((ChatMessage) message); handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeResultMessage) { } else if (message instanceof DisputeClosedMessage) {
onDisputeResultMessage((DisputeResultMessage) message); handleDisputeClosedMessage((DisputeClosedMessage) message);
} else { } else {
log.warn("Unsupported message at dispatchMessage. message={}", message); log.warn("Unsupported message at dispatchMessage. message={}", message);
} }
} }
} }
@Override
protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER;
}
@Override @Override
protected AckMessageSourceType getAckMessageSourceType() { protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.REFUND_MESSAGE; return AckMessageSourceType.REFUND_MESSAGE;
@ -161,7 +153,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
@Override @Override
// We get that message at both peers. The dispute object is in context of the trader // 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(); DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
String tradeId = disputeResult.getTradeId(); String tradeId = disputeResult.getTradeId();
ChatMessage chatMessage = disputeResult.getChatMessage(); ChatMessage chatMessage = disputeResult.getChatMessage();
@ -174,7 +166,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first // 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); delayMsgMap.put(uid, timer);
} else { } 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 result msg after we already repeated to apply the message after a delay. " +

View file

@ -154,7 +154,7 @@ public class TraderChatManager extends SupportManager {
log.info("Received {} with tradeId {} and uid {}", log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof ChatMessage) { if (message instanceof ChatMessage) {
onChatMessage((ChatMessage) message); handleChatMessage((ChatMessage) message);
} else { } else {
log.warn("Unsupported message at dispatchMessage. message={}", message); log.warn("Unsupported message at dispatchMessage. message={}", message);
} }

View file

@ -24,7 +24,11 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.util.JsonUtil; import bisq.core.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -32,9 +36,12 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.google.common.base.Charsets;
/** /**
* Collection of utilities. * Collection of utilities.
*/ */
@Slf4j
public class HavenoUtils { public class HavenoUtils {
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node 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 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 * @return true if the arbitrator's signature is valid for the offer
*/ */
public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) { public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) {
@ -92,15 +99,11 @@ public class HavenoUtils {
String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy); String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy);
// verify arbitrator signature // verify arbitrator signature
boolean isValid = true;
try { try {
isValid = Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature); return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
} catch (Exception e) { } 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) { public static void awaitLatch(CountDownLatch latch) {
try { try {
latch.await(); latch.await();
@ -157,13 +225,17 @@ public class HavenoUtils {
} }
} }
public static void awaitTasks(Collection<Runnable> tasks) { public static void executeTasks(Collection<Runnable> tasks) {
executeTasks(tasks, tasks.size());
}
public static void executeTasks(Collection<Runnable> tasks, int poolSize) {
if (tasks.isEmpty()) return; if (tasks.isEmpty()) return;
ExecutorService pool = Executors.newFixedThreadPool(tasks.size()); ExecutorService pool = Executors.newFixedThreadPool(poolSize);
for (Runnable task : tasks) pool.submit(task); for (Runnable task : tasks) pool.submit(task);
pool.shutdown(); pool.shutdown();
try { try {
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow();
} catch (InterruptedException e) { } catch (InterruptedException e) {
pool.shutdownNow(); pool.shutdownNow();
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -73,7 +73,11 @@ public abstract class SellerTrade extends Trade {
return true; return true;
case DISPUTE_REQUESTED: 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 DISPUTE_CLOSED:
case MEDIATION_REQUESTED: case MEDIATION_REQUESTED:
case MEDIATION_STARTED_BY_PEER: case MEDIATION_STARTED_BY_PEER:

View file

@ -35,6 +35,7 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.ProcessModel;
import bisq.core.trade.protocol.ProcessModelServiceProvider; import bisq.core.trade.protocol.ProcessModelServiceProvider;
import bisq.core.trade.protocol.TradeListener; import bisq.core.trade.protocol.TradeListener;
import bisq.core.trade.protocol.TradeProtocol;
import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.TradingPeer;
import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.trade.txproof.AssetTxProofResult;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
@ -44,6 +45,7 @@ import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.crypto.Encryption; import bisq.common.crypto.Encryption;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import bisq.common.taskrunner.Model; import bisq.common.taskrunner.Model;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
@ -72,6 +74,7 @@ import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Getter; import lombok.Getter;
@ -214,10 +217,10 @@ public abstract class Trade implements Tradable, Model {
} }
public enum PayoutState { public enum PayoutState {
UNPUBLISHED, PAYOUT_UNPUBLISHED,
PUBLISHED, PAYOUT_PUBLISHED,
CONFIRMED, PAYOUT_CONFIRMED,
UNLOCKED; PAYOUT_UNLOCKED;
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) { public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name()); return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
@ -234,9 +237,12 @@ public abstract class Trade implements Tradable, Model {
public enum DisputeState { public enum DisputeState {
NO_DISPUTE, NO_DISPUTE,
// arbitration DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager
DISPUTE_REQUESTED, DISPUTE_OPENED,
DISPUTE_STARTED_BY_PEER, 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, DISPUTE_CLOSED,
// mediation // mediation
@ -268,12 +274,12 @@ public abstract class Trade implements Tradable, Model {
} }
public boolean isArbitrated() { public boolean isArbitrated() {
return this == Trade.DisputeState.DISPUTE_REQUESTED || if (isMediated()) return false; // TODO: remove mediation?
this == Trade.DisputeState.DISPUTE_STARTED_BY_PEER || return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
this == Trade.DisputeState.DISPUTE_CLOSED || }
this == Trade.DisputeState.REFUND_REQUESTED ||
this == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER || public boolean isClosed() {
this == Trade.DisputeState.REFUND_REQUEST_CLOSED; return this == DisputeState.DISPUTE_CLOSED;
} }
} }
@ -324,7 +330,7 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
private State state = State.PREPARATION; private State state = State.PREPARATION;
@Getter @Getter
private PayoutState payoutState = PayoutState.UNPUBLISHED; private PayoutState payoutState = PayoutState.PAYOUT_UNPUBLISHED;
@Getter @Getter
private DisputeState disputeState = DisputeState.NO_DISPUTE; private DisputeState disputeState = DisputeState.NO_DISPUTE;
@Getter @Getter
@ -365,11 +371,13 @@ public abstract class Trade implements Tradable, Model {
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState); transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
transient private Subscription tradePhaseSubscription = null; transient private Subscription tradePhaseSubscription;
transient private Subscription payoutStateSubscription = null; transient private Subscription payoutStateSubscription;
transient private TaskLooper tradeTxsLooper; 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 private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds
// Mutable // Mutable
@Getter @Getter
@ -435,8 +443,9 @@ public abstract class Trade implements Tradable, Model {
private String payoutTxKey; private String payoutTxKey;
private Long startTime; // cache private Long startTime; // cache
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization // Constructors
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// maker // maker
@ -530,96 +539,56 @@ public abstract class Trade implements Tradable, Model {
setAmount(tradeAmount); setAmount(tradeAmount);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER // Listeners
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Override public void addListener(TradeListener listener) {
public Message toProtoMessage() { tradeListeners.add(listener);
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) { public void removeListener(TradeListener listener) {
trade.setTakeOfferDate(proto.getTakeOfferDate()); if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered");
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() // notified from TradeProtocol of verified trade messages
.map(ChatMessage::fromPayloadProto) public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
.collect(Collectors.toList())); for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onVerifiedTradeMessage(message, sender);
return trade;
} }
}
// notified from TradeProtocol of ack messages
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onAckMessage(ackMessage, sender);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(ProcessModelServiceProvider serviceProvider) { public void initialize(ProcessModelServiceProvider serviceProvider) {
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> { serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
}); });
isInitialized = true; // TODO: move to end?
// listen to daemon connection // listen to daemon connection
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection)); xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
// done if payout unlocked // check if done
if (isPayoutUnlocked()) return; if (isPayoutUnlocked()) return;
// handle trade state events // handle trade state events
if (isDepositPublished()) listenToTradeTxs();
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
updateTxListenerRefreshPeriod(); if (!isInitialized) return;
if (isDepositPublished()) listenToTradeTxs(); if (isDepositPublished() && !isPayoutUnlocked()) {
updateWalletRefreshPeriod();
listenToTradeTxs();
}
if (isCompleted()) { if (isCompleted()) {
UserThread.execute(() -> { UserThread.execute(() -> {
if (tradePhaseSubscription != null) { if (tradePhaseSubscription != null) {
@ -632,17 +601,23 @@ public abstract class Trade implements Tradable, Model {
// handle payout state events // handle payout state events
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> { payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
updateTxListenerRefreshPeriod(); if (!isInitialized) return;
if (isPayoutPublished()) updateWalletRefreshPeriod();
// cleanup when payout published // cleanup when payout published
if (isPayoutPublished()) { if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) {
log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); 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()); processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
} }
// cleanup when payout unlocks // 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? log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
deleteWallet(); deleteWallet();
if (tradeTxsLooper != null) { 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);
}
} }
public TradeProtocol getProtocol() {
/////////////////////////////////////////////////////////////////////////////////////////// return processModel.getTradeManager().getTradeProtocol(this);
// API }
///////////////////////////////////////////////////////////////////////////////////////////
public void setMyNodeAddress() { public void setMyNodeAddress() {
getSelf().setNodeAddress(P2PService.getMyNodeAddress()); getSelf().setNodeAddress(P2PService.getMyNodeAddress());
@ -755,9 +742,11 @@ public abstract class Trade implements Tradable, Model {
// exception expected // exception expected
} }
} }
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts"); 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; return payoutTx;
} }
@ -827,7 +816,7 @@ public abstract class Trade implements Tradable, Model {
// submit payout tx // submit payout tx
if (publish) { if (publish) {
multisigWallet.submitMultisigTxHex(payoutTxHex); 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() { 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()); log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
getWallet().sync(); getWallet().sync();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
pollWallet(); 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() { public void saveWallet() {
@ -938,6 +939,12 @@ public abstract class Trade implements Tradable, Model {
public void shutDown() { public void shutDown() {
isInitialized = false; 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(); 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<TradeListener>(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<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onAckMessage(ackMessage, sender);
}
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Setters // Setters
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -1031,7 +1012,7 @@ public abstract class Trade implements Tradable, Model {
public void setPayoutState(PayoutState payoutState) { public void setPayoutState(PayoutState payoutState) {
if (isInitialized) { if (isInitialized) {
// We don't want to log at startup the setState calls from all persisted trades // 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()) { if (payoutState.ordinal() < this.payoutState.ordinal()) {
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" + 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) { 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; this.disputeState = disputeState;
UserThread.execute(() -> {
disputeStateProperty.set(disputeState); disputeStateProperty.set(disputeState);
});
}
public void setDisputeStateIfProgress(DisputeState disputeState) {
if (disputeState.ordinal() > getDisputeState().ordinal()) setDisputeState(disputeState);
} }
public void setMediationResultState(MediationResultState mediationResultState) { public void setMediationResultState(MediationResultState mediationResultState) {
@ -1140,11 +1137,7 @@ public abstract class Trade implements Tradable, Model {
return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker(); return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker();
} }
/** // get the taker if maker, maker if taker, null if arbitrator
* Get the taker if maker, maker if taker, null if arbitrator.
*
* @return the trade peer
*/
public TradingPeer getTradingPeer() { public TradingPeer getTradingPeer() {
if (this instanceof MakerTrade) return processModel.getTaker(); if (this instanceof MakerTrade) return processModel.getTaker();
else if (this instanceof TakerTrade) return processModel.getMaker(); else if (this instanceof TakerTrade) return processModel.getMaker();
@ -1152,19 +1145,19 @@ public abstract class Trade implements Tradable, Model {
else throw new RuntimeException("Unknown trade type: " + getClass().getName()); else throw new RuntimeException("Unknown trade type: " + getClass().getName());
} }
/** // TODO (woodser): this naming convention is confusing
* 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
*/
public TradingPeer getTradingPeer(NodeAddress address) { public TradingPeer getTradingPeer(NodeAddress address) {
if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker(); if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker();
if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker(); if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker();
if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator(); 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() { public Date getTakeOfferDate() {
@ -1210,12 +1203,10 @@ public abstract class Trade implements Tradable, Model {
private long getStartTime() { private long getStartTime() {
if (startTime != null) return startTime; if (startTime != null) return startTime;
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
final MoneroTx takerDepositTx = getTakerDepositTx(); if (isDepositConfirmed() && getTakeOfferDate() != null) {
final MoneroTx makerDepositTx = getMakerDepositTx();
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
if (isDepositUnlocked()) { if (isDepositUnlocked()) {
final long tradeTime = getTakeOfferDate().getTime(); 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(); MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); 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: {}", 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)); new Date(startTime), new Date(tradeTime), new Date(blockTime));
} else { } 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; startTime = now;
} }
} else { } else {
@ -1259,34 +1250,7 @@ public abstract class Trade implements Tradable, Model {
} }
public boolean isFundsLockedIn() { public boolean isFundsLockedIn() {
// If no deposit tx was published we have no funds locked in return isDepositPublished() && !isPayoutPublished();
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;
} }
public boolean isDepositConfirmed() { public boolean isDepositConfirmed() {
@ -1310,15 +1274,15 @@ public abstract class Trade implements Tradable, Model {
} }
public boolean isPayoutPublished() { public boolean isPayoutPublished() {
return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal(); return getPayoutState().ordinal() >= PayoutState.PAYOUT_PUBLISHED.ordinal();
} }
public boolean isPayoutConfirmed() { public boolean isPayoutConfirmed() {
return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal(); return getPayoutState().ordinal() >= PayoutState.PAYOUT_CONFIRMED.ordinal();
} }
public boolean isPayoutUnlocked() { public boolean isPayoutUnlocked() {
return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal(); return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal();
} }
public ReadOnlyObjectProperty<State> stateProperty() { public ReadOnlyObjectProperty<State> stateProperty() {
@ -1439,17 +1403,12 @@ public abstract class Trade implements Tradable, Model {
// poll wallet for tx state // poll wallet for tx state
pollWallet(); pollWallet();
tradeTxsLooper = new TaskLooper(() -> { tradeTxsLooper = new TaskLooper(() -> { pollWallet(); });
try { tradeTxsLooper.start(walletRefreshPeriod);
pollWallet();
} catch (Exception e) {
if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage());
}
});
tradeTxsLooper.start(getWalletRefreshPeriod());
} }
private void pollWallet() { private void pollWallet() {
try {
// skip if payout unlocked // skip if payout unlocked
if (isPayoutUnlocked()) return; if (isPayoutUnlocked()) return;
@ -1498,21 +1457,27 @@ public abstract class Trade implements Tradable, Model {
if (!payoutTx.isLocked()) setPayoutStateUnlocked(); 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) { private void setDaemonConnection(MoneroRpcConnection connection) {
if (getWallet() == null) return;
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri()); log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId()); getWallet().setDaemonConnection(connection);
multisigWallet.setDaemonConnection(connection); updateWalletRefreshPeriod();
multisigWallet.startSyncing(getWalletRefreshPeriod());
updateTxListenerRefreshPeriod();
} }
private void updateTxListenerRefreshPeriod() { private void updateWalletRefreshPeriod() {
long walletRefreshPeriod = getWalletRefreshPeriod(); setWalletRefreshPeriod(getWalletRefreshPeriod());
if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return; }
log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod);
lastWalletRefreshPeriod = walletRefreshPeriod; 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) { if (tradeTxsLooper != null) {
tradeTxsLooper.stop(); tradeTxsLooper.stop();
tradeTxsLooper = null; tradeTxsLooper = null;
@ -1521,8 +1486,8 @@ public abstract class Trade implements Tradable, Model {
} }
private long getWalletRefreshPeriod() { 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 if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // slow arbitrator trade wallet after deposits confirm
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // else sync at default rate
} }
private void setStateDepositsPublished() { private void setStateDepositsPublished() {
@ -1538,15 +1503,87 @@ public abstract class Trade implements Tradable, Model {
} }
private void setPayoutStatePublished() { private void setPayoutStatePublished() {
if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED); if (!isPayoutPublished()) setPayoutState(PayoutState.PAYOUT_PUBLISHED);
} }
private void setPayoutStateConfirmed() { private void setPayoutStateConfirmed() {
if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED); if (!isPayoutConfirmed()) setPayoutState(PayoutState.PAYOUT_CONFIRMED);
} }
private void setPayoutStateUnlocked() { 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 @Override

View file

@ -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.Arbitrator;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.trade.Trade.DisputeState;
import bisq.core.trade.Trade.Phase; import bisq.core.trade.Trade.Phase;
import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.failed.FailedTradesManager;
import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.handlers.TradeResultHandler;
@ -75,7 +76,6 @@ import bisq.common.proto.persistable.PersistedDataHost;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.LongProperty; import javafx.beans.property.LongProperty;
@ -96,9 +96,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; 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.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -252,7 +249,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
if (p2PService.isBootstrapped()) { if (p2PService.isBootstrapped()) {
initPersistedTrades(); new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread
} else { } else {
p2PService.addP2PServiceListener(new BootstrapListener() { p2PService.addP2PServiceListener(new BootstrapListener() {
@Override @Override
@ -266,12 +263,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
onTradesChanged(); onTradesChanged();
xmrWalletService.setTradeManager(this); 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 // thaw unreserved outputs
thawUnreservedOutputs(); thawUnreservedOutputs();
@ -292,9 +283,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
trade.shutDown(); trade.shutDown();
} catch (Exception e) { } catch (Exception e) {
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?"); log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
e.printStackTrace();
} }
}); });
HavenoUtils.awaitTasks(tasks); HavenoUtils.executeTasks(tasks);
} }
private void thawUnreservedOutputs() { private void thawUnreservedOutputs() {
@ -346,35 +338,41 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initPersistedTrades() { private void initPersistedTrades() {
// get all trades // TODO: getAllTrades()
List<Trade> trades = new ArrayList<Trade>();
trades.addAll(tradableList.getList());
trades.addAll(closedTradableManager.getClosedTrades());
trades.addAll(failedTradesManager.getObservableList());
// open trades in parallel since each may open a multisig wallet // open trades in parallel since each may open a multisig wallet
List<Trade> trades = tradableList.getList(); int threadPoolSize = 10;
if (!trades.isEmpty()) { Set<Runnable> tasks = new HashSet<Runnable>();
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, trades.size()));
for (Trade trade : trades) { for (Trade trade : trades) {
pool.submit(new Runnable() { tasks.add(new Runnable() {
@Override @Override
public void run() { public void run() {
initPersistedTrade(trade); initPersistedTrade(trade);
} }
}); });
} };
pool.shutdown(); HavenoUtils.executeTasks(tasks, threadPoolSize);
try {
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); // reset any available address entries
} catch (InterruptedException e) { xmrWalletService.getAddressEntriesForAvailableBalanceStream()
pool.shutdownNow(); .filter(addressEntry -> addressEntry.getOfferId() != null)
throw new RuntimeException(e); .forEach(addressEntry -> {
} log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
} xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
});
persistedTradesInitialized.set(true); persistedTradesInitialized.set(true);
// We do not include failed trades as they should not be counted anyway in the trade statistics // We do not include failed trades as they should not be counted anyway in the trade statistics
Set<Trade> allTrades = new HashSet<>(closedTradableManager.getClosedTrades()); Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
allTrades.addAll(tradableList.getList()); nonFailedTrades.addAll(tradableList.getList());
String referralId = referralIdService.getOptionalReferralId().orElse(null); String referralId = referralIdService.getOptionalReferralId().orElse(null);
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
tradeStatisticsManager.maybeRepublishTradeStatistics(allTrades, referralId, isTorNetworkNode); tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
} }
private void initPersistedTrade(Trade trade) { private void initPersistedTrade(Trade trade) {
@ -485,6 +483,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
removeTrade(trade); removeTrade(trade);
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
}); });
requestPersistence(); requestPersistence();
@ -568,8 +567,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
log.warn("Maker error during trade initialization: " + errorMessage); log.warn("Maker error during trade initialization: " + errorMessage);
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
removeTrade(trade); removeTrade(trade);
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
}); });
@ -589,7 +588,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId()); Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) { if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId()); log.warn("No trade with id " + request.getTradeId() + " at node " + P2PService.getMyNodeAddress());
return; return;
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
@ -751,8 +750,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
requestPersistence(); requestPersistence();
}, errorMessage -> { }, errorMessage -> {
log.warn("Taker error during trade initialization: " + errorMessage); log.warn("Taker error during trade initialization: " + errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
removeTrade(trade); removeTrade(trade);
errorMessageHandler.handleErrorMessage(errorMessage);
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
}); });
requestPersistence(); 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 // 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) { public void onTradeCompleted(Trade trade) {
if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed");
closedTradableManager.add(trade); closedTradableManager.add(trade);
trade.setState(Trade.State.TRADE_COMPLETED); trade.setState(Trade.State.TRADE_COMPLETED);
removeTrade(trade); removeTrade(trade);
@ -818,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// Dispute // Dispute
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) { public void closeDisputedTrade(String tradeId, DisputeState disputeState) {
Optional<Trade> tradeOptional = getOpenTrade(tradeId); Optional<Trade> tradeOptional = getOpenTrade(tradeId);
if (tradeOptional.isPresent()) { if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get(); 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 tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
} else { } else {
log.warn("We found a closed trade with locked up funds. " + 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 { } 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()))); 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()))); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
} else { } else {
log.warn("We found a closed trade with locked up funds. " + 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 { } 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()))); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
} }
return trade.getId(); return trade.getId();
@ -1026,7 +1029,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
private synchronized void removeTrade(Trade trade) { private synchronized void removeTrade(Trade trade) {
log.info("TradeManager.removeTrade()"); log.info("TradeManager.removeTrade() " + trade.getId());
synchronized(tradableList) { synchronized(tradableList) {
if (!tradableList.contains(trade)) return; if (!tradableList.contains(trade)) return;

View file

@ -19,7 +19,6 @@ package bisq.core.trade.messages;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import java.util.Optional; import java.util.Optional;
@ -33,7 +32,7 @@ import lombok.Value;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Value
public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage { public final class DepositsConfirmedMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing; private final PubKeyRing pubKeyRing;
@Nullable @Nullable

View file

@ -29,14 +29,18 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.Value; import lombok.Value;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.google.protobuf.ByteString;
@Slf4j @Slf4j
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Getter
public final class PaymentReceivedMessage extends TradeMailboxMessage { public final class PaymentReceivedMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
@Nullable @Nullable
@ -44,7 +48,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
@Nullable @Nullable
private final String signedPayoutTxHex; private final String signedPayoutTxHex;
private final String updatedMultisigHex; 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 // Added in v1.4.0
@Nullable @Nullable
@ -56,7 +64,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
String unsignedPayoutTxHex, String unsignedPayoutTxHex,
String signedPayoutTxHex, String signedPayoutTxHex,
String updatedMultisigHex, String updatedMultisigHex,
boolean sawArrivedPaymentReceivedMsg) { boolean deferPublishPayout,
PaymentSentMessage paymentSentMessage) {
this(tradeId, this(tradeId,
senderNodeAddress, senderNodeAddress,
signedWitness, signedWitness,
@ -65,7 +74,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
unsignedPayoutTxHex, unsignedPayoutTxHex,
signedPayoutTxHex, signedPayoutTxHex,
updatedMultisigHex, updatedMultisigHex,
sawArrivedPaymentReceivedMsg); deferPublishPayout,
paymentSentMessage);
} }
@ -81,14 +91,16 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
String unsignedPayoutTxHex, String unsignedPayoutTxHex,
String signedPayoutTxHex, String signedPayoutTxHex,
String updatedMultisigHex, String updatedMultisigHex,
boolean sawArrivedPaymentReceivedMsg) { boolean deferPublishPayout,
PaymentSentMessage paymentSentMessage) {
super(messageVersion, tradeId, uid); super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.signedWitness = signedWitness; this.signedWitness = signedWitness;
this.unsignedPayoutTxHex = unsignedPayoutTxHex; this.unsignedPayoutTxHex = unsignedPayoutTxHex;
this.signedPayoutTxHex = signedPayoutTxHex; this.signedPayoutTxHex = signedPayoutTxHex;
this.updatedMultisigHex = updatedMultisigHex; this.updatedMultisigHex = updatedMultisigHex;
this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg; this.deferPublishPayout = deferPublishPayout;
this.paymentSentMessage = paymentSentMessage;
} }
@Override @Override
@ -97,11 +109,13 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
.setTradeId(tradeId) .setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid) .setUid(uid)
.setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg); .setDeferPublishPayout(deferPublishPayout);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex)); 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(); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
} }
@ -112,7 +126,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ? SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
SignedWitness.fromProto(protoSignedWitness) : SignedWitness.fromProto(protoSignedWitness) :
null; null;
return new PaymentReceivedMessage(proto.getTradeId(), PaymentReceivedMessage message = new PaymentReceivedMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()), NodeAddress.fromProto(proto.getSenderNodeAddress()),
signedWitness, signedWitness,
proto.getUid(), proto.getUid(),
@ -120,18 +134,23 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), 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 @Override
public String toString() { public String toString() {
return "SellerReceivedPaymentMessage{" + return "PaymentReceivedMessage{" +
"\n senderNodeAddress=" + senderNodeAddress + "\n senderNodeAddress=" + senderNodeAddress +
",\n signedWitness=" + signedWitness + ",\n signedWitness=" + signedWitness +
",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex + ",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
",\n signedPayoutTxHex=" + signedPayoutTxHex + ",\n signedPayoutTxHex=" + signedPayoutTxHex +
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + ",\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(); "\n} " + super.toString();
} }
} }

View file

@ -25,12 +25,13 @@ import bisq.common.proto.ProtoUtil;
import java.util.Optional; import java.util.Optional;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Getter
public final class PaymentSentMessage extends TradeMailboxMessage { public final class PaymentSentMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
@Nullable @Nullable
@ -41,6 +42,9 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
private final String updatedMultisigHex; private final String updatedMultisigHex;
@Nullable @Nullable
private final byte[] paymentAccountKey; private final byte[] paymentAccountKey;
@Setter
@Nullable
private byte[] buyerSignature;
// Added after v1.3.7 // 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. // 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(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); 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(); return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
} }
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto, public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
String messageVersion) { String messageVersion) {
return new PaymentSentMessage(proto.getTradeId(), PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()), NodeAddress.fromProto(proto.getSenderNodeAddress()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
@ -117,6 +122,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()) 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 payoutTxHex=" + payoutTxHex +
",\n updatedMultisigHex=" + updatedMultisigHex + ",\n updatedMultisigHex=" + updatedMultisigHex +
",\n paymentAccountKey=" + paymentAccountKey + ",\n paymentAccountKey=" + paymentAccountKey +
",\n buyerSignature=" + buyerSignature +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -113,7 +113,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() { public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class }; return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class };
} }
} }

View file

@ -19,19 +19,11 @@ package bisq.core.trade.protocol;
import bisq.core.trade.BuyerAsMakerTrade; import bisq.core.trade.BuyerAsMakerTrade;
import bisq.core.trade.Trade; 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.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.MakerSendInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.taskrunner.Task;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@ -45,10 +37,6 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
super(trade); super(trade);
} }
///////////////////////////////////////////////////////////////////////////////////////////
// MakerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@Override @Override
public void handleInitTradeRequest(InitTradeRequest message, public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer, NodeAddress peer,
@ -80,49 +68,4 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
} }
}).start(); }).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);
}
} }

View file

@ -92,60 +92,6 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}).start(); }).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 @Override
protected void handleError(String errorMessage) { protected void handleError(String errorMessage) {
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId()); trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());

View file

@ -25,7 +25,8 @@ import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; 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.SendDepositsConfirmedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
@ -58,7 +59,9 @@ public class BuyerProtocol extends DisputeProtocol {
given(anyPhase(Trade.Phase.PAYMENT_SENT) given(anyPhase(Trade.Phase.PAYMENT_SENT)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
.with(BuyerEvent.STARTUP)) .with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSendPaymentSentMessage.class)) .setup(tasks(
BuyerSendPaymentSentMessageToSeller.class,
BuyerSendPaymentSentMessageToArbitrator.class))
.executeTasks(); .executeTasks();
} }
@ -93,10 +96,9 @@ public class BuyerProtocol extends DisputeProtocol {
.with(event) .with(event)
.preCondition(trade.confirmPermitted())) .preCondition(trade.confirmPermitted()))
.setup(tasks(ApplyFilter.class, .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, BuyerPreparePaymentSentMessage.class,
//BuyerSetupPayoutTxListener.class, BuyerSendPaymentSentMessageToSeller.class,
BuyerSendPaymentSentMessage.class) // don't latch trade because this blocks and runs in background BuyerSendPaymentSentMessageToArbitrator.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
this.errorMessageHandler = null; this.errorMessageHandler = null;
@ -119,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() { public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class }; return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
} }
} }

View file

@ -20,18 +20,11 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerAsMakerTrade; import bisq.core.trade.SellerAsMakerTrade;
import bisq.core.trade.Trade; 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.InitTradeRequest;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest; import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -81,53 +74,4 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
} }
}).start(); }).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);
}
} }

View file

@ -22,18 +22,10 @@ import bisq.core.offer.Offer;
import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.SellerAsTakerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.handlers.TradeResultHandler; 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.ApplyFilter;
import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds; import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds;
import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -90,55 +82,6 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}).start(); }).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 @Override
protected void handleError(String errorMessage) { protected void handleError(String errorMessage) {
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId()); trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());

View file

@ -24,10 +24,10 @@ import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; 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.SellerSendPaymentReceivedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
@ -54,18 +54,11 @@ public class SellerProtocol extends DisputeProtocol {
@Override @Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) { protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer); super.onTradeMessage(message, peer);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peer);
}
} }
@Override @Override
public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
super.onMailboxMessage(message, peerNodeAddress); super.onMailboxMessage(message, peerNodeAddress);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
}
} }
@Override @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 // User interaction
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -160,7 +107,7 @@ public class SellerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() { public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToBuyer.class }; return new Class[] { SendDepositsConfirmedMessageToArbitrator.class, SendDepositsConfirmedMessageToBuyer.class };
} }
} }

View file

@ -23,6 +23,7 @@ import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.HavenoUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse; 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.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.RemoveOffer; 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.tasks.TradeTask;
import bisq.core.trade.protocol.FluentProtocol.Condition; 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.MaybeSendSignContractRequest;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse; import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; 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) { 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()); log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) { if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress); handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) { } else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress); 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()); log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) { if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress); handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) { } else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress); 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 @Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
@ -176,11 +145,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
} }
///////////////////////////////////////////////////////////////////////////////////////////
// DecryptedMailboxListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override @Override
public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return; if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return;
@ -240,10 +204,34 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Abstract // API
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public abstract Class<? extends TradeTask>[] getDepsitsConfirmedTasks(); public abstract Class<? extends TradeTask>[] 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) { public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()"); System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
@ -398,6 +386,53 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}).start(); }).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 // received by buyer and arbitrator
protected void handle(PaymentReceivedMessage message, NodeAddress peer) { protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)"); System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
@ -410,7 +445,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
latchTrade(); latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(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) .with(message)
.from(peer)) .from(peer))
.setup(tasks( .setup(tasks(
@ -427,6 +462,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
} }
public void onWithdrawCompleted() {
log.info("Withdraw completed");
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// FluentProtocol // FluentProtocol
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -590,15 +629,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// Validation // 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 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(); TradingPeer peer = trade.getTradingPeer(address);
else if (peer.equals(trade.getMaker().getNodeAddress())) return trade.getMaker().getPubKeyRing(); if (peer == null) {
else if (peer.equals(trade.getTaker().getNodeAddress())) return trade.getTaker().getPubKeyRing();
else {
log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + peer); 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 null;
} }
return peer.getPubKeyRing();
} }
private boolean isPubKeyValid(DecryptedMessageWithPubKey message) { private boolean isPubKeyValid(DecryptedMessageWithPubKey message) {
@ -707,7 +745,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
synchronized (trade) { synchronized (trade) {
latchTrade(); latchTrade();
expect(new Condition(trade)) expect(new Condition(trade))
.setup(tasks(getDepsitsConfirmedTasks()) .setup(tasks(getDepositsConfirmedTasks())
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages"); handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");

View file

@ -20,7 +20,9 @@ package bisq.core.trade.protocol;
import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.model.RawTransactionInput;
import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import bisq.common.proto.persistable.PersistablePayload; import bisq.common.proto.persistable.PersistablePayload;
@ -124,6 +126,8 @@ public final class TradingPeer implements PersistablePayload {
private String depositTxKey; private String depositTxKey;
@Nullable @Nullable
private String updatedMultisigHex; private String updatedMultisigHex;
@Nullable
private PaymentSentMessage paymentSentMessage;
public TradingPeer() { public TradingPeer() {
} }
@ -163,6 +167,7 @@ public final class TradingPeer implements PersistablePayload {
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
builder.setCurrentDate(currentDate); builder.setCurrentDate(currentDate);
return builder.build(); return builder.build();
@ -211,6 +216,7 @@ public final class TradingPeer implements PersistablePayload {
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
tradingPeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null);
return tradingPeer; return tradingPeer;
} }
} }

View file

@ -79,6 +79,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
else throw new RuntimeException("DepositRequest is not from maker or taker"); else throw new RuntimeException("DepositRequest is not from maker or taker");
// verify deposit tx // verify deposit tx
try {
trade.getXmrWalletService().verifyTradeTx(depositAddress, trade.getXmrWalletService().verifyTradeTx(depositAddress,
depositAmount, depositAmount,
tradeFee, tradeFee,
@ -87,6 +88,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
request.getDepositTxKey(), request.getDepositTxKey(),
null, null,
false); false);
} catch (Exception e) {
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// set deposit info // set deposit info
trader.setDepositTxHex(request.getDepositTxHex()); trader.setDepositTxHex(request.getDepositTxHex());

View file

@ -54,6 +54,7 @@ public class ArbitratorProcessReserveTx extends TradeTask {
// process reserve tx with expected terms // process reserve tx with expected terms
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee()); BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
try {
trade.getXmrWalletService().verifyTradeTx( trade.getXmrWalletService().verifyTradeTx(
request.getPayoutAddress(), request.getPayoutAddress(),
depositAmount, depositAmount,
@ -63,6 +64,9 @@ public class ArbitratorProcessReserveTx extends TradeTask {
request.getReserveTxKey(), request.getReserveTxKey(),
null, null,
true); 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 // save reserve tx to model
TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker(); TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker();

View file

@ -21,11 +21,18 @@ import bisq.core.network.MessageState;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.TradeMailboxMessage; 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.Timer;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -38,8 +45,8 @@ import lombok.extern.slf4j.Slf4j;
* online he will process it. * online he will process it.
*/ */
@Slf4j @Slf4j
public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { @EqualsAndHashCode(callSuper = true)
private PaymentSentMessage message; public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
private ChangeListener<MessageState> listener; private ChangeListener<MessageState> listener;
private Timer timer; private Timer timer;
@ -47,16 +54,34 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
super(taskHandler, trade); 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 @Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { 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 // 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 // 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 // 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. // other data stays the same when we re-send the message at any time later.
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress(); String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
message = new PaymentSentMessage(
// create payment sent message
PaymentSentMessage message = new PaymentSentMessage(
tradeId, tradeId,
processModel.getMyNodeAddress(), processModel.getMyNodeAddress(),
trade.getCounterCurrencyTxId(), trade.getCounterCurrencyTxId(),
@ -66,8 +91,18 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
trade.getSelf().getPaymentAccountKey() 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 @Override
@ -96,18 +131,6 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
} }
@Override
protected void run() {
try {
runInterceptHook();
super.run();
} catch (Throwable t) {
failed(t);
} finally {
cleanup();
}
}
private void cleanup() { private void cleanup() {
if (timer != null) { if (timer != null) {
timer.stop(); timer.stop();

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Trade> 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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Trade> 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();
}
}

View file

@ -53,7 +53,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
checkNotNull(makerRequest); checkNotNull(makerRequest);
checkTradeId(processModel.getOfferId(), 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(); Offer offer = processModel.getOffer();
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8)); byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));

View file

@ -37,17 +37,19 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
// get sender based on the pub key // get peer
// TODO: trade.getTradingPeer(PubKeyRing)
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
TradingPeer sender; TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing());
if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator(); if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
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 // update peer node address
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress()); 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);
// store updated multisig hex for processing on payment sent
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// decrypt seller payment account payload if key given // decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) { if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
@ -55,9 +57,6 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
} }
// store updated multisig hex for processing on payment sent
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// persist and complete // persist and complete
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
complete(); complete();

View file

@ -55,9 +55,6 @@ public class ProcessInitTradeRequest extends TradeTask {
checkNotNull(request); checkNotNull(request);
checkTradeId(processModel.getOfferId(), request); checkTradeId(processModel.getOfferId(), request);
System.out.println("PROCESS INIT TRADE REQUEST");
System.out.println(request);
// handle request as arbitrator // handle request as arbitrator
TradingPeer multisigParticipant; TradingPeer multisigParticipant;
if (trade instanceof ArbitratorTrade) { if (trade instanceof ArbitratorTrade) {

View file

@ -18,8 +18,8 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitness;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.util.Validator; import bisq.core.util.Validator;
@ -27,11 +27,13 @@ import common.utils.GenUtils;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import java.util.ArrayList;
import java.util.List;
@Slf4j @Slf4j
public class ProcessPaymentReceivedMessage extends TradeTask { public class ProcessPaymentReceivedMessage extends TradeTask {
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) { public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
@ -48,26 +50,34 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
checkNotNull(message); checkNotNull(message);
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided"); 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 // update to the latest peer address of our peer if the message is correct
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); 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<String> updatedMultisigHexes = new ArrayList<String>();
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 // handle if payout tx not published
if (!trade.isPayoutPublished()) { if (!trade.isPayoutPublished()) {
// import multisig hex // wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
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
boolean isSigned = message.getSignedPayoutTxHex() != null; boolean isSigned = message.getSignedPayoutTxHex() != null;
if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) { if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName()); log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
GenUtils.waitFor(30000); GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
multisigWallet.rescanSpent(); trade.syncWallet();
} }
// verify and publish payout tx // verify and publish payout tx
@ -77,11 +87,16 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
} else { } else {
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName()); log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
try {
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); 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 { } 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(); SignedWitness signedWitness = message.getSignedWitness();
@ -93,7 +108,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
} }
// complete // 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(); processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {

View file

@ -20,14 +20,15 @@ package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class SellerProcessPaymentSentMessage extends TradeTask { public class ProcessPaymentSentMessage extends TradeTask {
public SellerProcessPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) { public ProcessPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
@ -40,28 +41,26 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message); checkNotNull(message);
// store buyer info // verify signature of payment sent message
HavenoUtils.verifyPaymentSentMessage(trade, message);
// update buyer info
trade.setPayoutTxHex(message.getPayoutTxHex()); trade.setPayoutTxHex(message.getPayoutTxHex());
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getBuyer().setPaymentSentMessage(message);
// decrypt buyer's payment account payload // if seller, decrypt buyer's payment account payload
trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
// update latest peer address // update latest peer address
trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
// set state
String counterCurrencyTxId = message.getCounterCurrencyTxId(); String counterCurrencyTxId = message.getCounterCurrencyTxId();
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) { if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
trade.setCounterCurrencyTxId(counterCurrencyTxId);
}
String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) { if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
trade.setCounterCurrencyExtraData(counterCurrencyExtraData); trade.setStateIfProgress(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
}
trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {

View file

@ -107,7 +107,7 @@ public class ProcessSignContractResponse extends TradeTask {
trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST); trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
} else { } 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 complete(); // does not yet have needed signatures
} }
} catch (Throwable t) { } catch (Throwable t) {

View file

@ -63,11 +63,6 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx); trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
// export multisig hex once
if (trade.getSelf().getUpdatedMultisigHex() == null) {
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
}
} }
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();

View file

@ -21,8 +21,10 @@ import bisq.core.account.sign.SignedWitness;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMailboxMessage; import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -30,10 +32,13 @@ import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@EqualsAndHashCode(callSuper = true) import com.google.common.base.Charsets;
@Slf4j @Slf4j
@EqualsAndHashCode(callSuper = true)
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
SignedWitness signedWitness = null; SignedWitness signedWitness = null;
PaymentReceivedMessage message = null;
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) { public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
@ -47,13 +52,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
protected void run() { protected void run() {
try { try {
runInterceptHook(); runInterceptHook();
if (trade.getPayoutTxHex() == null) {
log.error("Payout tx is null");
failed("Payout tx is null");
return;
}
super.run(); super.run();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);
@ -63,6 +61,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
@Override @Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
if (message == null) {
// TODO: sign witness // TODO: sign witness
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); // AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
@ -71,15 +70,28 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); // accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
// } // }
return new PaymentReceivedMessage( // TODO: create with deterministic id like BuyerSendPaymentSentMessage
message = new PaymentReceivedMessage(
tradeId, tradeId,
processModel.getMyNodeAddress(), processModel.getMyNodeAddress(),
signedWitness, signedWitness,
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout 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 @Override

View file

@ -65,6 +65,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId); MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); 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 // 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

View file

@ -63,6 +63,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
log.info("Send {} to peer {}. tradeId={}, uid={}", log.info("Send {} to peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
TradeTask task = this;
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
peersNodeAddress, peersNodeAddress,
getReceiverPubKeyRing(), getReceiverPubKeyRing(),
@ -72,7 +73,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
public void onArrived() { public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
setStateArrived(); setStateArrived();
complete(); if (!task.isCompleted()) complete();
} }
@Override @Override
@ -95,7 +96,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
protected void onStoredInMailbox() { protected void onStoredInMailbox() {
setStateStoredInMailbox(); setStateStoredInMailbox();
complete(); if (!isCompleted()) complete();
} }
protected void onFault(String errorMessage, TradeMessage message) { protected void onFault(String errorMessage, TradeMessage message) {

View file

@ -19,11 +19,8 @@ package bisq.core.trade.protocol.tasks;
import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; 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.Trade;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.SendDirectMessageListener;
import java.util.HashSet; import java.util.HashSet;

View file

@ -1842,7 +1842,7 @@ disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3}
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\ disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
Open trade and accept or reject suggestion from mediator Open trade and accept or reject suggestion from mediator
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ 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.closePeer=You need to close also the trading peers ticket!
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
# suppress inspection "TrailingSpacesInProperty" # suppress inspection "TrailingSpacesInProperty"

View file

@ -212,13 +212,10 @@ public class AccountAgeWitnessServiceTest {
"summary", "summary",
null, null,
null, null,
null,
null,
100000, 100000,
0, 0,
null, null,
now - 1, now - 1));
false));
// Filtermanager says nothing is filtered // Filtermanager says nothing is filtered
when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isNodeAddressBanned(any())).thenReturn(false);

View file

@ -65,7 +65,7 @@ public class GrpcDisputesService extends DisputesImplBase {
}, },
(errorMessage, throwable) -> { (errorMessage, throwable) -> {
log.info("Error in openDispute" + errorMessage); log.info("Error in openDispute" + errorMessage);
exceptionHandler.handleException(log, throwable, responseObserver); exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver);
}); });
} catch (Throwable cause) { } catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver); exceptionHandler.handleException(log, cause, responseObserver);
@ -82,7 +82,7 @@ public class GrpcDisputesService extends DisputesImplBase {
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} catch (Throwable cause) { } 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.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} catch (Throwable cause) { } 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(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getOpenDisputeMethod().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));
}} }}
))); )));
} }

View file

@ -206,7 +206,7 @@ class GrpcOffersService extends OffersImplBase {
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getGetMyOffersMethod().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)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
}} }}
))); )));

View file

@ -244,9 +244,9 @@ class GrpcTradesService extends TradesImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(30, SECONDS));
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, 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(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));

View file

@ -189,9 +189,7 @@ class GrpcWalletsService extends WalletsImplBase {
.stream() .stream()
.map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount()))) .map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount())))
.collect(Collectors.toList())); .collect(Collectors.toList()));
log.info("Successfully created XMR tx: hash {}, metadata {}", log.info("Successfully created XMR tx: hash {}", tx.getHash());
tx.getHash(),
tx.getMetadata());
var reply = CreateXmrTxReply.newBuilder() var reply = CreateXmrTxReply.newBuilder()
.setTx(toXmrTx(tx).toProtoMessage()) .setTx(toXmrTx(tx).toProtoMessage())
.build(); .build();

View file

@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.MakerSetLockTime; import bisq.core.trade.protocol.tasks.MakerSetLockTime;
import bisq.core.trade.protocol.tasks.RemoveOffer; import bisq.core.trade.protocol.tasks.RemoveOffer;
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; 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.SellerPublishDepositTx;
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics; import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
@ -100,7 +100,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishDepositTx.class, SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class, SellerPublishTradeStatistics.class,
SellerProcessPaymentSentMessage.class, ProcessPaymentSentMessage.class,
ApplyFilter.class, ApplyFilter.class,
TakerVerifyMakerFeePayment.class, TakerVerifyMakerFeePayment.class,
@ -157,7 +157,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishDepositTx.class, SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class, SellerPublishTradeStatistics.class,
SellerProcessPaymentSentMessage.class, ProcessPaymentSentMessage.class,
ApplyFilter.class, ApplyFilter.class,
ApplyFilter.class, ApplyFilter.class,

View file

@ -92,10 +92,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final CoinFormatter formatter; private final CoinFormatter formatter;
private final ArbitrationManager arbitrationManager; private final ArbitrationManager arbitrationManager;
private final MediationManager mediationManager; private final MediationManager mediationManager;
private final XmrWalletService walletService; private final CoreDisputesService disputesService; private Dispute dispute;
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 ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
private DisputeResult disputeResult; private DisputeResult disputeResult;
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
@ -115,7 +112,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private ChangeListener<Toggle> reasonToggleSelectionListener; private ChangeListener<Toggle> reasonToggleSelectionListener;
private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField; private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField;
private ChangeListener<String> buyerPayoutAmountListener, sellerPayoutAmountListener; private ChangeListener<String> buyerPayoutAmountListener, sellerPayoutAmountListener;
private CheckBox isLoserPublisherCheckBox;
private ChangeListener<Toggle> tradeAmountToggleGroupListener; private ChangeListener<Toggle> tradeAmountToggleGroupListener;
@ -134,8 +130,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
this.formatter = formatter; this.formatter = formatter;
this.arbitrationManager = arbitrationManager; this.arbitrationManager = arbitrationManager;
this.mediationManager = mediationManager; this.mediationManager = mediationManager;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
this.disputesService = disputesService; this.disputesService = disputesService;
type = Type.Confirmation; type = Type.Confirmation;
@ -220,7 +214,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount());
disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount());
disputeResult.setWinner(peersDisputeResult.getWinner()); disputeResult.setWinner(peersDisputeResult.getWinner());
disputeResult.setLoserPublisher(peersDisputeResult.isLoserPublisher());
disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setReason(peersDisputeResult.getReason());
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
@ -248,13 +241,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
reasonWasPeerWasLateRadioButton.setDisable(true); reasonWasPeerWasLateRadioButton.setDisable(true);
reasonWasTradeAlreadySettledRadioButton.setDisable(true); reasonWasTradeAlreadySettledRadioButton.setDisable(true);
isLoserPublisherCheckBox.setDisable(true);
isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher());
applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get()); applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get());
applyTradeAmountRadioButtonStates(); applyTradeAmountRadioButtonStates();
} else {
isLoserPublisherCheckBox.setSelected(false);
} }
setReasonRadioButtonState(); setReasonRadioButtonState();
@ -426,11 +414,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller"));
sellerPayoutAmountInputTextField.setEditable(false); sellerPayoutAmountInputTextField.setEditable(false);
isLoserPublisherCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.payoutAmount.invert"));
VBox vBox = new VBox(); VBox vBox = new VBox();
vBox.setSpacing(15); 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.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
GridPane.setRowIndex(vBox, rowIndex); GridPane.setRowIndex(vBox, rowIndex);
GridPane.setColumnIndex(vBox, 1); GridPane.setColumnIndex(vBox, 1);
@ -590,7 +576,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second; Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> { closeTicketButton.setOnAction(e -> {
disputesService.applyDisputePayout(dispute, disputeResult, contract);
doClose(closeTicketButton); doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) { // if (dispute.getDepositTxSerialized() == null) {
@ -763,19 +748,14 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent); disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> {
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup() new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show();
.attention(Res.get("disputeSummaryWindow.close.closePeer"))
.show(),
200, TimeUnit.MILLISECONDS);
} }
disputeManager.requestPersistence(); disputeManager.requestPersistence();
});
closeTicketButton.disableProperty().unbind(); closeTicketButton.disableProperty().unbind();
hide(); hide();
} }

View file

@ -465,7 +465,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
byte[] payoutTxSerialized = null; byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null; String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.exportMultisigHex();
if (trade.getPayoutTxId() != null) { if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString(); // payoutTxHashAsString = payoutTx.getHashAsString();
@ -477,9 +476,9 @@ public class PendingTradesDataModel extends ActivatableDataModel {
// If mediation is not activated we use arbitration // If mediation is not activated we use arbitration
if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) { 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 // 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 // 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 { } else {
useMediation = false; useMediation = false;
useArbitration = true; useArbitration = true;
@ -549,27 +548,27 @@ public class PendingTradesDataModel extends ActivatableDataModel {
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} else if (useArbitration) { } else if (useArbitration) {
// Only if we have completed mediation we allow arbitration // Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager; disputeManager = arbitrationManager;
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} else { } else {
log.warn("Invalid dispute state {}", disputeState.name()); log.warn("Invalid dispute state {}", disputeState.name());
} }
} }
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) { private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, senderMultisigHex, disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex,
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> { () -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
if ((throwable instanceof DisputeAlreadyOpenException)) { if ((throwable instanceof DisputeAlreadyOpenException)) {
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
new Popup().warning(errorMessage) new Popup().warning(errorMessage)
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) .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(); .closeButtonText(Res.get("shared.cancel")).show();
} else { } else {
new Popup().warning(errorMessage).show(); new Popup().warning(errorMessage).show();

View file

@ -511,7 +511,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
if (trade instanceof ArbitratorTrade) return; if (trade instanceof ArbitratorTrade) return;
switch (payoutState) { switch (payoutState) {
case PUBLISHED: case PAYOUT_PUBLISHED:
sellerState.set(SellerState.STEP4); sellerState.set(SellerState.STEP4);
buyerState.set(BuyerState.STEP4); buyerState.set(BuyerState.STEP4);
break; break;

View file

@ -31,6 +31,7 @@ import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.support.dispute.mediation.MediationResultState;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.MakerTrade; import bisq.core.trade.MakerTrade;
import bisq.core.trade.TakerTrade; import bisq.core.trade.TakerTrade;
@ -480,31 +481,25 @@ public abstract class TradeStepView extends AnchorPane {
switch (disputeState) { switch (disputeState) {
case NO_DISPUTE: case NO_DISPUTE:
break; break;
case DISPUTE_REQUESTED: case DISPUTE_REQUESTED:
case DISPUTE_OPENED:
if (tradeStepInfo != null) { if (tradeStepInfo != null) {
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
} }
applyOnDisputeOpened(); applyOnDisputeOpened();
// update trade view unless arbitrator
if (trade instanceof ArbitratorTrade) break;
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId()); ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> { ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
});
break;
case DISPUTE_STARTED_BY_PEER:
if (tradeStepInfo != null) { if (tradeStepInfo != null) {
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); boolean isOpener = dispute.isDisputeOpenerIsBuyer() ? trade.isBuyer() : trade.isSeller();
tradeStepInfo.setState(isOpener ? TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED : TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
} }
applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
}); });
break; break;
case DISPUTE_CLOSED: case DISPUTE_CLOSED:
break; break;
case MEDIATION_REQUESTED: case MEDIATION_REQUESTED:

View file

@ -190,7 +190,7 @@ public class BuyerStep2View extends TradeStepView {
model.setMessageStateProperty(MessageState.FAILED); model.setMessageStateProperty(MessageState.FAILED);
break; break;
default: default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId()); log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId());
busyAnimation.stop(); busyAnimation.stop();
statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
break; break;
@ -608,12 +608,6 @@ public class BuyerStep2View extends TradeStepView {
busyAnimation.play(); busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation")); 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(() -> { model.dataModel.onPaymentStarted(() -> {
}, errorMessage -> { }, errorMessage -> {
busyAnimation.stop(); busyAnimation.stop();

View file

@ -145,6 +145,11 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.stop(); busyAnimation.stop();
statusLabel.setText(""); statusLabel.setText("");
break; 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: default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId()); log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
busyAnimation.stop(); busyAnimation.stop();

View file

@ -838,15 +838,19 @@ message TradeInfo {
string phase = 17; string phase = 17;
string period_state = 18; string period_state = 18;
string payout_state = 19; string payout_state = 19;
bool is_deposit_published = 20; string dispute_state = 20;
bool is_deposit_unlocked = 21; bool is_deposit_published = 21;
bool is_payment_sent = 22; bool is_deposit_confirmed = 22;
bool is_payment_received = 23; bool is_deposit_unlocked = 23;
bool is_payout_published = 24; bool is_payment_sent = 24;
bool is_completed = 25; bool is_payment_received = 25;
string contract_as_json = 26; bool is_payout_published = 26;
ContractInfo contract = 27; bool is_payout_confirmed = 27;
string trade_volume = 28; 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 maker_deposit_tx_id = 100;
string taker_deposit_tx_id = 101; string taker_deposit_tx_id = 101;

View file

@ -40,31 +40,29 @@ message NetworkEnvelope {
InputsForDepositTxResponse inputs_for_deposit_tx_response = 18; InputsForDepositTxResponse inputs_for_deposit_tx_response = 18;
DepositTxMessage deposit_tx_message = 19; DepositTxMessage deposit_tx_message = 19;
OpenNewDisputeMessage open_new_dispute_message = 20; DisputeOpenedMessage dispute_opened_message = 20;
PeerOpenedDisputeMessage peer_opened_dispute_message = 21; DisputeClosedMessage dispute_closed_message = 21;
ChatMessage chat_message = 22; 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; AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 24;
AckMessage ack_message = 27; AckMessage ack_message = 25;
BundleOfEnvelopes bundle_of_envelopes = 28; BundleOfEnvelopes bundle_of_envelopes = 26;
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 29; MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 27;
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 30; MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 28;
DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 31; DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 29;
DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 32; DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 30;
DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 33; DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 31;
PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 34; PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 32;
RefreshTradeStateRequest refresh_trade_state_request = 35 [deprecated = true]; RefreshTradeStateRequest refresh_trade_state_request = 33 [deprecated = true];
TraderSignedWitnessMessage trader_signed_witness_message = 36 [deprecated = true]; TraderSignedWitnessMessage trader_signed_witness_message = 34 [deprecated = true];
GetInventoryRequest get_inventory_request = 37; GetInventoryRequest get_inventory_request = 35;
GetInventoryResponse get_inventory_response = 38; GetInventoryResponse get_inventory_response = 36;
SignOfferRequest sign_offer_request = 1001; SignOfferRequest sign_offer_request = 1001;
SignOfferResponse sign_offer_response = 1002; SignOfferResponse sign_offer_response = 1002;
@ -77,8 +75,6 @@ message NetworkEnvelope {
DepositsConfirmedMessage deposits_confirmed_message = 1009; DepositsConfirmedMessage deposits_confirmed_message = 1009;
PaymentSentMessage payment_sent_message = 1010; PaymentSentMessage payment_sent_message = 1010;
PaymentReceivedMessage payment_received_message = 1011; 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; 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 { message PaymentSentMessage {
string trade_id = 1; string trade_id = 1;
NodeAddress sender_node_address = 2; NodeAddress sender_node_address = 2;
@ -416,6 +404,7 @@ message PaymentSentMessage {
string payout_tx_hex = 6; string payout_tx_hex = 6;
string updated_multisig_hex = 7; string updated_multisig_hex = 7;
bytes payment_account_key = 8; bytes payment_account_key = 8;
bytes buyer_signature = 9;
} }
message PaymentReceivedMessage { message PaymentReceivedMessage {
@ -426,23 +415,9 @@ message PaymentReceivedMessage {
string unsigned_payout_tx_hex = 5; string unsigned_payout_tx_hex = 5;
string signed_payout_tx_hex = 6; string signed_payout_tx_hex = 6;
string updated_multisig_hex = 7; string updated_multisig_hex = 7;
bool saw_arrived_payment_received_msg = 8; bool defer_publish_payout = 8;
} PaymentSentMessage payment_sent_message = 9;
bytes seller_signature = 10;
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;
} }
message MediatedPayoutTxPublishedMessage { message MediatedPayoutTxPublishedMessage {
@ -474,30 +449,6 @@ message TraderSignedWitnessMessage {
SignedWitness signed_witness = 4 [deprecated = true]; 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 { message ChatMessage {
int64 date = 1; int64 date = 1;
string trade_id = 2; string trade_id = 2;
@ -517,21 +468,32 @@ message ChatMessage {
bool was_displayed = 16; 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; string uid = 1;
DisputeResult dispute_result = 2; DisputeResult dispute_result = 2;
NodeAddress sender_node_address = 3; NodeAddress sender_node_address = 3;
SupportType type = 4; SupportType type = 4;
} string updated_multisig_hex = 5;
string unsigned_payout_tx_hex = 6;
message PeerPublishedDisputePayoutTxMessage { bool defer_publish_payout = 7;
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;
} }
message PrivateNotificationMessage { message PrivateNotificationMessage {
@ -944,8 +906,6 @@ message DisputeResult {
bytes arbitrator_pub_key = 13; bytes arbitrator_pub_key = 13;
int64 close_date = 14; int64 close_date = 14;
bool is_loser_publisher = 15; 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 { enum PayoutState {
UNPUBLISHED = 0; PAYOUT_UNPUBLISHED = 0;
PUBLISHED = 1; PAYOUT_PUBLISHED = 1;
CONFIRMED = 2; PAYOUT_CONFIRMED = 2;
UNLOCKED = 3; PAYOUT_UNLOCKED = 3;
} }
enum DisputeState { enum DisputeState {
PB_ERROR_DISPUTE_STATE = 0; PB_ERROR_DISPUTE_STATE = 0;
NO_DISPUTE = 1; NO_DISPUTE = 1;
DISPUTE_REQUESTED = 2; // arbitration We use the enum name for resolving enums so it cannot be renamed DISPUTE_REQUESTED = 2;
DISPUTE_STARTED_BY_PEER = 3; // arbitration We use the enum name for resolving enums so it cannot be renamed DISPUTE_OPENED = 3;
DISPUTE_CLOSED = 4; // arbitration We use the enum name for resolving enums so it cannot be renamed ARBITRATOR_SENT_DISPUTE_CLOSED_MSG = 4;
MEDIATION_REQUESTED = 5; ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG = 5;
MEDIATION_STARTED_BY_PEER = 6; ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG = 6;
MEDIATION_CLOSED = 7; ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG = 7;
REFUND_REQUESTED = 8; DISPUTE_CLOSED = 8;
REFUND_REQUEST_STARTED_BY_PEER = 9; MEDIATION_REQUESTED = 9;
REFUND_REQUEST_CLOSED = 10; MEDIATION_STARTED_BY_PEER = 10;
MEDIATION_CLOSED = 11;
REFUND_REQUESTED = 12;
REFUND_REQUEST_STARTED_BY_PEER = 13;
REFUND_REQUEST_CLOSED = 14;
} }
enum TradePeriodState { enum TradePeriodState {
@ -1782,6 +1746,7 @@ message TradingPeer {
string deposit_tx_hex = 1009; string deposit_tx_hex = 1009;
string deposit_tx_key = 1010; string deposit_tx_key = 1010;
string updated_multisig_hex = 1011; string updated_multisig_hex = 1011;
PaymentSentMessage payment_sent_message = 1012;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////