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