mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-21 14:18:50 -04:00
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
79 changed files with 1770 additions and 2480 deletions
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package bisq.common.taskrunner;
|
package bisq.common.taskrunner;
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -60,6 +58,10 @@ public abstract class Task<T extends Model> {
|
||||||
taskHandler.handleComplete();
|
taskHandler.handleComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCompleted() {
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
protected void failed(String message) {
|
protected void failed(String message) {
|
||||||
appendToErrorMessage(message);
|
appendToErrorMessage(message);
|
||||||
failed();
|
failed();
|
||||||
|
|
|
@ -23,7 +23,6 @@ import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.handlers.FaultHandler;
|
import bisq.common.handlers.FaultHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
|
@ -40,8 +39,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
|
|
||||||
import monero.wallet.MoneroWallet;
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -101,9 +98,7 @@ public class CoreDisputesService {
|
||||||
|
|
||||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler);
|
||||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
|
||||||
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
|
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,26 +136,26 @@ public class CoreDisputesService {
|
||||||
isSupportTicket,
|
isSupportTicket,
|
||||||
SupportType.ARBITRATION);
|
SupportType.ARBITRATION);
|
||||||
|
|
||||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
|
||||||
|
|
||||||
return dispute;
|
return dispute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
|
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
|
||||||
try {
|
try {
|
||||||
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
|
|
||||||
|
// get winning dispute
|
||||||
|
Dispute winningDispute;
|
||||||
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
|
var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
|
||||||
.filter(d -> tradeId.equals(d.getTradeId()))
|
.filter(d -> tradeId.equals(d.getTradeId()))
|
||||||
|
.filter(d -> trade.getTradingPeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
Dispute dispute;
|
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||||
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
|
|
||||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||||
|
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
var closeDate = new Date();
|
var closeDate = new Date();
|
||||||
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
|
var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
|
||||||
var contract = dispute.getContract();
|
|
||||||
|
|
||||||
DisputePayout payout;
|
DisputePayout payout;
|
||||||
if (customWinnerAmount > 0) {
|
if (customWinnerAmount > 0) {
|
||||||
|
@ -172,30 +167,28 @@ public class CoreDisputesService {
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
||||||
}
|
}
|
||||||
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
|
applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount);
|
||||||
|
|
||||||
// apply dispute payout
|
|
||||||
applyDisputePayout(dispute, disputeResult, contract);
|
|
||||||
|
|
||||||
// close dispute ticket
|
// close dispute ticket
|
||||||
closeDispute(arbitrationManager, dispute, disputeResult, false);
|
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
|
||||||
|
arbitrationManager.requestPersistence();
|
||||||
|
|
||||||
// close dispute ticket for peer
|
// close peer's dispute ticket
|
||||||
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
||||||
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
|
.filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId())
|
||||||
.findFirst();
|
.findFirst();
|
||||||
if (peersDisputeOptional.isPresent()) {
|
if (peersDisputeOptional.isPresent()) {
|
||||||
var peerDispute = peersDisputeOptional.get();
|
var peerDispute = peersDisputeOptional.get();
|
||||||
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
|
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
|
||||||
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
|
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
|
||||||
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
|
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
|
||||||
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
|
closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> {
|
||||||
applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
|
arbitrationManager.requestPersistence();
|
||||||
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("could not find peer dispute");
|
throw new IllegalStateException("could not find peer dispute");
|
||||||
}
|
}
|
||||||
arbitrationManager.requestPersistence();
|
});
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
|
@ -246,49 +239,13 @@ public class CoreDisputesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
|
|
||||||
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
|
|
||||||
if (!dispute.isMediationDispute()) {
|
|
||||||
try {
|
|
||||||
synchronized (tradeManager.getTrade(dispute.getTradeId())) {
|
|
||||||
System.out.println(disputeResult);
|
|
||||||
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
|
|
||||||
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
|
|
||||||
|
|
||||||
// determine if dispute is in context of publisher
|
|
||||||
boolean isOpener = dispute.isOpener();
|
|
||||||
boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER);
|
|
||||||
boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner;
|
|
||||||
|
|
||||||
// open multisig wallet
|
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
|
||||||
|
|
||||||
// if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx
|
|
||||||
if (isOpener) {
|
|
||||||
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
|
||||||
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
|
|
||||||
|
|
||||||
// if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex
|
|
||||||
if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
// send arbitrator's updated multisig hex with dispute result
|
|
||||||
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
|
||||||
}
|
|
||||||
} catch (AddressFormatException e2) {
|
|
||||||
log.error("Error at close dispute", e2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// From DisputeSummaryWindow.java
|
// From DisputeSummaryWindow.java
|
||||||
public void closeDispute(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, boolean isRefundAgent) {
|
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler) {
|
||||||
dispute.setDisputeResult(disputeResult);
|
dispute.setDisputeResult(disputeResult);
|
||||||
dispute.setIsClosed();
|
dispute.setIsClosed();
|
||||||
DisputeResult.Reason reason = disputeResult.getReason();
|
DisputeResult.Reason reason = disputeResult.getReason();
|
||||||
|
|
||||||
String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator");
|
String role = Res.get("shared.arbitrator");
|
||||||
String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
|
String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
String currencyCode = contract.getOfferPayload().getCurrencyCode();
|
String currencyCode = contract.getOfferPayload().getCurrencyCode();
|
||||||
|
@ -314,13 +271,8 @@ public class CoreDisputesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
||||||
|
|
||||||
if (isRefundAgent) {
|
|
||||||
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
|
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
|
||||||
} else {
|
disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler);
|
||||||
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
|
|
||||||
}
|
|
||||||
disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {
|
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {
|
||||||
|
|
|
@ -241,7 +241,6 @@ public final class CoreMoneroConnectionsService {
|
||||||
else {
|
else {
|
||||||
boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
|
boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
updateDaemonInfo();
|
|
||||||
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
|
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
|
||||||
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||||
} else {
|
} else {
|
||||||
|
@ -410,6 +409,7 @@ public final class CoreMoneroConnectionsService {
|
||||||
|
|
||||||
private void startPollingDaemon() {
|
private void startPollingDaemon() {
|
||||||
if (updateDaemonLooper != null) updateDaemonLooper.stop();
|
if (updateDaemonLooper != null) updateDaemonLooper.stop();
|
||||||
|
updateDaemonInfo();
|
||||||
updateDaemonLooper = new TaskLooper(() -> {
|
updateDaemonLooper = new TaskLooper(() -> {
|
||||||
updateDaemonInfo();
|
updateDaemonInfo();
|
||||||
});
|
});
|
||||||
|
|
|
@ -80,12 +80,16 @@ public class TradeInfo implements Payload {
|
||||||
private final String phase;
|
private final String phase;
|
||||||
private final String periodState;
|
private final String periodState;
|
||||||
private final String payoutState;
|
private final String payoutState;
|
||||||
|
private final String disputeState;
|
||||||
private final boolean isDepositPublished;
|
private final boolean isDepositPublished;
|
||||||
|
private final boolean isDepositConfirmed;
|
||||||
private final boolean isDepositUnlocked;
|
private final boolean isDepositUnlocked;
|
||||||
private final boolean isPaymentSent;
|
private final boolean isPaymentSent;
|
||||||
private final boolean isPaymentReceived;
|
private final boolean isPaymentReceived;
|
||||||
private final boolean isCompleted;
|
|
||||||
private final boolean isPayoutPublished;
|
private final boolean isPayoutPublished;
|
||||||
|
private final boolean isPayoutConfirmed;
|
||||||
|
private final boolean isPayoutUnlocked;
|
||||||
|
private final boolean isCompleted;
|
||||||
private final String contractAsJson;
|
private final String contractAsJson;
|
||||||
private final ContractInfo contract;
|
private final ContractInfo contract;
|
||||||
|
|
||||||
|
@ -109,11 +113,15 @@ public class TradeInfo implements Payload {
|
||||||
this.phase = builder.getPhase();
|
this.phase = builder.getPhase();
|
||||||
this.periodState = builder.getPeriodState();
|
this.periodState = builder.getPeriodState();
|
||||||
this.payoutState = builder.getPayoutState();
|
this.payoutState = builder.getPayoutState();
|
||||||
|
this.disputeState = builder.getDisputeState();
|
||||||
this.isDepositPublished = builder.isDepositPublished();
|
this.isDepositPublished = builder.isDepositPublished();
|
||||||
|
this.isDepositConfirmed = builder.isDepositConfirmed();
|
||||||
this.isDepositUnlocked = builder.isDepositUnlocked();
|
this.isDepositUnlocked = builder.isDepositUnlocked();
|
||||||
this.isPaymentSent = builder.isPaymentSent();
|
this.isPaymentSent = builder.isPaymentSent();
|
||||||
this.isPaymentReceived = builder.isPaymentReceived();
|
this.isPaymentReceived = builder.isPaymentReceived();
|
||||||
this.isPayoutPublished = builder.isPayoutPublished();
|
this.isPayoutPublished = builder.isPayoutPublished();
|
||||||
|
this.isPayoutConfirmed = builder.isPayoutConfirmed();
|
||||||
|
this.isPayoutUnlocked = builder.isPayoutUnlocked();
|
||||||
this.isCompleted = builder.isCompleted();
|
this.isCompleted = builder.isCompleted();
|
||||||
this.contractAsJson = builder.getContractAsJson();
|
this.contractAsJson = builder.getContractAsJson();
|
||||||
this.contract = builder.getContract();
|
this.contract = builder.getContract();
|
||||||
|
@ -161,11 +169,15 @@ public class TradeInfo implements Payload {
|
||||||
.withPhase(trade.getPhase().name())
|
.withPhase(trade.getPhase().name())
|
||||||
.withPeriodState(trade.getPeriodState().name())
|
.withPeriodState(trade.getPeriodState().name())
|
||||||
.withPayoutState(trade.getPayoutState().name())
|
.withPayoutState(trade.getPayoutState().name())
|
||||||
|
.withDisputeState(trade.getDisputeState().name())
|
||||||
.withIsDepositPublished(trade.isDepositPublished())
|
.withIsDepositPublished(trade.isDepositPublished())
|
||||||
|
.withIsDepositConfirmed(trade.isDepositConfirmed())
|
||||||
.withIsDepositUnlocked(trade.isDepositUnlocked())
|
.withIsDepositUnlocked(trade.isDepositUnlocked())
|
||||||
.withIsPaymentSent(trade.isPaymentSent())
|
.withIsPaymentSent(trade.isPaymentSent())
|
||||||
.withIsPaymentReceived(trade.isPaymentReceived())
|
.withIsPaymentReceived(trade.isPaymentReceived())
|
||||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||||
|
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
|
||||||
|
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
|
||||||
.withIsCompleted(trade.isCompleted())
|
.withIsCompleted(trade.isCompleted())
|
||||||
.withContractAsJson(trade.getContractAsJson())
|
.withContractAsJson(trade.getContractAsJson())
|
||||||
.withContract(contractInfo)
|
.withContract(contractInfo)
|
||||||
|
@ -199,12 +211,16 @@ public class TradeInfo implements Payload {
|
||||||
.setPhase(phase)
|
.setPhase(phase)
|
||||||
.setPeriodState(periodState)
|
.setPeriodState(periodState)
|
||||||
.setPayoutState(payoutState)
|
.setPayoutState(payoutState)
|
||||||
|
.setDisputeState(disputeState)
|
||||||
.setIsDepositPublished(isDepositPublished)
|
.setIsDepositPublished(isDepositPublished)
|
||||||
|
.setIsDepositConfirmed(isDepositConfirmed)
|
||||||
.setIsDepositUnlocked(isDepositUnlocked)
|
.setIsDepositUnlocked(isDepositUnlocked)
|
||||||
.setIsPaymentSent(isPaymentSent)
|
.setIsPaymentSent(isPaymentSent)
|
||||||
.setIsPaymentReceived(isPaymentReceived)
|
.setIsPaymentReceived(isPaymentReceived)
|
||||||
.setIsCompleted(isCompleted)
|
.setIsCompleted(isCompleted)
|
||||||
.setIsPayoutPublished(isPayoutPublished)
|
.setIsPayoutPublished(isPayoutPublished)
|
||||||
|
.setIsPayoutConfirmed(isPayoutConfirmed)
|
||||||
|
.setIsPayoutUnlocked(isPayoutUnlocked)
|
||||||
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
||||||
.setContract(contract.toProtoMessage())
|
.setContract(contract.toProtoMessage())
|
||||||
.build();
|
.build();
|
||||||
|
@ -227,16 +243,20 @@ public class TradeInfo implements Payload {
|
||||||
.withVolume(proto.getTradeVolume())
|
.withVolume(proto.getTradeVolume())
|
||||||
.withPeriodState(proto.getPeriodState())
|
.withPeriodState(proto.getPeriodState())
|
||||||
.withPayoutState(proto.getPayoutState())
|
.withPayoutState(proto.getPayoutState())
|
||||||
|
.withDisputeState(proto.getDisputeState())
|
||||||
.withState(proto.getState())
|
.withState(proto.getState())
|
||||||
.withPhase(proto.getPhase())
|
.withPhase(proto.getPhase())
|
||||||
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
||||||
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
|
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
|
||||||
.withIsDepositPublished(proto.getIsDepositPublished())
|
.withIsDepositPublished(proto.getIsDepositPublished())
|
||||||
|
.withIsDepositConfirmed(proto.getIsDepositConfirmed())
|
||||||
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
|
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
|
||||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||||
.withIsCompleted(proto.getIsCompleted())
|
.withIsCompleted(proto.getIsCompleted())
|
||||||
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
||||||
|
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
|
||||||
|
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
|
||||||
.withContractAsJson(proto.getContractAsJson())
|
.withContractAsJson(proto.getContractAsJson())
|
||||||
.withContract((ContractInfo.fromProto(proto.getContract())))
|
.withContract((ContractInfo.fromProto(proto.getContract())))
|
||||||
.build();
|
.build();
|
||||||
|
@ -262,12 +282,16 @@ public class TradeInfo implements Payload {
|
||||||
", phase='" + phase + '\'' + "\n" +
|
", phase='" + phase + '\'' + "\n" +
|
||||||
", periodState='" + periodState + '\'' + "\n" +
|
", periodState='" + periodState + '\'' + "\n" +
|
||||||
", payoutState='" + payoutState + '\'' + "\n" +
|
", payoutState='" + payoutState + '\'' + "\n" +
|
||||||
|
", disputeState='" + disputeState + '\'' + "\n" +
|
||||||
", isDepositPublished=" + isDepositPublished + "\n" +
|
", isDepositPublished=" + isDepositPublished + "\n" +
|
||||||
", isDepositConfirmed=" + isDepositUnlocked + "\n" +
|
", isDepositConfirmed=" + isDepositConfirmed + "\n" +
|
||||||
|
", isDepositUnlocked=" + isDepositUnlocked + "\n" +
|
||||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||||
", isCompleted=" + isCompleted + "\n" +
|
|
||||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||||
|
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
|
||||||
|
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
|
||||||
|
", isCompleted=" + isCompleted + "\n" +
|
||||||
", offer=" + offer + "\n" +
|
", offer=" + offer + "\n" +
|
||||||
", contractAsJson=" + contractAsJson + "\n" +
|
", contractAsJson=" + contractAsJson + "\n" +
|
||||||
", contract=" + contract + "\n" +
|
", contract=" + contract + "\n" +
|
||||||
|
|
|
@ -52,11 +52,15 @@ public final class TradeInfoV1Builder {
|
||||||
private String phase;
|
private String phase;
|
||||||
private String periodState;
|
private String periodState;
|
||||||
private String payoutState;
|
private String payoutState;
|
||||||
|
private String disputeState;
|
||||||
private boolean isDepositPublished;
|
private boolean isDepositPublished;
|
||||||
|
private boolean isDepositConfirmed;
|
||||||
private boolean isDepositUnlocked;
|
private boolean isDepositUnlocked;
|
||||||
private boolean isPaymentSent;
|
private boolean isPaymentSent;
|
||||||
private boolean isPaymentReceived;
|
private boolean isPaymentReceived;
|
||||||
private boolean isPayoutPublished;
|
private boolean isPayoutPublished;
|
||||||
|
private boolean isPayoutConfirmed;
|
||||||
|
private boolean isPayoutUnlocked;
|
||||||
private boolean isCompleted;
|
private boolean isCompleted;
|
||||||
private String contractAsJson;
|
private String contractAsJson;
|
||||||
private ContractInfo contract;
|
private ContractInfo contract;
|
||||||
|
@ -152,6 +156,11 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withDisputeState(String disputeState) {
|
||||||
|
this.disputeState = disputeState;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
|
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
|
||||||
this.arbitratorNodeAddress = arbitratorNodeAddress;
|
this.arbitratorNodeAddress = arbitratorNodeAddress;
|
||||||
return this;
|
return this;
|
||||||
|
@ -167,6 +176,11 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withIsDepositConfirmed(boolean isDepositConfirmed) {
|
||||||
|
this.isDepositConfirmed = isDepositConfirmed;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) {
|
public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) {
|
||||||
this.isDepositUnlocked = isDepositUnlocked;
|
this.isDepositUnlocked = isDepositUnlocked;
|
||||||
return this;
|
return this;
|
||||||
|
@ -187,6 +201,16 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withIsPayoutConfirmed(boolean isPayoutConfirmed) {
|
||||||
|
this.isPayoutConfirmed = isPayoutConfirmed;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withIsPayoutUnlocked(boolean isPayoutUnlocked) {
|
||||||
|
this.isPayoutUnlocked = isPayoutUnlocked;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
|
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
|
||||||
this.isCompleted = isCompleted;
|
this.isCompleted = isCompleted;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -95,17 +95,14 @@ public class Balances {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatedBalances() {
|
private void updatedBalances() {
|
||||||
// Need to delay a bit to get the balances correct
|
|
||||||
UserThread.execute(() -> { // TODO (woodser): running on user thread because JFX properties updated for legacy app
|
|
||||||
updateAvailableBalance();
|
updateAvailableBalance();
|
||||||
updatePendingBalance();
|
updatePendingBalance();
|
||||||
updateReservedOfferBalance();
|
updateReservedOfferBalance();
|
||||||
updateReservedTradeBalance();
|
updateReservedTradeBalance();
|
||||||
updateReservedBalance();
|
updateReservedBalance();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (woodser): balances being set as Coin from BigInteger.longValue(), which can lose precision. should be in centineros for consistency with the rest of the application
|
// TODO (woodser): converting to long should generally be avoided since can lose precision, but in practice these amounts are below max value
|
||||||
|
|
||||||
private void updateAvailableBalance() {
|
private void updateAvailableBalance() {
|
||||||
availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact()));
|
availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact()));
|
||||||
|
|
|
@ -68,9 +68,21 @@ public class MoneroWalletRpcManager {
|
||||||
int numAttempts = 0;
|
int numAttempts = 0;
|
||||||
while (numAttempts < NUM_ALLOWED_ATTEMPTS) {
|
while (numAttempts < NUM_ALLOWED_ATTEMPTS) {
|
||||||
int port = -1;
|
int port = -1;
|
||||||
|
ServerSocket socket = null;
|
||||||
try {
|
try {
|
||||||
numAttempts++;
|
numAttempts++;
|
||||||
port = registerPort();
|
|
||||||
|
// get port
|
||||||
|
if (startPort != null) port = registerNextPort();
|
||||||
|
else {
|
||||||
|
socket = new ServerSocket(0);
|
||||||
|
port = socket.getLocalPort();
|
||||||
|
synchronized (registeredPorts) {
|
||||||
|
registeredPorts.put(port, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start monero-wallet-rpc
|
||||||
List<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd
|
List<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd
|
||||||
cmdCopy.add(RPC_BIND_PORT_ARGUMENT);
|
cmdCopy.add(RPC_BIND_PORT_ARGUMENT);
|
||||||
cmdCopy.add("" + port);
|
cmdCopy.add("" + port);
|
||||||
|
@ -84,6 +96,8 @@ public class MoneroWalletRpcManager {
|
||||||
log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS);
|
log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (socket != null) socket.close(); // close socket if used
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here
|
throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here
|
||||||
|
@ -121,24 +135,13 @@ public class MoneroWalletRpcManager {
|
||||||
walletRpc.stopProcess();
|
walletRpc.stopProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int registerPort() throws IOException {
|
private int registerNextPort() throws IOException {
|
||||||
synchronized (registeredPorts) {
|
synchronized (registeredPorts) {
|
||||||
|
|
||||||
// register next consecutive port
|
|
||||||
if (startPort != null) {
|
|
||||||
int port = startPort;
|
int port = startPort;
|
||||||
while (registeredPorts.containsKey(port)) port++;
|
while (registeredPorts.containsKey(port)) port++;
|
||||||
registeredPorts.put(port, null);
|
registeredPorts.put(port, null);
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// register auto-assigned port
|
|
||||||
else {
|
|
||||||
int port = getLocalPort();
|
|
||||||
registeredPorts.put(port, null);
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterPort(int port) {
|
private void unregisterPort(int port) {
|
||||||
|
@ -146,11 +149,4 @@ public class MoneroWalletRpcManager {
|
||||||
registeredPorts.remove(port);
|
registeredPorts.remove(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getLocalPort() throws IOException {
|
|
||||||
ServerSocket socket = new ServerSocket(0); // use socket to get available port
|
|
||||||
int port = socket.getLocalPort();
|
|
||||||
socket.close();
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ public class XmrWalletService {
|
||||||
private static final String MONERO_WALLET_NAME = "haveno_XMR";
|
private static final String MONERO_WALLET_NAME = "haveno_XMR";
|
||||||
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_";
|
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_";
|
||||||
private static final int MINER_FEE_PADDING_MULTIPLIER = 2; // extra padding for miner fees = estimated fee * multiplier
|
private static final int MINER_FEE_PADDING_MULTIPLIER = 2; // extra padding for miner fees = estimated fee * multiplier
|
||||||
private static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of expected fee
|
private static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee
|
||||||
|
|
||||||
private final CoreAccountService accountService;
|
private final CoreAccountService accountService;
|
||||||
private final CoreMoneroConnectionsService connectionsService;
|
private final CoreMoneroConnectionsService connectionsService;
|
||||||
|
@ -103,6 +103,7 @@ public class XmrWalletService {
|
||||||
private Map<String, MoneroWallet> multisigWallets;
|
private Map<String, MoneroWallet> multisigWallets;
|
||||||
private Map<String, Object> walletLocks = new HashMap<String, Object>();
|
private Map<String, Object> walletLocks = new HashMap<String, Object>();
|
||||||
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
||||||
|
private boolean isShutDown = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
XmrWalletService(CoreAccountService accountService,
|
XmrWalletService(CoreAccountService accountService,
|
||||||
|
@ -199,9 +200,9 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneroWallet createMultisigWallet(String tradeId) {
|
public MoneroWallet createMultisigWallet(String tradeId) {
|
||||||
|
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
initWalletLock(tradeId);
|
initWalletLock(tradeId);
|
||||||
synchronized (walletLocks.get(tradeId)) {
|
synchronized (walletLocks.get(tradeId)) {
|
||||||
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
|
||||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||||
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||||
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port
|
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port
|
||||||
|
@ -212,6 +213,7 @@ public class XmrWalletService {
|
||||||
|
|
||||||
// TODO (woodser): provide progress notifications during open?
|
// TODO (woodser): provide progress notifications during open?
|
||||||
public MoneroWallet getMultisigWallet(String tradeId) {
|
public MoneroWallet getMultisigWallet(String tradeId) {
|
||||||
|
if (isShutDown) throw new RuntimeException(getClass().getName() + " is shut down");
|
||||||
initWalletLock(tradeId);
|
initWalletLock(tradeId);
|
||||||
synchronized (walletLocks.get(tradeId)) {
|
synchronized (walletLocks.get(tradeId)) {
|
||||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||||
|
@ -229,9 +231,9 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeMultisigWallet(String tradeId) {
|
public void closeMultisigWallet(String tradeId) {
|
||||||
|
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
initWalletLock(tradeId);
|
initWalletLock(tradeId);
|
||||||
synchronized (walletLocks.get(tradeId)) {
|
synchronized (walletLocks.get(tradeId)) {
|
||||||
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
|
||||||
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
|
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
|
||||||
MoneroWallet wallet = multisigWallets.remove(tradeId);
|
MoneroWallet wallet = multisigWallets.remove(tradeId);
|
||||||
closeWallet(wallet, true);
|
closeWallet(wallet, true);
|
||||||
|
@ -239,9 +241,9 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean deleteMultisigWallet(String tradeId) {
|
public boolean deleteMultisigWallet(String tradeId) {
|
||||||
|
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
initWalletLock(tradeId);
|
initWalletLock(tradeId);
|
||||||
synchronized (walletLocks.get(tradeId)) {
|
synchronized (walletLocks.get(tradeId)) {
|
||||||
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
|
||||||
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||||
if (!walletExists(walletName)) return false;
|
if (!walletExists(walletName)) return false;
|
||||||
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
|
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
|
||||||
|
@ -253,7 +255,7 @@ public class XmrWalletService {
|
||||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||||
try {
|
try {
|
||||||
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
||||||
printTxs("XmrWalletService.createTx", tx);
|
//printTxs("XmrWalletService.createTx", tx);
|
||||||
return tx;
|
return tx;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -268,7 +270,7 @@ public class XmrWalletService {
|
||||||
* @param tradeFee - trade fee
|
* @param tradeFee - trade fee
|
||||||
* @param depositAmount - amount needed for the trade minus the trade fee
|
* @param depositAmount - amount needed for the trade minus the trade fee
|
||||||
* @param returnAddress - return address for deposit amount
|
* @param returnAddress - return address for deposit amount
|
||||||
* @param addPadding - reserve extra padding for miner fee fluctuations
|
* @param addPadding - reserve additional padding to cover future mining fee
|
||||||
* @return a transaction to reserve a trade
|
* @return a transaction to reserve a trade
|
||||||
*/
|
*/
|
||||||
public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount, boolean addPadding) {
|
public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount, boolean addPadding) {
|
||||||
|
@ -278,14 +280,26 @@ public class XmrWalletService {
|
||||||
// add miner fee padding to deposit amount
|
// add miner fee padding to deposit amount
|
||||||
if (addPadding) {
|
if (addPadding) {
|
||||||
|
|
||||||
// get expected mining fee
|
// get estimated mining fee with deposit amount
|
||||||
MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig()
|
MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
||||||
.addDestination(returnAddress, depositAmount));
|
.addDestination(returnAddress, depositAmount));
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
|
|
||||||
// add extra padding to deposit amount
|
BigInteger daemonFeeEstimate = getFeeEstimate(feeEstimateTx.getWeight());
|
||||||
|
log.info("createReserveTx() 1st feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate)));
|
||||||
|
|
||||||
|
// get estimated mining fee with deposit amount + previous estimated mining fee for better accuracy
|
||||||
|
feeEstimateTx = wallet.createTx(new MoneroTxConfig()
|
||||||
|
.setAccountIndex(0)
|
||||||
|
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
||||||
|
.addDestination(returnAddress, depositAmount.add(feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)))));
|
||||||
|
feeEstimate = feeEstimateTx.getFee();
|
||||||
|
|
||||||
|
log.info("createReserveTx() 2nd feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate)));
|
||||||
|
|
||||||
|
// add padding to deposit amount
|
||||||
BigInteger minerFeePadding = feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER));
|
BigInteger minerFeePadding = feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER));
|
||||||
depositAmount = depositAmount.add(minerFeePadding);
|
depositAmount = depositAmount.add(minerFeePadding);
|
||||||
}
|
}
|
||||||
|
@ -295,11 +309,11 @@ public class XmrWalletService {
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
||||||
.addDestination(returnAddress, depositAmount));
|
.addDestination(returnAddress, depositAmount));
|
||||||
|
log.info("Reserve tx weight={}, fee={}, depositAmount={}", reserveTx.getWeight(), reserveTx.getFee(), depositAmount);
|
||||||
|
|
||||||
// freeze inputs
|
// freeze inputs
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
|
for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
|
||||||
wallet.save();
|
wallet.save();
|
||||||
|
|
||||||
return reserveTx;
|
return reserveTx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,12 +357,13 @@ public class XmrWalletService {
|
||||||
* @param txHex is the transaction hex
|
* @param txHex is the transaction hex
|
||||||
* @param txKey is the transaction key
|
* @param txKey is the transaction key
|
||||||
* @param keyImages are expected key images of inputs, ignored if null
|
* @param keyImages are expected key images of inputs, ignored if null
|
||||||
* @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase
|
* @param addPadding verifies depositAmount has additional padding to cover future mining fee
|
||||||
*/
|
*/
|
||||||
public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean miningFeePadding) {
|
public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean addPadding) {
|
||||||
MoneroDaemonRpc daemon = getDaemon();
|
MoneroDaemonRpc daemon = getDaemon();
|
||||||
MoneroWallet wallet = getWallet();
|
MoneroWallet wallet = getWallet();
|
||||||
try {
|
try {
|
||||||
|
log.info("Verifying trade tx with deposit amount={}", depositAmount);
|
||||||
|
|
||||||
// verify tx not submitted to pool
|
// verify tx not submitted to pool
|
||||||
MoneroTx tx = daemon.getTx(txHash);
|
MoneroTx tx = daemon.getTx(txHash);
|
||||||
|
@ -379,12 +394,18 @@ public class XmrWalletService {
|
||||||
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
|
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
|
||||||
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue();
|
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue();
|
||||||
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Mining fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
|
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Mining fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
|
||||||
|
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
|
||||||
|
|
||||||
// verify deposit amount
|
// verify deposit amount
|
||||||
check = wallet.checkTxKey(txHash, txKey, depositAddress);
|
check = wallet.checkTxKey(txHash, txKey, depositAddress);
|
||||||
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
|
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
|
||||||
if (miningFeePadding) depositAmount = depositAmount.add(feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER))); // prove reserve of at least deposit amount + miner fee padding
|
if (addPadding) {
|
||||||
if (check.getReceivedAmount().compareTo(depositAmount) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount());
|
BigInteger minPadding = BigInteger.valueOf((long) (tx.getFee().multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)).doubleValue() * (1.0 - MINER_FEE_TOLERANCE)));
|
||||||
|
BigInteger actualPadding = check.getReceivedAmount().subtract(depositAmount);
|
||||||
|
if (actualPadding.compareTo(minPadding) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount.add(minPadding) + " (with padding) but was " + check.getReceivedAmount());
|
||||||
|
} else if (check.getReceivedAmount().compareTo(depositAmount) < 0) {
|
||||||
|
throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount());
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
daemon.flushTxPool(txHash); // flush tx from pool
|
daemon.flushTxPool(txHash); // flush tx from pool
|
||||||
|
@ -405,11 +426,12 @@ public class XmrWalletService {
|
||||||
|
|
||||||
// get fee estimates per kB from daemon
|
// get fee estimates per kB from daemon
|
||||||
MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate();
|
MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate();
|
||||||
BigInteger baseFeeRate = feeEstimates.getFee(); // get normal fee per kB
|
BigInteger baseFeeEstimate = feeEstimates.getFee(); // get normal fee per kB
|
||||||
BigInteger qmask = feeEstimates.getQuantizationMask();
|
BigInteger qmask = feeEstimates.getQuantizationMask();
|
||||||
|
log.info("Monero base fee estimate={}, qmask={}: " + baseFeeEstimate, qmask);
|
||||||
|
|
||||||
// get tx base fee
|
// get tx base fee
|
||||||
BigInteger baseFee = baseFeeRate.multiply(BigInteger.valueOf(txWeight));
|
BigInteger baseFee = baseFeeEstimate.multiply(BigInteger.valueOf(txWeight));
|
||||||
|
|
||||||
// round up to multiple of quantization mask
|
// round up to multiple of quantization mask
|
||||||
BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask);
|
BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask);
|
||||||
|
@ -468,6 +490,7 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
|
this.isShutDown = true;
|
||||||
closeAllWallets();
|
closeAllWallets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,7 +596,7 @@ public class XmrWalletService {
|
||||||
|
|
||||||
// start syncing wallet in background
|
// start syncing wallet in background
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
log.info("Syncing wallet " + config.getPath() + " in background");
|
log.info("Starting background syncing for wallet " + config.getPath());
|
||||||
walletRpc.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
walletRpc.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
||||||
log.info("Done starting background sync for wallet " + config.getPath());
|
log.info("Done starting background sync for wallet " + config.getPath());
|
||||||
}).start();
|
}).start();
|
||||||
|
@ -645,11 +668,10 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeWalletPasswords(String oldPassword, String newPassword) {
|
private void changeWalletPasswords(String oldPassword, String newPassword) {
|
||||||
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
|
|
||||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size()));
|
// create task to change main wallet password
|
||||||
pool.submit(new Runnable() {
|
List<Runnable> tasks = new ArrayList<Runnable>();
|
||||||
@Override
|
tasks.add(() -> {
|
||||||
public void run() {
|
|
||||||
try {
|
try {
|
||||||
wallet.changePassword(oldPassword, newPassword);
|
wallet.changePassword(oldPassword, newPassword);
|
||||||
saveWallet(wallet);
|
saveWallet(wallet);
|
||||||
|
@ -657,35 +679,30 @@ public class XmrWalletService {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// create tasks to change multisig wallet passwords
|
||||||
|
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
|
||||||
for (String tradeId : tradeIds) {
|
for (String tradeId : tradeIds) {
|
||||||
pool.submit(new Runnable() {
|
tasks.add(() -> {
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
|
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
|
||||||
if (multisigWallet == null) return;
|
if (multisigWallet == null) return;
|
||||||
multisigWallet.changePassword(oldPassword, newPassword);
|
multisigWallet.changePassword(oldPassword, newPassword);
|
||||||
saveWallet(multisigWallet);
|
saveWallet(multisigWallet);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pool.shutdown();
|
|
||||||
try {
|
// excute tasks in parallel
|
||||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.size()));
|
||||||
} catch (InterruptedException e) {
|
|
||||||
try { pool.shutdownNow(); }
|
|
||||||
catch (Exception e2) { }
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeWallet(MoneroWallet walletRpc, boolean save) {
|
private void closeWallet(MoneroWallet walletRpc, boolean save) {
|
||||||
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
|
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
|
||||||
MoneroError err = null;
|
MoneroError err = null;
|
||||||
try {
|
try {
|
||||||
if (save) saveWallet(walletRpc);
|
String path = walletRpc.getPath();
|
||||||
walletRpc.close();
|
walletRpc.close(save);
|
||||||
|
if (save) backupWallet(path);
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
@ -721,7 +738,7 @@ public class XmrWalletService {
|
||||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
HavenoUtils.awaitTasks(tasks);
|
HavenoUtils.executeTasks(tasks);
|
||||||
|
|
||||||
// clear wallets
|
// clear wallets
|
||||||
wallet = null;
|
wallet = null;
|
||||||
|
|
|
@ -43,6 +43,7 @@ public class TradeEvents {
|
||||||
private final PubKeyRingProvider pubKeyRingProvider;
|
private final PubKeyRingProvider pubKeyRingProvider;
|
||||||
private final TradeManager tradeManager;
|
private final TradeManager tradeManager;
|
||||||
private final MobileNotificationService mobileNotificationService;
|
private final MobileNotificationService mobileNotificationService;
|
||||||
|
private boolean isInitialized = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) {
|
public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) {
|
||||||
|
@ -59,10 +60,11 @@ public class TradeEvents {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tradeManager.getObservableList().forEach(this::setTradePhaseListener);
|
tradeManager.getObservableList().forEach(this::setTradePhaseListener);
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTradePhaseListener(Trade trade) {
|
private void setTradePhaseListener(Trade trade) {
|
||||||
log.info("We got a new trade. id={}", trade.getId());
|
if (isInitialized) log.info("We got a new trade. id={}", trade.getId());
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
|
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
String msg = null;
|
String msg = null;
|
||||||
|
|
|
@ -117,14 +117,14 @@ public class CreateOfferService {
|
||||||
double buyerSecurityDepositAsDouble,
|
double buyerSecurityDepositAsDouble,
|
||||||
PaymentAccount paymentAccount) {
|
PaymentAccount paymentAccount) {
|
||||||
|
|
||||||
log.info("create and get offer with offerId={}, \n" +
|
log.info("create and get offer with offerId={}, " +
|
||||||
"currencyCode={}, \n" +
|
"currencyCode={}, " +
|
||||||
"direction={}, \n" +
|
"direction={}, " +
|
||||||
"price={}, \n" +
|
"price={}, " +
|
||||||
"useMarketBasedPrice={}, \n" +
|
"useMarketBasedPrice={}, " +
|
||||||
"marketPriceMargin={}, \n" +
|
"marketPriceMargin={}, " +
|
||||||
"amount={}, \n" +
|
"amount={}, " +
|
||||||
"minAmount={}, \n" +
|
"minAmount={}, " +
|
||||||
"buyerSecurityDeposit={}",
|
"buyerSecurityDeposit={}",
|
||||||
offerId,
|
offerId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
|
|
@ -675,6 +675,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
|
|
||||||
// handle unscheduled offer
|
// handle unscheduled offer
|
||||||
if (openOffer.getScheduledTxHashes() == null) {
|
if (openOffer.getScheduledTxHashes() == null) {
|
||||||
|
log.info("Scheduling offer " + openOffer.getId());
|
||||||
|
|
||||||
// check for sufficient balance - scheduled offers amount
|
// check for sufficient balance - scheduled offers amount
|
||||||
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount()).compareTo(offerReserveAmount) < 0) {
|
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount()).compareTo(offerReserveAmount) < 0) {
|
||||||
|
@ -743,6 +744,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
Coin offerReserveAmount, // TODO: switch to BigInteger
|
Coin offerReserveAmount, // TODO: switch to BigInteger
|
||||||
boolean useSavingsWallet, // TODO: remove this
|
boolean useSavingsWallet, // TODO: remove this
|
||||||
TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
|
log.info("Signing and posting offer " + openOffer.getId());
|
||||||
|
|
||||||
// create model
|
// create model
|
||||||
PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(),
|
PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(),
|
||||||
|
|
|
@ -99,7 +99,6 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
.setTakersTradePrice(takersTradePrice)
|
.setTakersTradePrice(takersTradePrice)
|
||||||
.setIsTakerApiUser(isTakerApiUser)
|
.setIsTakerApiUser(isTakerApiUser)
|
||||||
.setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest());
|
.setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest());
|
||||||
|
|
||||||
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
||||||
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,6 @@ public class PlaceOfferProtocol {
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void placeOffer() {
|
public void placeOffer() {
|
||||||
log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId());
|
|
||||||
|
|
||||||
timeoutTimer = UserThread.runAfter(() -> {
|
timeoutTimer = UserThread.runAfter(() -> {
|
||||||
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
||||||
|
@ -96,10 +95,12 @@ public class PlaceOfferProtocol {
|
||||||
|
|
||||||
// ignore if timer already stopped
|
// ignore if timer already stopped
|
||||||
if (timeoutTimer == null) {
|
if (timeoutTimer == null) {
|
||||||
log.warn("Ignoring sign offer response from arbitrator because timeout has expired");
|
log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOffer().getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset timer
|
||||||
|
stopTimeoutTimer();
|
||||||
timeoutTimer = UserThread.runAfter(() -> {
|
timeoutTimer = UserThread.runAfter(() -> {
|
||||||
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
||||||
}, TradeProtocol.TRADE_TIMEOUT);
|
}, TradeProtocol.TRADE_TIMEOUT);
|
||||||
|
|
|
@ -23,12 +23,15 @@ import bisq.core.btc.model.XmrAddressEntry;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.placeoffer.PlaceOfferModel;
|
import bisq.core.offer.placeoffer.PlaceOfferModel;
|
||||||
import bisq.core.util.ParsingUtils;
|
import bisq.core.util.ParsingUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import monero.daemon.model.MoneroOutput;
|
import monero.daemon.model.MoneroOutput;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
|
|
||||||
public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) {
|
public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) {
|
||||||
|
@ -47,6 +50,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||||
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
|
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
|
||||||
|
log.info("Maker creating reserve tx with maker fee={} and depositAmount={}", makerFee, depositAmount);
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount, true);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount, true);
|
||||||
|
|
||||||
// collect reserved key images // TODO (woodser): switch to proof of reserve?
|
// collect reserved key images // TODO (woodser): switch to proof of reserve?
|
||||||
|
|
|
@ -79,8 +79,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
sendSignOfferRequests(request, () -> {
|
sendSignOfferRequests(request, () -> {
|
||||||
complete();
|
complete();
|
||||||
}, (errorMessage) -> {
|
}, (errorMessage) -> {
|
||||||
log.warn("Error signing offer: " + errorMessage);
|
appendToErrorMessage("Error signing offer " + request.getOfferId() + ": " + errorMessage);
|
||||||
appendToErrorMessage("Error signing offer: " + errorMessage);
|
|
||||||
failed(errorMessage);
|
failed(errorMessage);
|
||||||
});
|
});
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
@ -94,7 +93,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager());
|
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager());
|
||||||
if (leastUsedArbitrator == null) {
|
if (leastUsedArbitrator == null) {
|
||||||
errorMessageHandler.handleErrorMessage("Could not get least used arbitrator");
|
errorMessageHandler.handleErrorMessage("Could not get least used arbitrator to send " + request.getClass().getSimpleName() + " for offer " + request.getOfferId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler);
|
sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler);
|
||||||
|
@ -102,7 +101,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
|
|
||||||
private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
|
|
||||||
// complete on successful ack message
|
// complete on successful ack message, fail on first nack
|
||||||
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
|
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
|
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
|
||||||
|
@ -117,8 +116,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
|
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Arbitrator nacked request: {}", errorMessage);
|
errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
|
||||||
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -135,7 +133,14 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
log.warn("Arbitrator unavailable: {}", errorMessage);
|
log.warn("Arbitrator unavailable: {}", errorMessage);
|
||||||
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
|
excludedArbitrators.add(arbitratorNodeAddress);
|
||||||
|
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators);
|
||||||
|
if (altArbitrator == null) {
|
||||||
|
errorMessageHandler.handleErrorMessage("Offer " + request.getOfferId() + " could not be signed by any arbitrator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
|
||||||
|
sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,15 +161,4 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
listener
|
listener
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleArbitratorFailure(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
excludedArbitrators.add(arbitratorNodeAddress);
|
|
||||||
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators);
|
|
||||||
if (altArbitrator == null) {
|
|
||||||
errorMessageHandler.handleErrorMessage("Offer could not be signed by any arbitrator");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
|
|
||||||
sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ public class PaymentAccountUtil {
|
||||||
|
|
||||||
public static boolean isAnyPaymentAccountValidForOffer(Offer offer,
|
public static boolean isAnyPaymentAccountValidForOffer(Offer offer,
|
||||||
Collection<PaymentAccount> paymentAccounts) {
|
Collection<PaymentAccount> paymentAccounts) {
|
||||||
for (PaymentAccount paymentAccount : paymentAccounts) {
|
for (PaymentAccount paymentAccount : new ArrayList<PaymentAccount>(paymentAccounts)) {
|
||||||
if (isPaymentAccountValidForOffer(offer, paymentAccount))
|
if (isPaymentAccountValidForOffer(offer, paymentAccount))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package bisq.core.presentation;
|
package bisq.core.presentation;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.core.btc.Balances;
|
import bisq.core.btc.Balances;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -43,13 +44,13 @@ public class BalancePresentation {
|
||||||
@Inject
|
@Inject
|
||||||
public BalancePresentation(Balances balances) {
|
public BalancePresentation(Balances balances) {
|
||||||
balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> {
|
balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> {
|
||||||
availableBalance.set(longToXmr(newValue.value));
|
UserThread.execute(() -> availableBalance.set(longToXmr(newValue.value)));
|
||||||
});
|
});
|
||||||
balances.getPendingBalance().addListener((observable, oldValue, newValue) -> {
|
balances.getPendingBalance().addListener((observable, oldValue, newValue) -> {
|
||||||
pendingBalance.set(longToXmr(newValue.value));
|
UserThread.execute(() -> pendingBalance.set(longToXmr(newValue.value)));
|
||||||
});
|
});
|
||||||
balances.getReservedBalance().addListener((observable, oldValue, newValue) -> {
|
balances.getReservedBalance().addListener((observable, oldValue, newValue) -> {
|
||||||
reservedBalance.set(longToXmr(newValue.value));
|
UserThread.execute(() -> reservedBalance.set(longToXmr(newValue.value)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,9 @@ import bisq.core.offer.messages.SignOfferRequest;
|
||||||
import bisq.core.offer.messages.SignOfferResponse;
|
import bisq.core.offer.messages.SignOfferResponse;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||||
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
|
||||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||||
import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest;
|
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||||
import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse;
|
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
|
||||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
|
||||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
|
||||||
import bisq.core.support.dispute.refund.refundagent.RefundAgent;
|
import bisq.core.support.dispute.refund.refundagent.RefundAgent;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
|
@ -170,20 +166,12 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
||||||
case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE:
|
case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE:
|
||||||
return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion);
|
return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion);
|
||||||
|
|
||||||
case OPEN_NEW_DISPUTE_MESSAGE:
|
case DISPUTE_OPENED_MESSAGE:
|
||||||
return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion);
|
return DisputeOpenedMessage.fromProto(proto.getDisputeOpenedMessage(), this, messageVersion);
|
||||||
case PEER_OPENED_DISPUTE_MESSAGE:
|
case DISPUTE_CLOSED_MESSAGE:
|
||||||
return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion);
|
return DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), messageVersion);
|
||||||
case CHAT_MESSAGE:
|
case CHAT_MESSAGE:
|
||||||
return ChatMessage.fromProto(proto.getChatMessage(), messageVersion);
|
return ChatMessage.fromProto(proto.getChatMessage(), messageVersion);
|
||||||
case DISPUTE_RESULT_MESSAGE:
|
|
||||||
return DisputeResultMessage.fromProto(proto.getDisputeResultMessage(), messageVersion);
|
|
||||||
case PEER_PUBLISHED_DISPUTE_PAYOUT_TX_MESSAGE:
|
|
||||||
return PeerPublishedDisputePayoutTxMessage.fromProto(proto.getPeerPublishedDisputePayoutTxMessage(), messageVersion);
|
|
||||||
case ARBITRATOR_PAYOUT_TX_REQUEST:
|
|
||||||
return ArbitratorPayoutTxRequest.fromProto(proto.getArbitratorPayoutTxRequest(), this, messageVersion);
|
|
||||||
case ARBITRATOR_PAYOUT_TX_RESPONSE:
|
|
||||||
return ArbitratorPayoutTxResponse.fromProto(proto.getArbitratorPayoutTxResponse(), this, messageVersion);
|
|
||||||
|
|
||||||
case PRIVATE_NOTIFICATION_MESSAGE:
|
case PRIVATE_NOTIFICATION_MESSAGE:
|
||||||
return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion);
|
return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion);
|
||||||
|
|
|
@ -144,7 +144,7 @@ public abstract class SupportManager {
|
||||||
// Message handler
|
// Message handler
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected void onChatMessage(ChatMessage chatMessage) {
|
protected void handleChatMessage(ChatMessage chatMessage) {
|
||||||
final String tradeId = chatMessage.getTradeId();
|
final String tradeId = chatMessage.getTradeId();
|
||||||
final String uid = chatMessage.getUid();
|
final String uid = chatMessage.getUid();
|
||||||
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
||||||
|
@ -152,7 +152,7 @@ public abstract class SupportManager {
|
||||||
if (!channelOpen) {
|
if (!channelOpen) {
|
||||||
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
Timer timer = UserThread.runAfter(() -> onChatMessage(chatMessage), 1);
|
Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1);
|
||||||
delayMsgMap.put(uid, timer);
|
delayMsgMap.put(uid, timer);
|
||||||
} else {
|
} else {
|
||||||
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
|
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
|
||||||
|
|
|
@ -31,15 +31,19 @@ import bisq.core.offer.OpenOfferManager;
|
||||||
import bisq.core.provider.price.MarketPrice;
|
import bisq.core.provider.price.MarketPrice;
|
||||||
import bisq.core.provider.price.PriceFeedService;
|
import bisq.core.provider.price.PriceFeedService;
|
||||||
import bisq.core.support.SupportManager;
|
import bisq.core.support.SupportManager;
|
||||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
import bisq.core.support.dispute.DisputeResult.Winner;
|
||||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
|
import bisq.core.trade.ArbitratorTrade;
|
||||||
import bisq.core.trade.ClosedTradableManager;
|
import bisq.core.trade.ClosedTradableManager;
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
|
import bisq.core.trade.HavenoUtils;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.TradeDataValidation;
|
import bisq.core.trade.TradeDataValidation;
|
||||||
import bisq.core.trade.TradeManager;
|
import bisq.core.trade.TradeManager;
|
||||||
|
import bisq.core.trade.protocol.TradingPeer;
|
||||||
|
import bisq.core.util.ParsingUtils;
|
||||||
import bisq.network.p2p.BootstrapListener;
|
import bisq.network.p2p.BootstrapListener;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
|
@ -63,8 +67,9 @@ import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -75,6 +80,10 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroError;
|
||||||
|
import monero.wallet.MoneroWallet;
|
||||||
|
import monero.wallet.model.MoneroTxConfig;
|
||||||
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
@ -82,8 +91,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import monero.wallet.MoneroWallet;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
|
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
|
||||||
protected final TradeWalletService tradeWalletService;
|
protected final TradeWalletService tradeWalletService;
|
||||||
|
@ -197,12 +204,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// We get that message at both peers. The dispute object is in context of the trader
|
// We get that message at both peers. The dispute object is in context of the trader
|
||||||
public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage);
|
public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage);
|
||||||
|
|
||||||
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
|
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
|
||||||
|
|
||||||
protected abstract Trade.DisputeState getDisputeStateStartedByPeer();
|
|
||||||
|
|
||||||
public abstract void cleanupDisputes();
|
public abstract void cleanupDisputes();
|
||||||
|
|
||||||
protected abstract String getDisputeInfo(Dispute dispute);
|
protected abstract String getDisputeInfo(Dispute dispute);
|
||||||
|
@ -299,157 +304,26 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Message handler
|
// Dispute handling
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// arbitrator receives that from trader who opens dispute
|
// trader sends message to arbitrator to open dispute
|
||||||
protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) {
|
public void sendDisputeOpenedMessage(Dispute dispute,
|
||||||
T disputeList = getDisputeList();
|
boolean reOpen,
|
||||||
if (disputeList == null) {
|
String updatedMultisigHex,
|
||||||
log.warn("disputes is null");
|
ResultHandler resultHandler,
|
||||||
return;
|
FaultHandler faultHandler) {
|
||||||
}
|
|
||||||
|
|
||||||
Dispute dispute = openNewDisputeMessage.getDispute();
|
|
||||||
log.info("{}.onOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
|
||||||
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
|
|
||||||
dispute.setSupportType(openNewDisputeMessage.getSupportType());
|
|
||||||
// disputes from clients < 1.6.0 have state not set as the field didn't exist before
|
|
||||||
dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release
|
|
||||||
|
|
||||||
|
// get trade
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (trade) {
|
log.info("Sending {} for {} {}, dispute {}",
|
||||||
|
DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(),
|
||||||
String errorMessage = null;
|
dispute.getTradeId(), dispute.getId());
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
addPriceInfoMessage(dispute, 0);
|
|
||||||
|
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
|
||||||
if (isAgent(dispute)) {
|
|
||||||
|
|
||||||
// update arbitrator's multisig wallet
|
|
||||||
trade.syncWallet();
|
|
||||||
trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
|
|
||||||
trade.saveWallet();
|
|
||||||
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
|
|
||||||
synchronized (disputeList) {
|
|
||||||
if (!disputeList.contains(dispute)) {
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing);
|
|
||||||
} else {
|
|
||||||
// valid case if both have opened a dispute and agent was not online.
|
|
||||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
|
||||||
dispute.getTradeId());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
|
|
||||||
log.warn(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
|
|
||||||
log.error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the ChatMessage not the openNewDisputeMessage for the ACK
|
|
||||||
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
|
|
||||||
if (!messages.isEmpty()) {
|
|
||||||
ChatMessage msg = messages.get(0);
|
|
||||||
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
|
||||||
sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMediationResultMessage(dispute);
|
|
||||||
|
|
||||||
try {
|
|
||||||
TradeDataValidation.validatePaymentAccountPayload(dispute);
|
|
||||||
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
|
||||||
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
|
||||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
|
|
||||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
|
|
||||||
} catch (TradeDataValidation.AddressException |
|
|
||||||
TradeDataValidation.NodeAddressException |
|
|
||||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
|
||||||
log.error(e.toString());
|
|
||||||
validationExceptions.add(e);
|
|
||||||
}
|
|
||||||
requestPersistence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not-dispute-requester receives that msg from dispute agent
|
|
||||||
protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
|
|
||||||
T disputeList = getDisputeList();
|
|
||||||
if (disputeList == null) {
|
|
||||||
log.warn("disputes is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String errorMessage = null;
|
|
||||||
Dispute dispute = peerOpenedDisputeMessage.getDispute();
|
|
||||||
log.info("{}.onPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
|
||||||
|
|
||||||
Optional<Trade> optionalTrade = tradeManager.getOpenTrade(dispute.getTradeId());
|
|
||||||
if (!optionalTrade.isPresent()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Trade trade = optionalTrade.get();
|
|
||||||
|
|
||||||
synchronized (trade) {
|
|
||||||
if (!isAgent(dispute)) {
|
|
||||||
synchronized (disputeList) {
|
|
||||||
if (!disputeList.contains(dispute)) {
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
trade.setDisputeState(getDisputeStateStartedByPeer());
|
|
||||||
tradeManager.requestPersistence();
|
|
||||||
errorMessage = null;
|
|
||||||
} else {
|
|
||||||
// valid case if both have opened a dispute and agent was not online.
|
|
||||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
|
||||||
dispute.getTradeId());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
|
||||||
log.warn(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
|
|
||||||
log.error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the ChatMessage not the peerOpenedDisputeMessage for the ACK
|
|
||||||
ObservableList<ChatMessage> messages = peerOpenedDisputeMessage.getDispute().getChatMessages();
|
|
||||||
if (!messages.isEmpty()) {
|
|
||||||
ChatMessage msg = messages.get(0);
|
|
||||||
sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
|
||||||
requestPersistence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Send message
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public void sendOpenNewDisputeMessage(Dispute dispute,
|
|
||||||
boolean reOpen,
|
|
||||||
String updatedMultisigHex,
|
|
||||||
ResultHandler resultHandler,
|
|
||||||
FaultHandler faultHandler) {
|
|
||||||
log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
|
||||||
|
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
|
@ -469,8 +343,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
String disputeInfo = getDisputeInfo(dispute);
|
String disputeInfo = getDisputeInfo(dispute);
|
||||||
String sysMsg = dispute.isSupportTicket() ?
|
String sysMsg = dispute.isSupportTicket() ?
|
||||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
|
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||||
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||||
|
|
||||||
ChatMessage chatMessage = new ChatMessage(
|
ChatMessage chatMessage = new ChatMessage(
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
|
@ -486,31 +360,33 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||||
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute,
|
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
updatedMultisigHex);
|
updatedMultisigHex,
|
||||||
|
trade.getBuyer().getPaymentSentMessage());
|
||||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||||
chatMessage.getUid());
|
chatMessage.getUid());
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||||
dispute.getAgentPubKeyRing(),
|
dispute.getAgentPubKeyRing(),
|
||||||
openNewDisputeMessage,
|
disputeOpenedMessage,
|
||||||
new SendMailboxMessageListener() {
|
new SendMailboxMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||||
chatMessage.getUid());
|
chatMessage.getUid());
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setArrived(true);
|
chatMessage.setArrived(true);
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
@ -519,13 +395,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
public void onStoredInMailbox() {
|
public void onStoredInMailbox() {
|
||||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||||
chatMessage.getUid());
|
chatMessage.getUid());
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setStoredInMailbox(true);
|
chatMessage.setStoredInMailbox(true);
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
@ -534,8 +411,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}, errorMessage={}",
|
"chatMessage.uid={}, errorMessage={}",
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||||
chatMessage.getUid(), errorMessage);
|
chatMessage.getUid(), errorMessage);
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
|
@ -545,8 +422,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
faultHandler.handleFault("Sending dispute message failed: " +
|
faultHandler.handleFault("Sending dispute message failed: " +
|
||||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
||||||
"TradeId = " + dispute.getTradeId();
|
"TradeId = " + dispute.getTradeId();
|
||||||
|
@ -558,10 +434,111 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispute agent sends that to trading peer when he received openDispute request
|
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
||||||
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
||||||
|
Dispute dispute = message.getDispute();
|
||||||
|
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||||
|
|
||||||
|
// intialize
|
||||||
|
T disputeList = getDisputeList();
|
||||||
|
if (disputeList == null) {
|
||||||
|
log.warn("disputes is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispute.setSupportType(message.getSupportType());
|
||||||
|
dispute.setState(Dispute.State.NEW); // TODO: unused, remove?
|
||||||
|
Contract contract = dispute.getContract();
|
||||||
|
|
||||||
|
// validate dispute
|
||||||
|
try {
|
||||||
|
TradeDataValidation.validatePaymentAccountPayload(dispute);
|
||||||
|
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
||||||
|
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
||||||
|
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config);
|
||||||
|
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config);
|
||||||
|
} catch (TradeDataValidation.AddressException |
|
||||||
|
TradeDataValidation.NodeAddressException |
|
||||||
|
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||||
|
log.error(e.toString());
|
||||||
|
validationExceptions.add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get trade
|
||||||
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
|
if (trade == null) {
|
||||||
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get sender
|
||||||
|
PubKeyRing senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||||
|
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
|
||||||
|
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||||
|
|
||||||
|
// message to trader is expected from arbitrator
|
||||||
|
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
||||||
|
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
||||||
|
}
|
||||||
|
|
||||||
|
// arbitrator verifies signature of payment sent message if given
|
||||||
|
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
|
||||||
|
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||||
|
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||||
|
trade.setStateIfProgress(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update multisig hex
|
||||||
|
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
|
|
||||||
|
// update peer node address
|
||||||
|
// TODO: tests can reuse the same addresses so nullify equal peer
|
||||||
|
sender.setNodeAddress(message.getSenderNodeAddress());
|
||||||
|
|
||||||
|
// add chat message with price info
|
||||||
|
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
|
// add dispute
|
||||||
|
String errorMessage = null;
|
||||||
|
synchronized (disputeList) {
|
||||||
|
if (!disputeList.contains(dispute)) {
|
||||||
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
if (!storedDisputeOptional.isPresent()) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
|
||||||
|
// send dispute opened message to peer if arbitrator
|
||||||
|
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||||
|
tradeManager.requestPersistence();
|
||||||
|
errorMessage = null;
|
||||||
|
} else {
|
||||||
|
// valid case if both have opened a dispute and agent was not online
|
||||||
|
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||||
|
dispute.getTradeId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
|
||||||
|
log.warn(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use chat message instead of open dispute message for the ack
|
||||||
|
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
||||||
|
if (!messages.isEmpty()) {
|
||||||
|
ChatMessage msg = messages.get(0);
|
||||||
|
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add chat message with mediation info if applicable // TODO: not applicable in haveno
|
||||||
|
addMediationResultMessage(dispute);
|
||||||
|
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
// arbitrator sends dispute opened message to opener's peer
|
||||||
|
private void sendDisputeOpenedMessageToPeer(Dispute disputeFromOpener,
|
||||||
Contract contractFromOpener,
|
Contract contractFromOpener,
|
||||||
PubKeyRing pubKeyRing) {
|
PubKeyRing pubKeyRing,
|
||||||
|
String updatedMultisigHex) {
|
||||||
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
||||||
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
||||||
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
||||||
|
@ -569,13 +546,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// from the code below.
|
// from the code below.
|
||||||
UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener,
|
UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener,
|
||||||
contractFromOpener,
|
contractFromOpener,
|
||||||
pubKeyRing),
|
pubKeyRing,
|
||||||
|
updatedMultisigHex),
|
||||||
100, TimeUnit.MILLISECONDS);
|
100, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
||||||
Contract contractFromOpener,
|
Contract contractFromOpener,
|
||||||
PubKeyRing pubKeyRing) {
|
PubKeyRing pubKeyRing,
|
||||||
|
String updatedMultisigHex) {
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
log.warn("disputes is null");
|
log.warn("disputes is null");
|
||||||
|
@ -638,14 +617,23 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
disputeList.add(dispute);
|
disputeList.add(dispute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get trade
|
||||||
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
|
if (trade == null) {
|
||||||
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We mirrored dispute already!
|
// We mirrored dispute already!
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
|
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
|
||||||
PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute,
|
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType());
|
getSupportType(),
|
||||||
|
updatedMultisigHex,
|
||||||
|
trade.getSelf().getPaymentSentMessage());
|
||||||
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
|
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
|
||||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||||
|
@ -701,8 +689,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
// arbitrator send result to trader
|
// arbitrator sends result to trader when their dispute is closed
|
||||||
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) {
|
public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) {
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
log.warn("disputes is null");
|
log.warn("disputes is null");
|
||||||
|
@ -720,75 +708,114 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
disputeResult.setChatMessage(chatMessage);
|
disputeResult.setChatMessage(chatMessage);
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
|
|
||||||
NodeAddress peersNodeAddress;
|
// get trade
|
||||||
Contract contract = dispute.getContract();
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()))
|
if (trade == null) {
|
||||||
peersNodeAddress = contract.getBuyerNodeAddress();
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
else
|
return;
|
||||||
peersNodeAddress = contract.getSellerNodeAddress();
|
}
|
||||||
DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult,
|
|
||||||
|
// create unsigned dispute payout tx if not already published and arbitrator has trader's updated multisig info
|
||||||
|
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
|
||||||
|
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) {
|
||||||
|
|
||||||
|
// import multisig hex
|
||||||
|
MoneroWallet multisigWallet = trade.getWallet();
|
||||||
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
|
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
||||||
|
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||||
|
if (!updatedMultisigHexes.isEmpty()) {
|
||||||
|
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||||
|
trade.syncWallet();
|
||||||
|
trade.saveWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create unsigned dispute payout tx
|
||||||
|
if (!trade.isPayoutPublished()) {
|
||||||
|
log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId());
|
||||||
|
try {
|
||||||
|
MoneroTxWallet payoutTx = createDisputePayoutTx(trade, dispute, disputeResult, multisigWallet);
|
||||||
|
trade.setPayoutTx(payoutTx);
|
||||||
|
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (!trade.isPayoutPublished()) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create dispute closed message
|
||||||
|
String unsignedPayoutTxHex = receiver.getUpdatedMultisigHex() == null ? null : trade.getPayoutTxHex();
|
||||||
|
TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
|
||||||
|
boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
|
||||||
|
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType());
|
getSupportType(),
|
||||||
log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}",
|
trade.getSelf().getUpdatedMultisigHex(),
|
||||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(),
|
trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info
|
||||||
disputeResultMessage.getUid(), chatMessage.getUid());
|
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
|
|
||||||
|
// send dispute closed message
|
||||||
|
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",
|
||||||
|
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
|
||||||
|
disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(),
|
||||||
|
disputeClosedMessage.getUid(), chatMessage.getUid());
|
||||||
|
mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(),
|
||||||
dispute.getTraderPubKeyRing(),
|
dispute.getTraderPubKeyRing(),
|
||||||
disputeResultMessage,
|
disputeClosedMessage,
|
||||||
new SendMailboxMessageListener() {
|
new SendMailboxMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
|
||||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
|
||||||
chatMessage.getUid());
|
chatMessage.getUid());
|
||||||
|
|
||||||
// TODO: hack to sync wallet after dispute message received in order to detect payout published
|
// We use the chatMessage wrapped inside the DisputeClosedMessage for
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
|
||||||
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
UserThread.runAfter(() -> {
|
|
||||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
|
||||||
}, defaultRefreshPeriod / 1000 * (i + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setArrived(true);
|
chatMessage.setArrived(true);
|
||||||
|
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
|
||||||
|
trade.syncWalletNormallyForMs(30000);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStoredInMailbox() {
|
public void onStoredInMailbox() {
|
||||||
log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
|
||||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
|
||||||
chatMessage.getUid());
|
chatMessage.getUid());
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
// We use the chatMessage wrapped inside the DisputeClosedMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setStoredInMailbox(true);
|
chatMessage.setStoredInMailbox(true);
|
||||||
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
|
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
|
||||||
"chatMessage.uid={}, errorMessage={}",
|
"chatMessage.uid={}, errorMessage={}",
|
||||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
|
||||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
|
||||||
chatMessage.getUid(), errorMessage);
|
chatMessage.getUid(), errorMessage);
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
// We use the chatMessage wrapped inside the DisputeClosedMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setSendMessageError(errorMessage);
|
chatMessage.setSendMessageError(errorMessage);
|
||||||
|
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -796,6 +823,52 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
|
||||||
|
|
||||||
|
// multisig wallet must be synced
|
||||||
|
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
|
||||||
|
|
||||||
|
// collect winner and loser payout address and amounts
|
||||||
|
Contract contract = dispute.getContract();
|
||||||
|
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
|
||||||
|
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
|
||||||
|
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
|
||||||
|
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
||||||
|
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||||
|
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
||||||
|
|
||||||
|
// create transaction to get fee estimate
|
||||||
|
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
|
||||||
|
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
|
||||||
|
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
|
||||||
|
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
|
||||||
|
|
||||||
|
// create payout tx by increasing estimated fee until successful
|
||||||
|
MoneroTxWallet payoutTx = null;
|
||||||
|
int numAttempts = 0;
|
||||||
|
while (payoutTx == null && numAttempts < 50) {
|
||||||
|
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
|
||||||
|
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
|
||||||
|
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
|
||||||
|
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
|
||||||
|
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
|
||||||
|
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
|
||||||
|
}
|
||||||
|
numAttempts++;
|
||||||
|
try {
|
||||||
|
payoutTx = multisigWallet.createTx(txConfig);
|
||||||
|
} catch (MoneroError e) {
|
||||||
|
// exception expected // TODO: better way of estimating fee?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
|
||||||
|
log.info("Dispute payout transaction generated on attempt {}", numAttempts);
|
||||||
|
|
||||||
|
// save updated multisig hex
|
||||||
|
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||||
|
return payoutTx;
|
||||||
|
}
|
||||||
|
|
||||||
private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
|
private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
|
||||||
PubKeyRing receiverPubKeyRing = null;
|
PubKeyRing receiverPubKeyRing = null;
|
||||||
NodeAddress peerNodeAddress = null;
|
NodeAddress peerNodeAddress = null;
|
||||||
|
@ -878,15 +951,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
|
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
|
||||||
if (dispute.getMediatorsDisputeResult() != null) {
|
if (dispute.getMediatorsDisputeResult() != null) {
|
||||||
String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult());
|
String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult());
|
||||||
ChatMessage mediatorsDisputeResultMessage = new ChatMessage(
|
ChatMessage mediatorsDisputeClosedMessage = new ChatMessage(
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
dispute.getTradeId(),
|
dispute.getTradeId(),
|
||||||
pubKeyRing.hashCode(),
|
pubKeyRing.hashCode(),
|
||||||
false,
|
false,
|
||||||
mediatorsDisputeResult,
|
mediatorsDisputeResult,
|
||||||
p2PService.getAddress());
|
p2PService.getAddress());
|
||||||
mediatorsDisputeResultMessage.setSystemMessage(true);
|
mediatorsDisputeClosedMessage.setSystemMessage(true);
|
||||||
dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage);
|
dispute.addAndPersistChatMessage(mediatorsDisputeClosedMessage);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@ import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.proto.network.NetworkPayload;
|
import bisq.common.proto.network.NetworkPayload;
|
||||||
import bisq.common.util.Utilities;
|
import bisq.common.util.Utilities;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
@ -89,16 +87,6 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
@Nullable
|
@Nullable
|
||||||
private byte[] arbitratorPubKey;
|
private byte[] arbitratorPubKey;
|
||||||
private long closeDate;
|
private long closeDate;
|
||||||
@Setter
|
|
||||||
private boolean isLoserPublisher;
|
|
||||||
|
|
||||||
// added for XMR integration
|
|
||||||
@Nullable
|
|
||||||
@Setter
|
|
||||||
String arbitratorSignedPayoutTxHex;
|
|
||||||
@Nullable
|
|
||||||
@Setter
|
|
||||||
String arbitratorUpdatedMultisigHex;
|
|
||||||
|
|
||||||
public DisputeResult(String tradeId, int traderId) {
|
public DisputeResult(String tradeId, int traderId) {
|
||||||
this.tradeId = tradeId;
|
this.tradeId = tradeId;
|
||||||
|
@ -115,13 +103,10 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
String summaryNotes,
|
String summaryNotes,
|
||||||
@Nullable ChatMessage chatMessage,
|
@Nullable ChatMessage chatMessage,
|
||||||
@Nullable byte[] arbitratorSignature,
|
@Nullable byte[] arbitratorSignature,
|
||||||
@Nullable String arbitratorPayoutTxSigned,
|
|
||||||
@Nullable String arbitratorUpdatedMultisigHex,
|
|
||||||
long buyerPayoutAmount,
|
long buyerPayoutAmount,
|
||||||
long sellerPayoutAmount,
|
long sellerPayoutAmount,
|
||||||
@Nullable byte[] arbitratorPubKey,
|
@Nullable byte[] arbitratorPubKey,
|
||||||
long closeDate,
|
long closeDate) {
|
||||||
boolean isLoserPublisher) {
|
|
||||||
this.tradeId = tradeId;
|
this.tradeId = tradeId;
|
||||||
this.traderId = traderId;
|
this.traderId = traderId;
|
||||||
this.winner = winner;
|
this.winner = winner;
|
||||||
|
@ -132,13 +117,10 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
this.summaryNotesProperty.set(summaryNotes);
|
this.summaryNotesProperty.set(summaryNotes);
|
||||||
this.chatMessage = chatMessage;
|
this.chatMessage = chatMessage;
|
||||||
this.arbitratorSignature = arbitratorSignature;
|
this.arbitratorSignature = arbitratorSignature;
|
||||||
this.arbitratorSignedPayoutTxHex = arbitratorPayoutTxSigned;
|
|
||||||
this.arbitratorUpdatedMultisigHex = arbitratorUpdatedMultisigHex;
|
|
||||||
this.buyerPayoutAmount = buyerPayoutAmount;
|
this.buyerPayoutAmount = buyerPayoutAmount;
|
||||||
this.sellerPayoutAmount = sellerPayoutAmount;
|
this.sellerPayoutAmount = sellerPayoutAmount;
|
||||||
this.arbitratorPubKey = arbitratorPubKey;
|
this.arbitratorPubKey = arbitratorPubKey;
|
||||||
this.closeDate = closeDate;
|
this.closeDate = closeDate;
|
||||||
this.isLoserPublisher = isLoserPublisher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,13 +139,10 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
proto.getSummaryNotes(),
|
proto.getSummaryNotes(),
|
||||||
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
|
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
|
||||||
proto.getArbitratorSignature().toByteArray(),
|
proto.getArbitratorSignature().toByteArray(),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignedPayoutTxHex()),
|
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getArbitratorUpdatedMultisigHex()),
|
|
||||||
proto.getBuyerPayoutAmount(),
|
proto.getBuyerPayoutAmount(),
|
||||||
proto.getSellerPayoutAmount(),
|
proto.getSellerPayoutAmount(),
|
||||||
proto.getArbitratorPubKey().toByteArray(),
|
proto.getArbitratorPubKey().toByteArray(),
|
||||||
proto.getCloseDate(),
|
proto.getCloseDate());
|
||||||
proto.getIsLoserPublisher());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -178,13 +157,8 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
.setSummaryNotes(summaryNotesProperty.get())
|
.setSummaryNotes(summaryNotesProperty.get())
|
||||||
.setBuyerPayoutAmount(buyerPayoutAmount)
|
.setBuyerPayoutAmount(buyerPayoutAmount)
|
||||||
.setSellerPayoutAmount(sellerPayoutAmount)
|
.setSellerPayoutAmount(sellerPayoutAmount)
|
||||||
.setCloseDate(closeDate)
|
.setCloseDate(closeDate);
|
||||||
.setIsLoserPublisher(isLoserPublisher);
|
|
||||||
|
|
||||||
Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature)));
|
|
||||||
Optional.ofNullable(arbitratorSignedPayoutTxHex).ifPresent(arbitratorPayoutTxSigned -> builder.setArbitratorSignedPayoutTxHex(arbitratorPayoutTxSigned));
|
|
||||||
Optional.ofNullable(arbitratorUpdatedMultisigHex).ifPresent(arbitratorUpdatedMultisigHex -> builder.setArbitratorUpdatedMultisigHex(arbitratorUpdatedMultisigHex));
|
|
||||||
Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey)));
|
|
||||||
Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name())));
|
Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name())));
|
||||||
Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
|
Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
|
||||||
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
|
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
|
||||||
|
@ -265,13 +239,10 @@ public final class DisputeResult implements NetworkPayload {
|
||||||
",\n summaryNotesProperty=" + summaryNotesProperty +
|
",\n summaryNotesProperty=" + summaryNotesProperty +
|
||||||
",\n chatMessage=" + chatMessage +
|
",\n chatMessage=" + chatMessage +
|
||||||
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
||||||
",\n arbitratorPayoutTxSigned=" + arbitratorSignedPayoutTxHex +
|
|
||||||
",\n arbitratorUpdatedMultisigHex=" + arbitratorUpdatedMultisigHex +
|
|
||||||
",\n buyerPayoutAmount=" + buyerPayoutAmount +
|
",\n buyerPayoutAmount=" + buyerPayoutAmount +
|
||||||
",\n sellerPayoutAmount=" + sellerPayoutAmount +
|
",\n sellerPayoutAmount=" + sellerPayoutAmount +
|
||||||
",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) +
|
",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) +
|
||||||
",\n closeDate=" + closeDate +
|
",\n closeDate=" + closeDate +
|
||||||
",\n isLoserPublisher=" + isLoserPublisher +
|
|
||||||
"\n}";
|
"\n}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
import bisq.core.offer.OpenOffer;
|
|
||||||
import bisq.core.offer.OpenOfferManager;
|
import bisq.core.offer.OpenOfferManager;
|
||||||
import bisq.core.provider.price.PriceFeedService;
|
import bisq.core.provider.price.PriceFeedService;
|
||||||
import bisq.core.support.SupportType;
|
import bisq.core.support.SupportType;
|
||||||
|
@ -30,17 +29,12 @@ import bisq.core.support.dispute.Dispute;
|
||||||
import bisq.core.support.dispute.DisputeManager;
|
import bisq.core.support.dispute.DisputeManager;
|
||||||
import bisq.core.support.dispute.DisputeResult;
|
import bisq.core.support.dispute.DisputeResult;
|
||||||
import bisq.core.support.dispute.DisputeResult.Winner;
|
import bisq.core.support.dispute.DisputeResult.Winner;
|
||||||
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||||
import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest;
|
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse;
|
|
||||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
|
||||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
|
||||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.support.messages.SupportMessage;
|
import bisq.core.support.messages.SupportMessage;
|
||||||
import bisq.core.trade.ClosedTradableManager;
|
import bisq.core.trade.ClosedTradableManager;
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
import bisq.core.trade.Tradable;
|
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.TradeManager;
|
import bisq.core.trade.TradeManager;
|
||||||
import bisq.core.util.ParsingUtils;
|
import bisq.core.util.ParsingUtils;
|
||||||
|
@ -48,24 +42,20 @@ import bisq.core.util.ParsingUtils;
|
||||||
import bisq.network.p2p.AckMessageSourceType;
|
import bisq.network.p2p.AckMessageSourceType;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
import bisq.network.p2p.SendDirectMessageListener;
|
import common.utils.GenUtils;
|
||||||
import bisq.network.p2p.SendMailboxMessageListener;
|
|
||||||
|
|
||||||
import bisq.common.Timer;
|
import bisq.common.Timer;
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -73,11 +63,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import monero.common.MoneroError;
|
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroDestination;
|
import monero.wallet.model.MoneroDestination;
|
||||||
import monero.wallet.model.MoneroMultisigSignResult;
|
import monero.wallet.model.MoneroMultisigSignResult;
|
||||||
import monero.wallet.model.MoneroTxConfig;
|
|
||||||
import monero.wallet.model.MoneroTxSet;
|
import monero.wallet.model.MoneroTxSet;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
|
@ -122,20 +110,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
log.info("Received {} from {} with tradeId {} and uid {}",
|
log.info("Received {} from {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
if (message instanceof OpenNewDisputeMessage) {
|
if (message instanceof DisputeOpenedMessage) {
|
||||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
|
||||||
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
|
||||||
} else if (message instanceof ChatMessage) {
|
} else if (message instanceof ChatMessage) {
|
||||||
onChatMessage((ChatMessage) message);
|
handleChatMessage((ChatMessage) message);
|
||||||
} else if (message instanceof DisputeResultMessage) {
|
} else if (message instanceof DisputeClosedMessage) {
|
||||||
onDisputeResultMessage((DisputeResultMessage) message);
|
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||||
} else if (message instanceof PeerPublishedDisputePayoutTxMessage) {
|
|
||||||
onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message);
|
|
||||||
} else if (message instanceof ArbitratorPayoutTxRequest) {
|
|
||||||
onArbitratorPayoutTxRequest((ArbitratorPayoutTxRequest) message);
|
|
||||||
} else if (message instanceof ArbitratorPayoutTxResponse) {
|
|
||||||
onArbitratorPayoutTxResponse((ArbitratorPayoutTxResponse) message);
|
|
||||||
} else {
|
} else {
|
||||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||||
}
|
}
|
||||||
|
@ -147,11 +127,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
return dispute.getContract().getArbitratorNodeAddress();
|
return dispute.getContract().getArbitratorNodeAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
|
||||||
return Trade.DisputeState.DISPUTE_STARTED_BY_PEER;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AckMessageSourceType getAckMessageSourceType() {
|
protected AckMessageSourceType getAckMessageSourceType() {
|
||||||
return AckMessageSourceType.ARBITRATION_MESSAGE;
|
return AckMessageSourceType.ARBITRATION_MESSAGE;
|
||||||
|
@ -159,7 +134,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanupDisputes() {
|
public void cleanupDisputes() {
|
||||||
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED));
|
// no action
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -185,43 +160,52 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Message handler
|
// Dispute handling
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// received by both peers when arbitrator closes disputes
|
||||||
@Override
|
@Override
|
||||||
// We get that message at both peers. The dispute object is in context of the trader
|
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
|
||||||
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
|
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
||||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||||
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId());
|
|
||||||
|
|
||||||
String tradeId = disputeResult.getTradeId();
|
String tradeId = disputeResult.getTradeId();
|
||||||
log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId());
|
|
||||||
|
// get trade
|
||||||
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
|
if (trade == null) {
|
||||||
|
log.warn("Dispute trade {} does not exist", tradeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
|
||||||
|
|
||||||
|
// get dispute
|
||||||
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
||||||
String uid = disputeResultMessage.getUid();
|
String uid = disputeClosedMessage.getUid();
|
||||||
if (!disputeOptional.isPresent()) {
|
if (!disputeOptional.isPresent()) {
|
||||||
log.warn("We got a dispute result msg but we don't have a matching dispute. " +
|
log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
|
||||||
"That might happen when we get the disputeResultMessage before the dispute was created. " +
|
"That might happen when we get the DisputeClosedMessage before the dispute was created. " +
|
||||||
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
"We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||||
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2);
|
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeClosedMessage), 2);
|
||||||
delayMsgMap.put(uid, timer);
|
delayMsgMap.put(uid, timer);
|
||||||
} else {
|
} else {
|
||||||
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
|
||||||
"That should never happen. TradeId = " + tradeId);
|
"That should never happen. TradeId = " + tradeId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Dispute dispute = disputeOptional.get();
|
Dispute dispute = disputeOptional.get();
|
||||||
|
|
||||||
// verify that arbitrator does not get DisputeResultMessage
|
// verify that arbitrator does not get DisputeClosedMessage
|
||||||
if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) {
|
if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) {
|
||||||
log.error("Arbitrator received disputeResultMessage. That must never happen.");
|
log.error("Arbitrator received disputeResultMessage. That should never happen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set dispute state
|
||||||
cleanupRetryMap(uid);
|
cleanupRetryMap(uid);
|
||||||
if (!dispute.getChatMessages().contains(chatMessage)) {
|
if (!dispute.getChatMessages().contains(chatMessage)) {
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
|
@ -229,281 +213,76 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||||
}
|
}
|
||||||
dispute.setIsClosed();
|
dispute.setIsClosed();
|
||||||
|
|
||||||
if (dispute.disputeResultProperty().get() != null) {
|
if (dispute.disputeResultProperty().get() != null) {
|
||||||
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
|
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
|
||||||
"again because the first close did not succeed. TradeId = " + tradeId);
|
"again because the first close did not succeed. TradeId = " + tradeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispute.setDisputeResult(disputeResult);
|
dispute.setDisputeResult(disputeResult);
|
||||||
|
|
||||||
|
// import multisig hex
|
||||||
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
|
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
|
||||||
|
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||||
|
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||||
|
|
||||||
|
// sync and save wallet
|
||||||
|
trade.syncWallet();
|
||||||
|
trade.saveWallet();
|
||||||
|
|
||||||
|
// run off main thread
|
||||||
|
new Thread(() -> {
|
||||||
String errorMessage = null;
|
String errorMessage = null;
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
boolean requestUpdatedPayoutTx = false;
|
|
||||||
Contract contract = dispute.getContract();
|
// attempt to sign and publish dispute payout tx if given and not already published
|
||||||
|
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
|
||||||
|
|
||||||
|
// wait to sign and publish payout tx if defer flag set
|
||||||
|
if (disputeClosedMessage.isDeferPublishPayout()) {
|
||||||
|
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
|
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||||
|
trade.syncWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign and publish dispute payout tx if peer still has not published
|
||||||
|
if (!trade.isPayoutPublished()) {
|
||||||
try {
|
try {
|
||||||
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
|
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb)
|
signAndPublishDisputePayoutTx(trade, disputeClosedMessage.getUnsignedPayoutTxHex());
|
||||||
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
|
|
||||||
// more BTC as he has deposited
|
|
||||||
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
|
||||||
DisputeResult.Winner publisher = disputeResult.getWinner();
|
|
||||||
|
|
||||||
// Sometimes the user who receives the trade amount is never online, so we might want to
|
|
||||||
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
|
|
||||||
// Default isLoserPublisher is set to false
|
|
||||||
if (disputeResult.isLoserPublisher()) {
|
|
||||||
// we invert the logic
|
|
||||||
if (publisher == DisputeResult.Winner.BUYER)
|
|
||||||
publisher = DisputeResult.Winner.SELLER;
|
|
||||||
else if (publisher == DisputeResult.Winner.SELLER)
|
|
||||||
publisher = DisputeResult.Winner.BUYER;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|
|
||||||
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)) {
|
|
||||||
|
|
||||||
MoneroTxWallet payoutTx = null;
|
|
||||||
if (tradeOptional.isPresent()) {
|
|
||||||
payoutTx = tradeOptional.get().getPayoutTx();
|
|
||||||
} else {
|
|
||||||
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
|
|
||||||
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
|
|
||||||
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); // TODO (woodser): payout tx is transient so won't exist after restart?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (payoutTx == null) {
|
|
||||||
|
|
||||||
// gather relevant info
|
|
||||||
String arbitratorSignedPayoutTxHex = disputeResult.getArbitratorSignedPayoutTxHex();
|
|
||||||
|
|
||||||
if (arbitratorSignedPayoutTxHex != null) {
|
|
||||||
if (!tradeOptional.isPresent()) throw new RuntimeException("Trade must not be null when trader signs arbitrator's payout tx");
|
|
||||||
|
|
||||||
try {
|
|
||||||
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
|
|
||||||
onTraderSignedDisputePayoutTx(tradeId, txSet);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
|
// check if payout published again
|
||||||
|
trade.syncWallet();
|
||||||
|
if (trade.isPayoutPublished()) {
|
||||||
|
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
|
} else {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId + " SignedPayoutTx = " + arbitratorSignedPayoutTxHex;
|
errorMessage = "Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
requestUpdatedPayoutTx = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("We already got a payout tx. That might be the case if the other peer did not get the " +
|
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
"payout tx and opened a dispute. TradeId = " + tradeId);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.trace("We don't publish the tx as we are not the winning party.");
|
if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
// Clean up tangling trades
|
else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getName(), trade.getId());
|
||||||
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
|
|
||||||
closeTradeOrOffer(tradeId);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// catch (TransactionVerificationException e) {
|
|
||||||
// errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
|
||||||
// log.error(errorMessage, e);
|
|
||||||
// success = false;
|
|
||||||
//
|
|
||||||
// // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
|
|
||||||
// // we get a TransactionVerificationException. No reason to keep that dispute open...
|
|
||||||
// updateTradeOrOpenOfferManager(tradeId);
|
|
||||||
//
|
|
||||||
// throw new RuntimeException(errorMessage);
|
|
||||||
// }
|
|
||||||
// catch (AddressFormatException | WalletException e) {
|
|
||||||
catch (Exception e) {
|
|
||||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
|
|
||||||
log.error(errorMessage, e);
|
|
||||||
success = false;
|
|
||||||
|
|
||||||
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
|
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
||||||
// we get a TransactionVerificationException. No reason to keep that dispute open...
|
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
||||||
closeTradeOrOffer(tradeId); // TODO (woodser): only close in case of verification exception?
|
|
||||||
|
|
||||||
throw new RuntimeException(errorMessage);
|
|
||||||
} finally {
|
|
||||||
// We use the chatMessage as we only persist those not the disputeResultMessage.
|
|
||||||
// If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage.
|
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
||||||
|
|
||||||
// If dispute opener's peer is co-signer, send updated multisig hex to arbitrator to receive updated payout tx
|
|
||||||
if (requestUpdatedPayoutTx) {
|
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
synchronized (trade) {
|
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // TODO (woodser): this is closed after sending ArbitratorPayoutTxRequest to arbitrator which opens and syncs multisig and responds with signed dispute tx. more efficient way is to include with arbitrator-signed dispute tx with dispute result?
|
|
||||||
sendArbitratorPayoutTxRequest(multisigWallet.exportMultisigHex(), dispute, contract);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
|
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) {
|
||||||
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
|
||||||
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
|
||||||
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
|
|
||||||
synchronized (trade) {
|
|
||||||
|
|
||||||
// get dispute and trade
|
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
|
||||||
if (!disputeOptional.isPresent()) {
|
|
||||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
|
||||||
// We delay 3 sec. to be sure the close msg gets added first
|
|
||||||
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
|
|
||||||
delayMsgMap.put(uid, timer);
|
|
||||||
} else {
|
|
||||||
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
|
|
||||||
"That should never happen. TradeId = " + tradeId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Dispute dispute = disputeOptional.get();
|
|
||||||
|
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
|
||||||
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
|
||||||
|
|
||||||
cleanupRetryMap(uid);
|
|
||||||
|
|
||||||
// update trade wallet
|
|
||||||
MoneroWallet wallet = trade.getWallet();
|
|
||||||
if (wallet != null) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
|
|
||||||
trade.syncWallet();
|
|
||||||
wallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
|
|
||||||
trade.saveWallet();
|
|
||||||
MoneroTxWallet parsedPayoutTx = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
|
||||||
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
|
||||||
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs());
|
|
||||||
// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx");
|
|
||||||
// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet());
|
|
||||||
|
|
||||||
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
|
||||||
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
|
||||||
requestPersistence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
|
|
||||||
// TODO: this should be invoked from mailbox message and send mailbox message response to support offline arbitrator
|
|
||||||
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
|
|
||||||
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
|
|
||||||
String tradeId = request.getTradeId();
|
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
synchronized (trade) {
|
|
||||||
Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get();
|
|
||||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
|
|
||||||
// verify sender is co-signer and receiver is arbitrator
|
|
||||||
// System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first
|
|
||||||
// System.out.println(disputeResult);
|
|
||||||
// System.out.println(disputeResult.getWinner());
|
|
||||||
// System.out.println(contract.getBuyerNodeAddress());
|
|
||||||
// System.out.println(contract.getSellerNodeAddress());
|
|
||||||
boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress()));
|
|
||||||
boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher();
|
|
||||||
boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing());
|
|
||||||
|
|
||||||
if (!senderIsCosigner) {
|
|
||||||
log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!receiverIsArbitrator) {
|
|
||||||
log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update arbitrator's multisig wallet with co-signer's multisig hex
|
|
||||||
trade.syncWallet();
|
|
||||||
MoneroWallet multisigWallet = trade.getWallet();
|
|
||||||
try {
|
|
||||||
multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
|
|
||||||
trade.saveWallet();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create updated payout tx
|
|
||||||
MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
|
||||||
System.out.println("Arbitrator created updated payout tx for co-signer!!!");
|
|
||||||
System.out.println(payoutTx);
|
|
||||||
|
|
||||||
// send updated payout tx to sender
|
|
||||||
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
|
||||||
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
|
|
||||||
tradeId,
|
|
||||||
p2PService.getAddress(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
SupportType.ARBITRATION,
|
|
||||||
payoutTx.getTxSet().getMultisigTxHex());
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
|
||||||
p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(),
|
|
||||||
senderPubKeyRing,
|
|
||||||
response,
|
|
||||||
new SendDirectMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
|
||||||
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
|
||||||
|
|
||||||
// TODO: hack to sync wallet after dispute message received in order to detect payout published
|
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
|
||||||
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
UserThread.runAfter(() -> {
|
|
||||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
|
||||||
}, defaultRefreshPeriod / 1000 * (i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
|
||||||
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer)
|
|
||||||
private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) {
|
|
||||||
log.info("{}.onArbitratorPayoutTxResponse()", getClass().getSimpleName());
|
|
||||||
|
|
||||||
// gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc
|
|
||||||
String tradeId = response.getTradeId();
|
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
synchronized (trade) {
|
|
||||||
|
|
||||||
// verify and sign dispute payout tx
|
|
||||||
MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex());
|
|
||||||
|
|
||||||
// process fully signed payout tx (publish, notify peer, etc)
|
|
||||||
onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) {
|
|
||||||
|
|
||||||
// gather trade info
|
// gather trade info
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
MoneroWallet multisigWallet = trade.getWallet();
|
||||||
multisigWallet.sync();
|
Optional<Dispute> disputeOptional = findDispute(trade.getId());
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + trade.getId());
|
||||||
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
|
||||||
Dispute dispute = disputeOptional.get();
|
Dispute dispute = disputeOptional.get();
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
||||||
|
@ -514,10 +293,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
||||||
|
|
||||||
// parse arbitrator-signed payout tx
|
// parse arbitrator-signed payout tx
|
||||||
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
MoneroTxSet signedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||||
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
||||||
MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0);
|
MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0);
|
||||||
log.info("Received updated multisig hex and partially signed payout tx from arbitrator:\n" + arbitratorSignedPayoutTx);
|
|
||||||
|
|
||||||
// verify payout tx has 1 or 2 destinations
|
// verify payout tx has 1 or 2 destinations
|
||||||
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
||||||
|
@ -553,168 +331,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
||||||
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
||||||
|
|
||||||
// update multisig wallet from arbitrator
|
|
||||||
multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex());
|
|
||||||
xmrWalletService.saveWallet(multisigWallet);
|
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
return parsedTxSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) {
|
|
||||||
|
|
||||||
// gather trade info
|
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
|
||||||
if (!disputeOptional.isPresent()) {
|
|
||||||
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Dispute dispute = disputeOptional.get();
|
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
Trade trade = tradeManager.getOpenTrade(tradeId).get();
|
|
||||||
|
|
||||||
// submit fully signed payout tx to the network
|
// submit fully signed payout tx to the network
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // closed when trade completed (TradeManager.onTradeCompleted())
|
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
|
||||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex());
|
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||||
txSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||||
trade.setPayoutTxId(txSet.getTxs().get(0).getHash());
|
trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
||||||
trade.setPayoutState(Trade.PayoutState.PUBLISHED);
|
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||||
dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash());
|
dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
||||||
sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract);
|
return signedTxSet;
|
||||||
closeTradeOrOffer(tradeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Send messages
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// winner (or buyer in case of 50/50) sends tx to other peer
|
|
||||||
private void sendPeerPublishedPayoutTxMessage(String updatedMultisigHex, String payoutTxHex, Dispute dispute, Contract contract) {
|
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
|
||||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
|
||||||
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress);
|
|
||||||
PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(updatedMultisigHex,
|
|
||||||
payoutTxHex,
|
|
||||||
dispute.getTradeId(),
|
|
||||||
p2PService.getAddress(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
getSupportType());
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
|
|
||||||
peersPubKeyRing,
|
|
||||||
message,
|
|
||||||
new SendMailboxMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStoredInMailbox() {
|
|
||||||
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
|
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeTradeOrOffer(String tradeId) {
|
|
||||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
|
||||||
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
|
||||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
|
|
||||||
} else {
|
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
|
||||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx
|
|
||||||
private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) {
|
|
||||||
ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest(
|
|
||||||
dispute,
|
|
||||||
p2PService.getAddress(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
SupportType.ARBITRATION,
|
|
||||||
updatedMultisigHex);
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
|
||||||
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid());
|
|
||||||
p2PService.sendEncryptedDirectMessage(contract.getArbitratorNodeAddress(),
|
|
||||||
dispute.getAgentPubKeyRing(),
|
|
||||||
request,
|
|
||||||
new SendDirectMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
|
||||||
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
|
||||||
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid(), errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Disputed payout tx signing
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
|
|
||||||
|
|
||||||
// multisig wallet must be synced
|
|
||||||
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
|
|
||||||
|
|
||||||
// collect winner and loser payout address and amounts
|
|
||||||
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
|
|
||||||
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
|
|
||||||
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
|
|
||||||
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
|
||||||
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
|
||||||
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
|
||||||
|
|
||||||
// create transaction to get fee estimate
|
|
||||||
// TODO (woodser): include arbitration fee
|
|
||||||
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
|
|
||||||
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
|
|
||||||
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
|
|
||||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
|
|
||||||
|
|
||||||
// create payout tx by increasing estimated fee until successful
|
|
||||||
MoneroTxWallet payoutTx = null;
|
|
||||||
int numAttempts = 0;
|
|
||||||
while (payoutTx == null && numAttempts < 50) {
|
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
|
|
||||||
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
|
|
||||||
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
|
|
||||||
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
|
|
||||||
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
|
|
||||||
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
|
|
||||||
}
|
|
||||||
numAttempts++;
|
|
||||||
try {
|
|
||||||
payoutTx = multisigWallet.createTx(txConfig);
|
|
||||||
} catch (MoneroError e) {
|
|
||||||
// exception expected // TODO: better way of estimating fee?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
|
|
||||||
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
|
||||||
return payoutTx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Dispute;
|
||||||
import bisq.core.support.dispute.DisputeManager;
|
import bisq.core.support.dispute.DisputeManager;
|
||||||
import bisq.core.support.dispute.DisputeResult;
|
import bisq.core.support.dispute.DisputeResult;
|
||||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.support.messages.SupportMessage;
|
import bisq.core.support.messages.SupportMessage;
|
||||||
import bisq.core.trade.ClosedTradableManager;
|
import bisq.core.trade.ClosedTradableManager;
|
||||||
|
@ -107,25 +106,18 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||||
log.info("Received {} with tradeId {} and uid {}",
|
log.info("Received {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
if (message instanceof OpenNewDisputeMessage) {
|
if (message instanceof DisputeOpenedMessage) {
|
||||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
|
||||||
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
|
||||||
} else if (message instanceof ChatMessage) {
|
} else if (message instanceof ChatMessage) {
|
||||||
onChatMessage((ChatMessage) message);
|
handleChatMessage((ChatMessage) message);
|
||||||
} else if (message instanceof DisputeResultMessage) {
|
} else if (message instanceof DisputeClosedMessage) {
|
||||||
onDisputeResultMessage((DisputeResultMessage) message);
|
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
|
||||||
return Trade.DisputeState.MEDIATION_STARTED_BY_PEER;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AckMessageSourceType getAckMessageSourceType() {
|
protected AckMessageSourceType getAckMessageSourceType() {
|
||||||
return AckMessageSourceType.MEDIATION_MESSAGE;
|
return AckMessageSourceType.MEDIATION_MESSAGE;
|
||||||
|
@ -164,7 +156,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// We get that message at both peers. The dispute object is in context of the trader
|
// We get that message at both peers. The dispute object is in context of the trader
|
||||||
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
|
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
|
||||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||||
String tradeId = disputeResult.getTradeId();
|
String tradeId = disputeResult.getTradeId();
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
|
@ -177,7 +169,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||||
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||||
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2);
|
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
|
||||||
delayMsgMap.put(uid, timer);
|
delayMsgMap.put(uid, timer);
|
||||||
} else {
|
} else {
|
||||||
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
||||||
|
|
|
@ -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.network.p2p.NodeAddress;
|
||||||
|
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
|
import bisq.common.proto.ProtoUtil;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public final class DisputeResultMessage extends DisputeMessage {
|
public final class DisputeClosedMessage extends DisputeMessage {
|
||||||
private final DisputeResult disputeResult;
|
private final DisputeResult disputeResult;
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
|
private final String updatedMultisigHex;
|
||||||
|
@Nullable
|
||||||
|
private final String unsignedPayoutTxHex;
|
||||||
|
private final boolean deferPublishPayout;
|
||||||
|
|
||||||
public DisputeResultMessage(DisputeResult disputeResult,
|
public DisputeClosedMessage(DisputeResult disputeResult,
|
||||||
NodeAddress senderNodeAddress,
|
NodeAddress senderNodeAddress,
|
||||||
String uid,
|
String uid,
|
||||||
SupportType supportType) {
|
SupportType supportType,
|
||||||
|
String updatedMultisigHex,
|
||||||
|
@Nullable String unsignedPayoutTxHex,
|
||||||
|
boolean deferPublishPayout) {
|
||||||
this(disputeResult,
|
this(disputeResult,
|
||||||
senderNodeAddress,
|
senderNodeAddress,
|
||||||
uid,
|
uid,
|
||||||
Version.getP2PMessageVersion(),
|
Version.getP2PMessageVersion(),
|
||||||
supportType);
|
supportType,
|
||||||
|
updatedMultisigHex,
|
||||||
|
unsignedPayoutTxHex,
|
||||||
|
deferPublishPayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,34 +65,45 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||||
// PROTO BUFFER
|
// PROTO BUFFER
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private DisputeResultMessage(DisputeResult disputeResult,
|
private DisputeClosedMessage(DisputeResult disputeResult,
|
||||||
NodeAddress senderNodeAddress,
|
NodeAddress senderNodeAddress,
|
||||||
String uid,
|
String uid,
|
||||||
String messageVersion,
|
String messageVersion,
|
||||||
SupportType supportType) {
|
SupportType supportType,
|
||||||
|
String updatedMultisigHex,
|
||||||
|
String unsignedPayoutTxHex,
|
||||||
|
boolean deferPublishPayout) {
|
||||||
super(messageVersion, uid, supportType);
|
super(messageVersion, uid, supportType);
|
||||||
this.disputeResult = disputeResult;
|
this.disputeResult = disputeResult;
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
|
this.updatedMultisigHex = updatedMultisigHex;
|
||||||
|
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
|
||||||
|
this.deferPublishPayout = deferPublishPayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||||
return getNetworkEnvelopeBuilder()
|
protobuf.DisputeClosedMessage.Builder builder = protobuf.DisputeClosedMessage.newBuilder()
|
||||||
.setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder()
|
|
||||||
.setDisputeResult(disputeResult.toProtoMessage())
|
.setDisputeResult(disputeResult.toProtoMessage())
|
||||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||||
.setUid(uid)
|
.setUid(uid)
|
||||||
.setType(SupportType.toProtoMessage(supportType)))
|
.setType(SupportType.toProtoMessage(supportType))
|
||||||
.build();
|
.setUpdatedMultisigHex(updatedMultisigHex)
|
||||||
|
.setDeferPublishPayout(deferPublishPayout);
|
||||||
|
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
|
||||||
|
return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DisputeResultMessage fromProto(protobuf.DisputeResultMessage proto, String messageVersion) {
|
public static DisputeClosedMessage fromProto(protobuf.DisputeClosedMessage proto, String messageVersion) {
|
||||||
checkArgument(proto.hasDisputeResult(), "DisputeResult must be set");
|
checkArgument(proto.hasDisputeResult(), "DisputeResult must be set");
|
||||||
return new DisputeResultMessage(DisputeResult.fromProto(proto.getDisputeResult()),
|
return new DisputeClosedMessage(DisputeResult.fromProto(proto.getDisputeResult()),
|
||||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||||
proto.getUid(),
|
proto.getUid(),
|
||||||
messageVersion,
|
messageVersion,
|
||||||
SupportType.fromProto(proto.getType()));
|
SupportType.fromProto(proto.getType()),
|
||||||
|
proto.getUpdatedMultisigHex(),
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
|
||||||
|
proto.getDeferPublishPayout());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -88,12 +113,13 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "DisputeResultMessage{" +
|
return "DisputeClosedMessage{" +
|
||||||
"\n disputeResult=" + disputeResult +
|
"\n disputeResult=" + disputeResult +
|
||||||
",\n senderNodeAddress=" + senderNodeAddress +
|
",\n senderNodeAddress=" + senderNodeAddress +
|
||||||
",\n DisputeResultMessage.uid='" + uid + '\'' +
|
",\n DisputeClosedMessage.uid='" + uid + '\'' +
|
||||||
",\n messageVersion=" + messageVersion +
|
",\n messageVersion=" + messageVersion +
|
||||||
",\n supportType=" + supportType +
|
",\n supportType=" + supportType +
|
||||||
|
",\n deferPublishPayout=" + deferPublishPayout +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,9 +20,11 @@ package bisq.core.support.dispute.messages;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
import bisq.core.support.SupportType;
|
import bisq.core.support.SupportType;
|
||||||
import bisq.core.support.dispute.Dispute;
|
import bisq.core.support.dispute.Dispute;
|
||||||
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
@ -30,22 +32,25 @@ import lombok.Value;
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Value
|
@Value
|
||||||
public final class OpenNewDisputeMessage extends DisputeMessage {
|
public final class DisputeOpenedMessage extends DisputeMessage {
|
||||||
private final Dispute dispute;
|
private final Dispute dispute;
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
private final String updatedMultisigHex;
|
private final String updatedMultisigHex;
|
||||||
|
private final PaymentSentMessage paymentSentMessage;
|
||||||
|
|
||||||
public OpenNewDisputeMessage(Dispute dispute,
|
public DisputeOpenedMessage(Dispute dispute,
|
||||||
NodeAddress senderNodeAddress,
|
NodeAddress senderNodeAddress,
|
||||||
String uid,
|
String uid,
|
||||||
SupportType supportType,
|
SupportType supportType,
|
||||||
String updatedMultisigHex) {
|
String updatedMultisigHex,
|
||||||
|
PaymentSentMessage paymentSentMessage) {
|
||||||
this(dispute,
|
this(dispute,
|
||||||
senderNodeAddress,
|
senderNodeAddress,
|
||||||
uid,
|
uid,
|
||||||
Version.getP2PMessageVersion(),
|
Version.getP2PMessageVersion(),
|
||||||
supportType,
|
supportType,
|
||||||
updatedMultisigHex);
|
updatedMultisigHex,
|
||||||
|
paymentSentMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,39 +58,42 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||||
// PROTO BUFFER
|
// PROTO BUFFER
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private OpenNewDisputeMessage(Dispute dispute,
|
private DisputeOpenedMessage(Dispute dispute,
|
||||||
NodeAddress senderNodeAddress,
|
NodeAddress senderNodeAddress,
|
||||||
String uid,
|
String uid,
|
||||||
String messageVersion,
|
String messageVersion,
|
||||||
SupportType supportType,
|
SupportType supportType,
|
||||||
String updatedMultisigHex) {
|
String updatedMultisigHex,
|
||||||
|
PaymentSentMessage paymentSentMessage) {
|
||||||
super(messageVersion, uid, supportType);
|
super(messageVersion, uid, supportType);
|
||||||
this.dispute = dispute;
|
this.dispute = dispute;
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
this.updatedMultisigHex = updatedMultisigHex;
|
this.updatedMultisigHex = updatedMultisigHex;
|
||||||
|
this.paymentSentMessage = paymentSentMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||||
return getNetworkEnvelopeBuilder()
|
protobuf.DisputeOpenedMessage.Builder builder = protobuf.DisputeOpenedMessage.newBuilder()
|
||||||
.setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder()
|
|
||||||
.setUid(uid)
|
.setUid(uid)
|
||||||
.setDispute(dispute.toProtoMessage())
|
.setDispute(dispute.toProtoMessage())
|
||||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||||
.setType(SupportType.toProtoMessage(supportType))
|
.setType(SupportType.toProtoMessage(supportType))
|
||||||
.setUpdatedMultisigHex(updatedMultisigHex))
|
.setUpdatedMultisigHex(updatedMultisigHex);
|
||||||
.build();
|
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||||
|
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OpenNewDisputeMessage fromProto(protobuf.OpenNewDisputeMessage proto,
|
public static DisputeOpenedMessage fromProto(protobuf.DisputeOpenedMessage proto,
|
||||||
CoreProtoResolver coreProtoResolver,
|
CoreProtoResolver coreProtoResolver,
|
||||||
String messageVersion) {
|
String messageVersion) {
|
||||||
return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
|
return new DisputeOpenedMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
|
||||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||||
proto.getUid(),
|
proto.getUid(),
|
||||||
messageVersion,
|
messageVersion,
|
||||||
SupportType.fromProto(proto.getType()),
|
SupportType.fromProto(proto.getType()),
|
||||||
proto.getUpdatedMultisigHex());
|
proto.getUpdatedMultisigHex(),
|
||||||
|
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -95,13 +103,14 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "OpenNewDisputeMessage{" +
|
return "DisputeOpenedMessage{" +
|
||||||
"\n dispute=" + dispute +
|
"\n dispute=" + dispute +
|
||||||
",\n senderNodeAddress=" + senderNodeAddress +
|
",\n senderNodeAddress=" + senderNodeAddress +
|
||||||
",\n OpenNewDisputeMessage.uid='" + uid + '\'' +
|
",\n DisputeOpenedMessage.uid='" + uid + '\'' +
|
||||||
",\n messageVersion=" + messageVersion +
|
",\n messageVersion=" + messageVersion +
|
||||||
",\n supportType=" + supportType +
|
",\n supportType=" + supportType +
|
||||||
",\n updatedMultisigHex=" + updatedMultisigHex +
|
",\n updatedMultisigHex=" + updatedMultisigHex +
|
||||||
|
",\n paymentSentMessage=" + paymentSentMessage +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.Dispute;
|
||||||
import bisq.core.support.dispute.DisputeManager;
|
import bisq.core.support.dispute.DisputeManager;
|
||||||
import bisq.core.support.dispute.DisputeResult;
|
import bisq.core.support.dispute.DisputeResult;
|
||||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.support.messages.SupportMessage;
|
import bisq.core.support.messages.SupportMessage;
|
||||||
import bisq.core.trade.ClosedTradableManager;
|
import bisq.core.trade.ClosedTradableManager;
|
||||||
|
@ -101,25 +100,18 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
log.info("Received {} with tradeId {} and uid {}",
|
log.info("Received {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
if (message instanceof OpenNewDisputeMessage) {
|
if (message instanceof DisputeOpenedMessage) {
|
||||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
|
||||||
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
|
||||||
} else if (message instanceof ChatMessage) {
|
} else if (message instanceof ChatMessage) {
|
||||||
onChatMessage((ChatMessage) message);
|
handleChatMessage((ChatMessage) message);
|
||||||
} else if (message instanceof DisputeResultMessage) {
|
} else if (message instanceof DisputeClosedMessage) {
|
||||||
onDisputeResultMessage((DisputeResultMessage) message);
|
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
|
||||||
return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AckMessageSourceType getAckMessageSourceType() {
|
protected AckMessageSourceType getAckMessageSourceType() {
|
||||||
return AckMessageSourceType.REFUND_MESSAGE;
|
return AckMessageSourceType.REFUND_MESSAGE;
|
||||||
|
@ -161,7 +153,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// We get that message at both peers. The dispute object is in context of the trader
|
// We get that message at both peers. The dispute object is in context of the trader
|
||||||
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
|
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
|
||||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||||
String tradeId = disputeResult.getTradeId();
|
String tradeId = disputeResult.getTradeId();
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
|
@ -174,7 +166,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||||
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2);
|
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
|
||||||
delayMsgMap.put(uid, timer);
|
delayMsgMap.put(uid, timer);
|
||||||
} else {
|
} else {
|
||||||
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
||||||
|
|
|
@ -154,7 +154,7 @@ public class TraderChatManager extends SupportManager {
|
||||||
log.info("Received {} with tradeId {} and uid {}",
|
log.info("Received {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||||
if (message instanceof ChatMessage) {
|
if (message instanceof ChatMessage) {
|
||||||
onChatMessage((ChatMessage) message);
|
handleChatMessage((ChatMessage) message);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,11 @@ import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.OfferPayload;
|
import bisq.core.offer.OfferPayload;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||||
import bisq.core.trade.messages.InitTradeRequest;
|
import bisq.core.trade.messages.InitTradeRequest;
|
||||||
|
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||||
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.core.util.JsonUtil;
|
import bisq.core.util.JsonUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
@ -32,9 +36,12 @@ import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of utilities.
|
* Collection of utilities.
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class HavenoUtils {
|
public class HavenoUtils {
|
||||||
|
|
||||||
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
|
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
|
||||||
|
@ -73,10 +80,10 @@ public class HavenoUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the arbitrator signature for an offer is valid.
|
* Check if the arbitrator signature is valid for an offer.
|
||||||
*
|
*
|
||||||
* @param offer is a signed offer with payload
|
* @param offer is a signed offer with payload
|
||||||
* @param arbitrator is the possible original arbitrator
|
* @param arbitrator is the original signing arbitrator
|
||||||
* @return true if the arbitrator's signature is valid for the offer
|
* @return true if the arbitrator's signature is valid for the offer
|
||||||
*/
|
*/
|
||||||
public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) {
|
public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) {
|
||||||
|
@ -92,15 +99,11 @@ public class HavenoUtils {
|
||||||
String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy);
|
String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy);
|
||||||
|
|
||||||
// verify arbitrator signature
|
// verify arbitrator signature
|
||||||
boolean isValid = true;
|
|
||||||
try {
|
try {
|
||||||
isValid = Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
|
return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
isValid = false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return result
|
|
||||||
return isValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,6 +152,71 @@ public class HavenoUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the buyer signature for a PaymentSentMessage.
|
||||||
|
*
|
||||||
|
* @param trade - the trade to verify
|
||||||
|
* @param message - signed payment sent message to verify
|
||||||
|
* @return true if the buyer's signature is valid for the message
|
||||||
|
*/
|
||||||
|
public static void verifyPaymentSentMessage(Trade trade, PaymentSentMessage message) {
|
||||||
|
|
||||||
|
// remove signature from message
|
||||||
|
byte[] signature = message.getBuyerSignature();
|
||||||
|
message.setBuyerSignature(null);
|
||||||
|
|
||||||
|
// get unsigned message as json string
|
||||||
|
String unsignedMessageAsJson = JsonUtil.objectToJson(message);
|
||||||
|
|
||||||
|
// replace signature
|
||||||
|
message.setBuyerSignature(signature);
|
||||||
|
|
||||||
|
// verify signature
|
||||||
|
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId();
|
||||||
|
try {
|
||||||
|
if (!Sig.verify(trade.getBuyer().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(errMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify trade id
|
||||||
|
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the seller signature for a PaymentReceivedMessage.
|
||||||
|
*
|
||||||
|
* @param trade - the trade to verify
|
||||||
|
* @param message - signed payment received message to verify
|
||||||
|
* @return true if the seller's signature is valid for the message
|
||||||
|
*/
|
||||||
|
public static void verifyPaymentReceivedMessage(Trade trade, PaymentReceivedMessage message) {
|
||||||
|
|
||||||
|
// remove signature from message
|
||||||
|
byte[] signature = message.getSellerSignature();
|
||||||
|
message.setSellerSignature(null);
|
||||||
|
|
||||||
|
// get unsigned message as json string
|
||||||
|
String unsignedMessageAsJson = JsonUtil.objectToJson(message);
|
||||||
|
|
||||||
|
// replace signature
|
||||||
|
message.setSellerSignature(signature);
|
||||||
|
|
||||||
|
// verify signature
|
||||||
|
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId();
|
||||||
|
try {
|
||||||
|
if (!Sig.verify(trade.getSeller().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(errMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify trade id
|
||||||
|
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||||
|
|
||||||
|
// verify buyer signature of payment sent message
|
||||||
|
verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||||
|
}
|
||||||
|
|
||||||
public static void awaitLatch(CountDownLatch latch) {
|
public static void awaitLatch(CountDownLatch latch) {
|
||||||
try {
|
try {
|
||||||
latch.await();
|
latch.await();
|
||||||
|
@ -157,13 +225,17 @@ public class HavenoUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void awaitTasks(Collection<Runnable> tasks) {
|
public static void executeTasks(Collection<Runnable> tasks) {
|
||||||
|
executeTasks(tasks, tasks.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void executeTasks(Collection<Runnable> tasks, int poolSize) {
|
||||||
if (tasks.isEmpty()) return;
|
if (tasks.isEmpty()) return;
|
||||||
ExecutorService pool = Executors.newFixedThreadPool(tasks.size());
|
ExecutorService pool = Executors.newFixedThreadPool(poolSize);
|
||||||
for (Runnable task : tasks) pool.submit(task);
|
for (Runnable task : tasks) pool.submit(task);
|
||||||
pool.shutdown();
|
pool.shutdown();
|
||||||
try {
|
try {
|
||||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
pool.shutdownNow();
|
pool.shutdownNow();
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
|
@ -73,7 +73,11 @@ public abstract class SellerTrade extends Trade {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case DISPUTE_REQUESTED:
|
case DISPUTE_REQUESTED:
|
||||||
case DISPUTE_STARTED_BY_PEER:
|
case DISPUTE_OPENED:
|
||||||
|
case ARBITRATOR_SENT_DISPUTE_CLOSED_MSG:
|
||||||
|
case ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG:
|
||||||
|
case ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG:
|
||||||
|
case ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG:
|
||||||
case DISPUTE_CLOSED:
|
case DISPUTE_CLOSED:
|
||||||
case MEDIATION_REQUESTED:
|
case MEDIATION_REQUESTED:
|
||||||
case MEDIATION_STARTED_BY_PEER:
|
case MEDIATION_STARTED_BY_PEER:
|
||||||
|
|
|
@ -35,6 +35,7 @@ import bisq.core.trade.messages.TradeMessage;
|
||||||
import bisq.core.trade.protocol.ProcessModel;
|
import bisq.core.trade.protocol.ProcessModel;
|
||||||
import bisq.core.trade.protocol.ProcessModelServiceProvider;
|
import bisq.core.trade.protocol.ProcessModelServiceProvider;
|
||||||
import bisq.core.trade.protocol.TradeListener;
|
import bisq.core.trade.protocol.TradeListener;
|
||||||
|
import bisq.core.trade.protocol.TradeProtocol;
|
||||||
import bisq.core.trade.protocol.TradingPeer;
|
import bisq.core.trade.protocol.TradingPeer;
|
||||||
import bisq.core.trade.txproof.AssetTxProofResult;
|
import bisq.core.trade.txproof.AssetTxProofResult;
|
||||||
import bisq.core.util.ParsingUtils;
|
import bisq.core.util.ParsingUtils;
|
||||||
|
@ -44,6 +45,7 @@ import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.crypto.Encryption;
|
import bisq.common.crypto.Encryption;
|
||||||
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.taskrunner.Model;
|
import bisq.common.taskrunner.Model;
|
||||||
import bisq.common.util.Utilities;
|
import bisq.common.util.Utilities;
|
||||||
|
@ -72,6 +74,7 @@ import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -214,10 +217,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PayoutState {
|
public enum PayoutState {
|
||||||
UNPUBLISHED,
|
PAYOUT_UNPUBLISHED,
|
||||||
PUBLISHED,
|
PAYOUT_PUBLISHED,
|
||||||
CONFIRMED,
|
PAYOUT_CONFIRMED,
|
||||||
UNLOCKED;
|
PAYOUT_UNLOCKED;
|
||||||
|
|
||||||
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
|
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
|
||||||
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
|
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
|
||||||
|
@ -234,9 +237,12 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
public enum DisputeState {
|
public enum DisputeState {
|
||||||
NO_DISPUTE,
|
NO_DISPUTE,
|
||||||
// arbitration
|
DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager
|
||||||
DISPUTE_REQUESTED,
|
DISPUTE_OPENED,
|
||||||
DISPUTE_STARTED_BY_PEER,
|
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
|
||||||
|
ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG,
|
||||||
|
ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG,
|
||||||
|
ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG,
|
||||||
DISPUTE_CLOSED,
|
DISPUTE_CLOSED,
|
||||||
|
|
||||||
// mediation
|
// mediation
|
||||||
|
@ -268,12 +274,12 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isArbitrated() {
|
public boolean isArbitrated() {
|
||||||
return this == Trade.DisputeState.DISPUTE_REQUESTED ||
|
if (isMediated()) return false; // TODO: remove mediation?
|
||||||
this == Trade.DisputeState.DISPUTE_STARTED_BY_PEER ||
|
return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
|
||||||
this == Trade.DisputeState.DISPUTE_CLOSED ||
|
}
|
||||||
this == Trade.DisputeState.REFUND_REQUESTED ||
|
|
||||||
this == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER ||
|
public boolean isClosed() {
|
||||||
this == Trade.DisputeState.REFUND_REQUEST_CLOSED;
|
return this == DisputeState.DISPUTE_CLOSED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +330,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
@Getter
|
@Getter
|
||||||
private State state = State.PREPARATION;
|
private State state = State.PREPARATION;
|
||||||
@Getter
|
@Getter
|
||||||
private PayoutState payoutState = PayoutState.UNPUBLISHED;
|
private PayoutState payoutState = PayoutState.PAYOUT_UNPUBLISHED;
|
||||||
@Getter
|
@Getter
|
||||||
private DisputeState disputeState = DisputeState.NO_DISPUTE;
|
private DisputeState disputeState = DisputeState.NO_DISPUTE;
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -365,11 +371,13 @@ public abstract class Trade implements Tradable, Model {
|
||||||
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
|
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
|
||||||
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
|
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
|
||||||
transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
|
transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
|
||||||
transient private Subscription tradePhaseSubscription = null;
|
transient private Subscription tradePhaseSubscription;
|
||||||
transient private Subscription payoutStateSubscription = null;
|
transient private Subscription payoutStateSubscription;
|
||||||
transient private TaskLooper tradeTxsLooper;
|
transient private TaskLooper tradeTxsLooper;
|
||||||
transient private Long lastWalletRefreshPeriod;
|
transient private Long walletRefreshPeriod;
|
||||||
|
transient private Long syncNormalStartTime;
|
||||||
private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
|
private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
|
||||||
|
public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds
|
||||||
|
|
||||||
// Mutable
|
// Mutable
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -435,8 +443,9 @@ public abstract class Trade implements Tradable, Model {
|
||||||
private String payoutTxKey;
|
private String payoutTxKey;
|
||||||
private Long startTime; // cache
|
private Long startTime; // cache
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor, initialization
|
// Constructors
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// maker
|
// maker
|
||||||
|
@ -530,96 +539,56 @@ public abstract class Trade implements Tradable, Model {
|
||||||
setAmount(tradeAmount);
|
setAmount(tradeAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// PROTO BUFFER
|
// Listeners
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
public void addListener(TradeListener listener) {
|
||||||
public Message toProtoMessage() {
|
tradeListeners.add(listener);
|
||||||
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
|
|
||||||
.setOffer(offer.toProtoMessage())
|
|
||||||
.setTxFeeAsLong(txFeeAsLong)
|
|
||||||
.setTakerFeeAsLong(takerFeeAsLong)
|
|
||||||
.setTakeOfferDate(takeOfferDate)
|
|
||||||
.setProcessModel(processModel.toProtoMessage())
|
|
||||||
.setAmountAsLong(amountAsLong)
|
|
||||||
.setPrice(price)
|
|
||||||
.setState(Trade.State.toProtoMessage(state))
|
|
||||||
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
|
|
||||||
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
|
|
||||||
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
|
|
||||||
.addAllChatMessage(chatMessages.stream()
|
|
||||||
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.setLockTime(lockTime)
|
|
||||||
.setUid(uid);
|
|
||||||
|
|
||||||
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
|
||||||
Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage()));
|
|
||||||
Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson);
|
|
||||||
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash)));
|
|
||||||
Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage);
|
|
||||||
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
|
|
||||||
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
|
|
||||||
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
|
|
||||||
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
|
||||||
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey));
|
|
||||||
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
|
||||||
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
|
|
||||||
return builder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
|
public void removeListener(TradeListener listener) {
|
||||||
trade.setTakeOfferDate(proto.getTakeOfferDate());
|
if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered");
|
||||||
trade.setState(State.fromProto(proto.getState()));
|
|
||||||
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
|
|
||||||
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
|
|
||||||
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
|
|
||||||
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
|
|
||||||
trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()));
|
|
||||||
trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey()));
|
|
||||||
trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null);
|
|
||||||
trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()));
|
|
||||||
trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()));
|
|
||||||
trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()));
|
|
||||||
trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId());
|
|
||||||
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
|
|
||||||
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
|
|
||||||
trade.setLockTime(proto.getLockTime());
|
|
||||||
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
|
||||||
|
|
||||||
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
|
|
||||||
// We do not want to show the user the last pending state when he starts up the app again, so we clear it.
|
|
||||||
if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) {
|
|
||||||
persistedAssetTxProofResult = null;
|
|
||||||
}
|
}
|
||||||
trade.setAssetTxProofResult(persistedAssetTxProofResult);
|
|
||||||
|
|
||||||
trade.chatMessages.addAll(proto.getChatMessageList().stream()
|
// notified from TradeProtocol of verified trade messages
|
||||||
.map(ChatMessage::fromPayloadProto)
|
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
|
||||||
.collect(Collectors.toList()));
|
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
|
||||||
|
listener.onVerifiedTradeMessage(message, sender);
|
||||||
return trade;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notified from TradeProtocol of ack messages
|
||||||
|
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
||||||
|
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
|
||||||
|
listener.onAckMessage(ackMessage, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void initialize(ProcessModelServiceProvider serviceProvider) {
|
public void initialize(ProcessModelServiceProvider serviceProvider) {
|
||||||
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
|
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
|
||||||
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
||||||
});
|
});
|
||||||
|
|
||||||
isInitialized = true; // TODO: move to end?
|
|
||||||
|
|
||||||
// listen to daemon connection
|
// listen to daemon connection
|
||||||
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
|
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
|
||||||
|
|
||||||
// done if payout unlocked
|
// check if done
|
||||||
if (isPayoutUnlocked()) return;
|
if (isPayoutUnlocked()) return;
|
||||||
|
|
||||||
// handle trade state events
|
// handle trade state events
|
||||||
if (isDepositPublished()) listenToTradeTxs();
|
|
||||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||||
updateTxListenerRefreshPeriod();
|
if (!isInitialized) return;
|
||||||
if (isDepositPublished()) listenToTradeTxs();
|
if (isDepositPublished() && !isPayoutUnlocked()) {
|
||||||
|
updateWalletRefreshPeriod();
|
||||||
|
listenToTradeTxs();
|
||||||
|
}
|
||||||
if (isCompleted()) {
|
if (isCompleted()) {
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
if (tradePhaseSubscription != null) {
|
if (tradePhaseSubscription != null) {
|
||||||
|
@ -632,17 +601,23 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
// handle payout state events
|
// handle payout state events
|
||||||
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
|
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
|
||||||
updateTxListenerRefreshPeriod();
|
if (!isInitialized) return;
|
||||||
|
if (isPayoutPublished()) updateWalletRefreshPeriod();
|
||||||
|
|
||||||
// cleanup when payout published
|
// cleanup when payout published
|
||||||
if (isPayoutPublished()) {
|
if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) {
|
||||||
log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
|
log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
|
||||||
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published
|
|
||||||
|
// complete disputed trade
|
||||||
|
if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) processModel.getTradeManager().closeDisputedTrade(getId(), Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
|
||||||
|
// complete arbitrator trade
|
||||||
|
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this);
|
||||||
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
|
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup when payout unlocks
|
// cleanup when payout unlocks
|
||||||
if (isPayoutUnlocked()) {
|
if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) {
|
||||||
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
|
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
|
||||||
deleteWallet();
|
deleteWallet();
|
||||||
if (tradeTxsLooper != null) {
|
if (tradeTxsLooper != null) {
|
||||||
|
@ -657,12 +632,24 @@ public abstract class Trade implements Tradable, Model {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
// start listening to trade wallet
|
||||||
|
if (isDepositPublished()) {
|
||||||
|
updateWalletRefreshPeriod();
|
||||||
|
listenToTradeTxs();
|
||||||
|
|
||||||
|
// allow state notifications to process before returning
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
UserThread.execute(() -> latch.countDown());
|
||||||
|
HavenoUtils.awaitLatch(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeProtocol getProtocol() {
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
return processModel.getTradeManager().getTradeProtocol(this);
|
||||||
// API
|
}
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public void setMyNodeAddress() {
|
public void setMyNodeAddress() {
|
||||||
getSelf().setNodeAddress(P2PService.getMyNodeAddress());
|
getSelf().setNodeAddress(P2PService.getMyNodeAddress());
|
||||||
|
@ -755,9 +742,11 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// exception expected
|
// exception expected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
|
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
|
||||||
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
log.info("Payout transaction generated on attempt {}", numAttempts);
|
||||||
|
|
||||||
|
// save updated multisig hex
|
||||||
|
getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -827,7 +816,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// submit payout tx
|
// submit payout tx
|
||||||
if (publish) {
|
if (publish) {
|
||||||
multisigWallet.submitMultisigTxHex(payoutTxHex);
|
multisigWallet.submitMultisigTxHex(payoutTxHex);
|
||||||
setPayoutState(Trade.PayoutState.PUBLISHED);
|
setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -921,10 +910,22 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncWallet() {
|
public void syncWallet() {
|
||||||
|
if (getWallet() == null) {
|
||||||
|
log.warn("Cannot sync multisig wallet because it doesn't exist for {}, {}", getClass().getSimpleName(), getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
getWallet().sync();
|
getWallet().sync();
|
||||||
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
|
||||||
pollWallet();
|
pollWallet();
|
||||||
|
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
||||||
|
syncNormalStartTime = System.currentTimeMillis();
|
||||||
|
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
|
||||||
|
}, syncNormalDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveWallet() {
|
public void saveWallet() {
|
||||||
|
@ -938,6 +939,12 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
|
if (tradeTxsLooper != null) {
|
||||||
|
tradeTxsLooper.stop();
|
||||||
|
tradeTxsLooper = null;
|
||||||
|
}
|
||||||
|
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
||||||
|
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -958,32 +965,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
public abstract boolean confirmPermitted();
|
public abstract boolean confirmPermitted();
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Listeners
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public void addListener(TradeListener listener) {
|
|
||||||
tradeListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeListener(TradeListener listener) {
|
|
||||||
if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// notified from TradeProtocol of verified trade messages
|
|
||||||
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
|
|
||||||
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
|
|
||||||
listener.onVerifiedTradeMessage(message, sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notified from TradeProtocol of ack messages
|
|
||||||
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
|
||||||
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
|
|
||||||
listener.onAckMessage(ackMessage, sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Setters
|
// Setters
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1031,7 +1012,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
public void setPayoutState(PayoutState payoutState) {
|
public void setPayoutState(PayoutState payoutState) {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
// We don't want to log at startup the setState calls from all persisted trades
|
// We don't want to log at startup the setState calls from all persisted trades
|
||||||
log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState);
|
log.info("Set new payout state for {} {}: {}", this.getClass().getSimpleName(), getId(), payoutState);
|
||||||
}
|
}
|
||||||
if (payoutState.ordinal() < this.payoutState.ordinal()) {
|
if (payoutState.ordinal() < this.payoutState.ordinal()) {
|
||||||
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
|
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
|
||||||
|
@ -1046,8 +1027,24 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDisputeState(DisputeState disputeState) {
|
public void setDisputeState(DisputeState disputeState) {
|
||||||
|
if (isInitialized) {
|
||||||
|
// We don't want to log at startup the setState calls from all persisted trades
|
||||||
|
log.info("Set new dispute state for {} {}: {}", this.getClass().getSimpleName(), getShortId(), disputeState);
|
||||||
|
}
|
||||||
|
if (disputeState.ordinal() < this.disputeState.ordinal()) {
|
||||||
|
String message = "We got a dispute state change to a previous state (id=" + getShortId() + ").\n" +
|
||||||
|
"Old dispute state is: " + this.disputeState + ". New dispute state is: " + disputeState;
|
||||||
|
log.warn(message);
|
||||||
|
}
|
||||||
|
|
||||||
this.disputeState = disputeState;
|
this.disputeState = disputeState;
|
||||||
|
UserThread.execute(() -> {
|
||||||
disputeStateProperty.set(disputeState);
|
disputeStateProperty.set(disputeState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisputeStateIfProgress(DisputeState disputeState) {
|
||||||
|
if (disputeState.ordinal() > getDisputeState().ordinal()) setDisputeState(disputeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMediationResultState(MediationResultState mediationResultState) {
|
public void setMediationResultState(MediationResultState mediationResultState) {
|
||||||
|
@ -1140,11 +1137,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker();
|
return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// get the taker if maker, maker if taker, null if arbitrator
|
||||||
* Get the taker if maker, maker if taker, null if arbitrator.
|
|
||||||
*
|
|
||||||
* @return the trade peer
|
|
||||||
*/
|
|
||||||
public TradingPeer getTradingPeer() {
|
public TradingPeer getTradingPeer() {
|
||||||
if (this instanceof MakerTrade) return processModel.getTaker();
|
if (this instanceof MakerTrade) return processModel.getTaker();
|
||||||
else if (this instanceof TakerTrade) return processModel.getMaker();
|
else if (this instanceof TakerTrade) return processModel.getMaker();
|
||||||
|
@ -1152,19 +1145,19 @@ public abstract class Trade implements Tradable, Model {
|
||||||
else throw new RuntimeException("Unknown trade type: " + getClass().getName());
|
else throw new RuntimeException("Unknown trade type: " + getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// TODO (woodser): this naming convention is confusing
|
||||||
* Get the peer with the given address which can be self.
|
|
||||||
*
|
|
||||||
* TODO (woodser): this naming convention is confusing
|
|
||||||
*
|
|
||||||
* @param address is the address of the peer to get
|
|
||||||
* @return the trade peer
|
|
||||||
*/
|
|
||||||
public TradingPeer getTradingPeer(NodeAddress address) {
|
public TradingPeer getTradingPeer(NodeAddress address) {
|
||||||
if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker();
|
if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker();
|
||||||
if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker();
|
if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker();
|
||||||
if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator();
|
if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator();
|
||||||
throw new RuntimeException("No trade participant with the given address. Their address might have changed: " + address);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TradingPeer getTradingPeer(PubKeyRing pubKeyRing) {
|
||||||
|
if (getMaker() != null && getMaker().getPubKeyRing().equals(pubKeyRing)) return getMaker();
|
||||||
|
if (getTaker() != null && getTaker().getPubKeyRing().equals(pubKeyRing)) return getTaker();
|
||||||
|
if (getArbitrator() != null && getArbitrator().getPubKeyRing().equals(pubKeyRing)) return getArbitrator();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getTakeOfferDate() {
|
public Date getTakeOfferDate() {
|
||||||
|
@ -1210,12 +1203,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
private long getStartTime() {
|
private long getStartTime() {
|
||||||
if (startTime != null) return startTime;
|
if (startTime != null) return startTime;
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
final MoneroTx takerDepositTx = getTakerDepositTx();
|
if (isDepositConfirmed() && getTakeOfferDate() != null) {
|
||||||
final MoneroTx makerDepositTx = getMakerDepositTx();
|
|
||||||
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
|
|
||||||
if (isDepositUnlocked()) {
|
if (isDepositUnlocked()) {
|
||||||
final long tradeTime = getTakeOfferDate().getTime();
|
final long tradeTime = getTakeOfferDate().getTime();
|
||||||
long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight());
|
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||||
|
|
||||||
|
@ -1233,7 +1224,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
|
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
|
||||||
new Date(startTime), new Date(tradeTime), new Date(blockTime));
|
new Date(startTime), new Date(tradeTime), new Date(blockTime));
|
||||||
} else {
|
} else {
|
||||||
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", makerDepositTx.getHash(), takerDepositTx.getHash());
|
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash());
|
||||||
startTime = now;
|
startTime = now;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1259,34 +1250,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFundsLockedIn() {
|
public boolean isFundsLockedIn() {
|
||||||
// If no deposit tx was published we have no funds locked in
|
return isDepositPublished() && !isPayoutPublished();
|
||||||
if (!isDepositPublished()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have the payout tx published (non disputed case) we have no funds locked in. Here we might have more
|
|
||||||
// complex cases where users open a mediation but continue the trade to finalize it without mediated payout.
|
|
||||||
// The trade state handles that but does not handle mediated payouts or refund agents payouts.
|
|
||||||
if (isPayoutPublished()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for closed disputed case
|
|
||||||
if (disputeState == DisputeState.DISPUTE_CLOSED) return false;
|
|
||||||
|
|
||||||
// In mediation case we check for the mediationResultState. As there are multiple sub-states we use ordinal.
|
|
||||||
if (disputeState == DisputeState.MEDIATION_CLOSED) {
|
|
||||||
if (mediationResultState != null &&
|
|
||||||
mediationResultState.ordinal() >= MediationResultState.PAYOUT_TX_PUBLISHED.ordinal()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as
|
|
||||||
// locked in funds.
|
|
||||||
return disputeState != DisputeState.REFUND_REQUESTED &&
|
|
||||||
disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER &&
|
|
||||||
disputeState != DisputeState.REFUND_REQUEST_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositConfirmed() {
|
public boolean isDepositConfirmed() {
|
||||||
|
@ -1310,15 +1274,15 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPayoutPublished() {
|
public boolean isPayoutPublished() {
|
||||||
return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal();
|
return getPayoutState().ordinal() >= PayoutState.PAYOUT_PUBLISHED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPayoutConfirmed() {
|
public boolean isPayoutConfirmed() {
|
||||||
return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal();
|
return getPayoutState().ordinal() >= PayoutState.PAYOUT_CONFIRMED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPayoutUnlocked() {
|
public boolean isPayoutUnlocked() {
|
||||||
return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal();
|
return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||||
|
@ -1439,17 +1403,12 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
// poll wallet for tx state
|
// poll wallet for tx state
|
||||||
pollWallet();
|
pollWallet();
|
||||||
tradeTxsLooper = new TaskLooper(() -> {
|
tradeTxsLooper = new TaskLooper(() -> { pollWallet(); });
|
||||||
try {
|
tradeTxsLooper.start(walletRefreshPeriod);
|
||||||
pollWallet();
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tradeTxsLooper.start(getWalletRefreshPeriod());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pollWallet() {
|
private void pollWallet() {
|
||||||
|
try {
|
||||||
|
|
||||||
// skip if payout unlocked
|
// skip if payout unlocked
|
||||||
if (isPayoutUnlocked()) return;
|
if (isPayoutUnlocked()) return;
|
||||||
|
@ -1498,21 +1457,27 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
|
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (isInitialized) log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); // TODO (monero-java): poller.isPolling() and then don't need to use isInitialized here as shutdown flag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||||
|
if (getWallet() == null) return;
|
||||||
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
|
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId());
|
getWallet().setDaemonConnection(connection);
|
||||||
multisigWallet.setDaemonConnection(connection);
|
updateWalletRefreshPeriod();
|
||||||
multisigWallet.startSyncing(getWalletRefreshPeriod());
|
|
||||||
updateTxListenerRefreshPeriod();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTxListenerRefreshPeriod() {
|
private void updateWalletRefreshPeriod() {
|
||||||
long walletRefreshPeriod = getWalletRefreshPeriod();
|
setWalletRefreshPeriod(getWalletRefreshPeriod());
|
||||||
if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return;
|
}
|
||||||
log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod);
|
|
||||||
lastWalletRefreshPeriod = walletRefreshPeriod;
|
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
||||||
|
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
||||||
|
log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod);
|
||||||
|
this.walletRefreshPeriod = walletRefreshPeriod;
|
||||||
|
getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period
|
||||||
if (tradeTxsLooper != null) {
|
if (tradeTxsLooper != null) {
|
||||||
tradeTxsLooper.stop();
|
tradeTxsLooper.stop();
|
||||||
tradeTxsLooper = null;
|
tradeTxsLooper = null;
|
||||||
|
@ -1521,8 +1486,8 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getWalletRefreshPeriod() {
|
private long getWalletRefreshPeriod() {
|
||||||
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup
|
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // slow arbitrator trade wallet after deposits confirm
|
||||||
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate
|
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // else sync at default rate
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStateDepositsPublished() {
|
private void setStateDepositsPublished() {
|
||||||
|
@ -1538,15 +1503,87 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPayoutStatePublished() {
|
private void setPayoutStatePublished() {
|
||||||
if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED);
|
if (!isPayoutPublished()) setPayoutState(PayoutState.PAYOUT_PUBLISHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPayoutStateConfirmed() {
|
private void setPayoutStateConfirmed() {
|
||||||
if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED);
|
if (!isPayoutConfirmed()) setPayoutState(PayoutState.PAYOUT_CONFIRMED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPayoutStateUnlocked() {
|
private void setPayoutStateUnlocked() {
|
||||||
if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED);
|
if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message toProtoMessage() {
|
||||||
|
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
|
||||||
|
.setOffer(offer.toProtoMessage())
|
||||||
|
.setTxFeeAsLong(txFeeAsLong)
|
||||||
|
.setTakerFeeAsLong(takerFeeAsLong)
|
||||||
|
.setTakeOfferDate(takeOfferDate)
|
||||||
|
.setProcessModel(processModel.toProtoMessage())
|
||||||
|
.setAmountAsLong(amountAsLong)
|
||||||
|
.setPrice(price)
|
||||||
|
.setState(Trade.State.toProtoMessage(state))
|
||||||
|
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
|
||||||
|
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
|
||||||
|
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
|
||||||
|
.addAllChatMessage(chatMessages.stream()
|
||||||
|
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.setLockTime(lockTime)
|
||||||
|
.setUid(uid);
|
||||||
|
|
||||||
|
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
||||||
|
Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage()));
|
||||||
|
Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson);
|
||||||
|
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash)));
|
||||||
|
Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage);
|
||||||
|
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
|
||||||
|
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
|
||||||
|
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
|
||||||
|
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
||||||
|
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey));
|
||||||
|
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
||||||
|
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
|
||||||
|
trade.setTakeOfferDate(proto.getTakeOfferDate());
|
||||||
|
trade.setState(State.fromProto(proto.getState()));
|
||||||
|
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
|
||||||
|
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
|
||||||
|
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
|
||||||
|
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
|
||||||
|
trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()));
|
||||||
|
trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey()));
|
||||||
|
trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null);
|
||||||
|
trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()));
|
||||||
|
trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()));
|
||||||
|
trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()));
|
||||||
|
trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId());
|
||||||
|
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
|
||||||
|
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
|
||||||
|
trade.setLockTime(proto.getLockTime());
|
||||||
|
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
||||||
|
|
||||||
|
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
|
||||||
|
// We do not want to show the user the last pending state when he starts up the app again, so we clear it.
|
||||||
|
if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) {
|
||||||
|
persistedAssetTxProofResult = null;
|
||||||
|
}
|
||||||
|
trade.setAssetTxProofResult(persistedAssetTxProofResult);
|
||||||
|
|
||||||
|
trade.chatMessages.addAll(proto.getChatMessageList().stream()
|
||||||
|
.map(ChatMessage::fromPayloadProto)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
|
return trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -34,6 +34,7 @@ import bisq.core.provider.price.PriceFeedService;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||||
|
import bisq.core.trade.Trade.DisputeState;
|
||||||
import bisq.core.trade.Trade.Phase;
|
import bisq.core.trade.Trade.Phase;
|
||||||
import bisq.core.trade.failed.FailedTradesManager;
|
import bisq.core.trade.failed.FailedTradesManager;
|
||||||
import bisq.core.trade.handlers.TradeResultHandler;
|
import bisq.core.trade.handlers.TradeResultHandler;
|
||||||
|
@ -75,7 +76,6 @@ import bisq.common.proto.persistable.PersistedDataHost;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.LongProperty;
|
import javafx.beans.property.LongProperty;
|
||||||
|
@ -96,9 +96,6 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -252,7 +249,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
public void onAllServicesInitialized() {
|
public void onAllServicesInitialized() {
|
||||||
if (p2PService.isBootstrapped()) {
|
if (p2PService.isBootstrapped()) {
|
||||||
initPersistedTrades();
|
new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread
|
||||||
} else {
|
} else {
|
||||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -266,12 +263,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
onTradesChanged();
|
onTradesChanged();
|
||||||
|
|
||||||
xmrWalletService.setTradeManager(this);
|
xmrWalletService.setTradeManager(this);
|
||||||
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
|
||||||
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
|
||||||
.forEach(addressEntry -> {
|
|
||||||
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
|
||||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
|
||||||
});
|
|
||||||
|
|
||||||
// thaw unreserved outputs
|
// thaw unreserved outputs
|
||||||
thawUnreservedOutputs();
|
thawUnreservedOutputs();
|
||||||
|
@ -292,9 +283,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
trade.shutDown();
|
trade.shutDown();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
|
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
HavenoUtils.awaitTasks(tasks);
|
HavenoUtils.executeTasks(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void thawUnreservedOutputs() {
|
private void thawUnreservedOutputs() {
|
||||||
|
@ -346,35 +338,41 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
private void initPersistedTrades() {
|
private void initPersistedTrades() {
|
||||||
|
|
||||||
|
// get all trades // TODO: getAllTrades()
|
||||||
|
List<Trade> trades = new ArrayList<Trade>();
|
||||||
|
trades.addAll(tradableList.getList());
|
||||||
|
trades.addAll(closedTradableManager.getClosedTrades());
|
||||||
|
trades.addAll(failedTradesManager.getObservableList());
|
||||||
|
|
||||||
// open trades in parallel since each may open a multisig wallet
|
// open trades in parallel since each may open a multisig wallet
|
||||||
List<Trade> trades = tradableList.getList();
|
int threadPoolSize = 10;
|
||||||
if (!trades.isEmpty()) {
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, trades.size()));
|
|
||||||
for (Trade trade : trades) {
|
for (Trade trade : trades) {
|
||||||
pool.submit(new Runnable() {
|
tasks.add(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
initPersistedTrade(trade);
|
initPersistedTrade(trade);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
pool.shutdown();
|
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
||||||
try {
|
|
||||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
// reset any available address entries
|
||||||
} catch (InterruptedException e) {
|
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
||||||
pool.shutdownNow();
|
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
||||||
throw new RuntimeException(e);
|
.forEach(addressEntry -> {
|
||||||
}
|
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
||||||
}
|
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||||
|
});
|
||||||
|
|
||||||
persistedTradesInitialized.set(true);
|
persistedTradesInitialized.set(true);
|
||||||
|
|
||||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
||||||
Set<Trade> allTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
||||||
allTrades.addAll(tradableList.getList());
|
nonFailedTrades.addAll(tradableList.getList());
|
||||||
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
||||||
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
||||||
tradeStatisticsManager.maybeRepublishTradeStatistics(allTrades, referralId, isTorNetworkNode);
|
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPersistedTrade(Trade trade) {
|
private void initPersistedTrade(Trade trade) {
|
||||||
|
@ -485,6 +483,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||||
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
|
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
@ -568,8 +567,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||||
log.warn("Maker error during trade initialization: " + errorMessage);
|
log.warn("Maker error during trade initialization: " + errorMessage);
|
||||||
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
|
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
|
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
|
||||||
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -589,7 +588,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId() + " at node " + P2PService.getMyNodeAddress());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
|
@ -751,8 +750,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}, errorMessage -> {
|
}, errorMessage -> {
|
||||||
log.warn("Taker error during trade initialization: " + errorMessage);
|
log.warn("Taker error during trade initialization: " + errorMessage);
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
@ -804,6 +804,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
|
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
|
||||||
public void onTradeCompleted(Trade trade) {
|
public void onTradeCompleted(Trade trade) {
|
||||||
|
if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed");
|
||||||
closedTradableManager.add(trade);
|
closedTradableManager.add(trade);
|
||||||
trade.setState(Trade.State.TRADE_COMPLETED);
|
trade.setState(Trade.State.TRADE_COMPLETED);
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
|
@ -818,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
// Dispute
|
// Dispute
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) {
|
public void closeDisputedTrade(String tradeId, DisputeState disputeState) {
|
||||||
Optional<Trade> tradeOptional = getOpenTrade(tradeId);
|
Optional<Trade> tradeOptional = getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
|
@ -911,9 +912,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
|
||||||
} else {
|
} else {
|
||||||
log.warn("We found a closed trade with locked up funds. " +
|
log.warn("We found a closed trade with locked up funds. " +
|
||||||
"That should never happen. trade ID=" + trade.getId());
|
"That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -923,9 +925,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
||||||
} else {
|
} else {
|
||||||
log.warn("We found a closed trade with locked up funds. " +
|
log.warn("We found a closed trade with locked up funds. " +
|
||||||
"That should never happen. trade ID=" + trade.getId());
|
"That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||||
}
|
}
|
||||||
return trade.getId();
|
return trade.getId();
|
||||||
|
@ -1026,7 +1029,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void removeTrade(Trade trade) {
|
private synchronized void removeTrade(Trade trade) {
|
||||||
log.info("TradeManager.removeTrade()");
|
log.info("TradeManager.removeTrade() " + trade.getId());
|
||||||
synchronized(tradableList) {
|
synchronized(tradableList) {
|
||||||
if (!tradableList.contains(trade)) return;
|
if (!tradableList.contains(trade)) return;
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package bisq.core.trade.messages;
|
||||||
|
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
|
|
||||||
import bisq.network.p2p.DirectMessage;
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -33,7 +32,7 @@ import lombok.Value;
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Value
|
@Value
|
||||||
public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage {
|
public final class DepositsConfirmedMessage extends TradeMailboxMessage {
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
private final PubKeyRing pubKeyRing;
|
private final PubKeyRing pubKeyRing;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
@ -29,14 +29,18 @@ import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Value
|
@Getter
|
||||||
public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -44,7 +48,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String signedPayoutTxHex;
|
private final String signedPayoutTxHex;
|
||||||
private final String updatedMultisigHex;
|
private final String updatedMultisigHex;
|
||||||
private final boolean sawArrivedPaymentReceivedMsg;
|
private final boolean deferPublishPayout;
|
||||||
|
private final PaymentSentMessage paymentSentMessage;
|
||||||
|
@Setter
|
||||||
|
@Nullable
|
||||||
|
private byte[] sellerSignature;
|
||||||
|
|
||||||
// Added in v1.4.0
|
// Added in v1.4.0
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -56,7 +64,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
String unsignedPayoutTxHex,
|
String unsignedPayoutTxHex,
|
||||||
String signedPayoutTxHex,
|
String signedPayoutTxHex,
|
||||||
String updatedMultisigHex,
|
String updatedMultisigHex,
|
||||||
boolean sawArrivedPaymentReceivedMsg) {
|
boolean deferPublishPayout,
|
||||||
|
PaymentSentMessage paymentSentMessage) {
|
||||||
this(tradeId,
|
this(tradeId,
|
||||||
senderNodeAddress,
|
senderNodeAddress,
|
||||||
signedWitness,
|
signedWitness,
|
||||||
|
@ -65,7 +74,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
unsignedPayoutTxHex,
|
unsignedPayoutTxHex,
|
||||||
signedPayoutTxHex,
|
signedPayoutTxHex,
|
||||||
updatedMultisigHex,
|
updatedMultisigHex,
|
||||||
sawArrivedPaymentReceivedMsg);
|
deferPublishPayout,
|
||||||
|
paymentSentMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,14 +91,16 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
String unsignedPayoutTxHex,
|
String unsignedPayoutTxHex,
|
||||||
String signedPayoutTxHex,
|
String signedPayoutTxHex,
|
||||||
String updatedMultisigHex,
|
String updatedMultisigHex,
|
||||||
boolean sawArrivedPaymentReceivedMsg) {
|
boolean deferPublishPayout,
|
||||||
|
PaymentSentMessage paymentSentMessage) {
|
||||||
super(messageVersion, tradeId, uid);
|
super(messageVersion, tradeId, uid);
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
this.signedWitness = signedWitness;
|
this.signedWitness = signedWitness;
|
||||||
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
|
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
|
||||||
this.signedPayoutTxHex = signedPayoutTxHex;
|
this.signedPayoutTxHex = signedPayoutTxHex;
|
||||||
this.updatedMultisigHex = updatedMultisigHex;
|
this.updatedMultisigHex = updatedMultisigHex;
|
||||||
this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg;
|
this.deferPublishPayout = deferPublishPayout;
|
||||||
|
this.paymentSentMessage = paymentSentMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -97,11 +109,13 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
.setTradeId(tradeId)
|
.setTradeId(tradeId)
|
||||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||||
.setUid(uid)
|
.setUid(uid)
|
||||||
.setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg);
|
.setDeferPublishPayout(deferPublishPayout);
|
||||||
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
|
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
|
||||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||||
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
|
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
|
||||||
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
|
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
|
||||||
|
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||||
|
Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e)));
|
||||||
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +126,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
|
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
|
||||||
SignedWitness.fromProto(protoSignedWitness) :
|
SignedWitness.fromProto(protoSignedWitness) :
|
||||||
null;
|
null;
|
||||||
return new PaymentReceivedMessage(proto.getTradeId(),
|
PaymentReceivedMessage message = new PaymentReceivedMessage(proto.getTradeId(),
|
||||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||||
signedWitness,
|
signedWitness,
|
||||||
proto.getUid(),
|
proto.getUid(),
|
||||||
|
@ -120,18 +134,23 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
||||||
proto.getSawArrivedPaymentReceivedMsg());
|
proto.getDeferPublishPayout(),
|
||||||
|
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
|
||||||
|
message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature()));
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "SellerReceivedPaymentMessage{" +
|
return "PaymentReceivedMessage{" +
|
||||||
"\n senderNodeAddress=" + senderNodeAddress +
|
"\n senderNodeAddress=" + senderNodeAddress +
|
||||||
",\n signedWitness=" + signedWitness +
|
",\n signedWitness=" + signedWitness +
|
||||||
",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
|
",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
|
||||||
",\n signedPayoutTxHex=" + signedPayoutTxHex +
|
",\n signedPayoutTxHex=" + signedPayoutTxHex +
|
||||||
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
|
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
|
||||||
",\n sawArrivedPaymentReceivedMsg=" + sawArrivedPaymentReceivedMsg +
|
",\n deferPublishPayout=" + deferPublishPayout +
|
||||||
|
",\n paymentSentMessage=" + paymentSentMessage +
|
||||||
|
",\n sellerSignature=" + sellerSignature +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,13 @@ import bisq.common.proto.ProtoUtil;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Value
|
@Getter
|
||||||
public final class PaymentSentMessage extends TradeMailboxMessage {
|
public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -41,6 +42,9 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||||
private final String updatedMultisigHex;
|
private final String updatedMultisigHex;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final byte[] paymentAccountKey;
|
private final byte[] paymentAccountKey;
|
||||||
|
@Setter
|
||||||
|
@Nullable
|
||||||
|
private byte[] buyerSignature;
|
||||||
|
|
||||||
// Added after v1.3.7
|
// Added after v1.3.7
|
||||||
// We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets.
|
// We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets.
|
||||||
|
@ -101,13 +105,14 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||||
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
||||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||||
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
|
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
|
||||||
|
Optional.ofNullable(buyerSignature).ifPresent(e -> builder.setBuyerSignature(ByteString.copyFrom(e)));
|
||||||
|
|
||||||
return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
|
return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
|
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
|
||||||
String messageVersion) {
|
String messageVersion) {
|
||||||
return new PaymentSentMessage(proto.getTradeId(),
|
PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(),
|
||||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
|
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
|
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
|
||||||
|
@ -117,6 +122,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
||||||
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())
|
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())
|
||||||
);
|
);
|
||||||
|
message.setBuyerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getBuyerSignature()));
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,6 +137,7 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||||
",\n payoutTxHex=" + payoutTxHex +
|
",\n payoutTxHex=" + payoutTxHex +
|
||||||
",\n updatedMultisigHex=" + updatedMultisigHex +
|
",\n updatedMultisigHex=" + updatedMultisigHex +
|
||||||
",\n paymentAccountKey=" + paymentAccountKey +
|
",\n paymentAccountKey=" + paymentAccountKey +
|
||||||
|
",\n buyerSignature=" + buyerSignature +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
|
||||||
return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class };
|
return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,11 @@ package bisq.core.trade.protocol;
|
||||||
|
|
||||||
import bisq.core.trade.BuyerAsMakerTrade;
|
import bisq.core.trade.BuyerAsMakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
|
||||||
import bisq.core.trade.messages.InitMultisigRequest;
|
|
||||||
import bisq.core.trade.messages.InitTradeRequest;
|
import bisq.core.trade.messages.InitTradeRequest;
|
||||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
|
||||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
|
||||||
import bisq.core.trade.messages.SignContractRequest;
|
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
|
||||||
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
|
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
|
||||||
import bisq.common.taskrunner.Task;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -45,10 +37,6 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
||||||
super(trade);
|
super(trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// MakerProtocol
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitTradeRequest(InitTradeRequest message,
|
public void handleInitTradeRequest(InitTradeRequest message,
|
||||||
NodeAddress peer,
|
NodeAddress peer,
|
||||||
|
@ -80,49 +68,4 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
|
||||||
super.handleInitMultisigRequest(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
|
||||||
super.handleSignContractRequest(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
|
||||||
super.handleSignContractResponse(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
|
||||||
super.handleDepositResponse(response, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
|
|
||||||
super.handle(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// User interaction
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which events we expect
|
|
||||||
@Override
|
|
||||||
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
super.onPaymentStarted(resultHandler, errorMessageHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Incoming message Payout tx
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which messages we expect
|
|
||||||
@Override
|
|
||||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
|
||||||
super.handle(message, peer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,60 +92,6 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
|
||||||
super.handleInitMultisigRequest(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
|
||||||
super.handleSignContractRequest(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
|
||||||
super.handleSignContractResponse(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
|
||||||
super.handleDepositResponse(response, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
|
|
||||||
super.handle(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// User interaction
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which events we expect
|
|
||||||
@Override
|
|
||||||
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
super.onPaymentStarted(resultHandler, errorMessageHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Incoming message Payout tx
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which messages we expect
|
|
||||||
@Override
|
|
||||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
|
||||||
super.handle(message, peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Message dispatcher
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
|
||||||
super.onTradeMessage(message, peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleError(String errorMessage) {
|
protected void handleError(String errorMessage) {
|
||||||
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());
|
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());
|
||||||
|
|
|
@ -25,7 +25,8 @@ import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
import bisq.core.trade.messages.TradeMessage;
|
||||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
|
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
|
||||||
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
|
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator;
|
||||||
|
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller;
|
||||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
|
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
|
||||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
@ -58,7 +59,9 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||||
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
||||||
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
|
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
|
||||||
.with(BuyerEvent.STARTUP))
|
.with(BuyerEvent.STARTUP))
|
||||||
.setup(tasks(BuyerSendPaymentSentMessage.class))
|
.setup(tasks(
|
||||||
|
BuyerSendPaymentSentMessageToSeller.class,
|
||||||
|
BuyerSendPaymentSentMessageToArbitrator.class))
|
||||||
.executeTasks();
|
.executeTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +96,9 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||||
.with(event)
|
.with(event)
|
||||||
.preCondition(trade.confirmPermitted()))
|
.preCondition(trade.confirmPermitted()))
|
||||||
.setup(tasks(ApplyFilter.class,
|
.setup(tasks(ApplyFilter.class,
|
||||||
//UpdateMultisigWithTradingPeer.class, // TODO (woodser): can use this to test protocol with updated multisig from peer. peer should attempt to send updated multisig hex earlier as part of protocol. cannot use with countdown latch because response comes back in a separate thread and blocks on trade
|
|
||||||
BuyerPreparePaymentSentMessage.class,
|
BuyerPreparePaymentSentMessage.class,
|
||||||
//BuyerSetupPayoutTxListener.class,
|
BuyerSendPaymentSentMessageToSeller.class,
|
||||||
BuyerSendPaymentSentMessage.class) // don't latch trade because this blocks and runs in background
|
BuyerSendPaymentSentMessageToArbitrator.class)
|
||||||
.using(new TradeTaskRunner(trade,
|
.using(new TradeTaskRunner(trade,
|
||||||
() -> {
|
() -> {
|
||||||
this.errorMessageHandler = null;
|
this.errorMessageHandler = null;
|
||||||
|
@ -119,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
|
||||||
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
|
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,18 +20,11 @@ package bisq.core.trade.protocol;
|
||||||
|
|
||||||
import bisq.core.trade.SellerAsMakerTrade;
|
import bisq.core.trade.SellerAsMakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
|
||||||
import bisq.core.trade.messages.SignContractRequest;
|
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
|
||||||
import bisq.core.trade.messages.InitMultisigRequest;
|
|
||||||
import bisq.core.trade.messages.InitTradeRequest;
|
import bisq.core.trade.messages.InitTradeRequest;
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
|
||||||
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
|
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -81,53 +74,4 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
|
||||||
super.handleInitMultisigRequest(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
|
||||||
super.handleSignContractRequest(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
|
||||||
super.handleSignContractResponse(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
|
||||||
super.handleDepositResponse(response, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// User interaction
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which events we expect
|
|
||||||
@Override
|
|
||||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
super.onPaymentReceived(resultHandler, errorMessageHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Massage dispatcher
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
|
||||||
super.onTradeMessage(message, peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Incoming message when buyer has clicked payment started button
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which messages we expect
|
|
||||||
@Override
|
|
||||||
protected void handle(PaymentSentMessage message, NodeAddress peer) {
|
|
||||||
super.handle(message, peer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,18 +22,10 @@ import bisq.core.offer.Offer;
|
||||||
import bisq.core.trade.SellerAsTakerTrade;
|
import bisq.core.trade.SellerAsTakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.handlers.TradeResultHandler;
|
import bisq.core.trade.handlers.TradeResultHandler;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
|
||||||
import bisq.core.trade.messages.SignContractRequest;
|
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
|
||||||
import bisq.core.trade.messages.InitMultisigRequest;
|
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
|
||||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds;
|
import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds;
|
||||||
import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
|
import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
|
||||||
import bisq.network.p2p.NodeAddress;
|
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -90,55 +82,6 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
|
||||||
super.handleInitMultisigRequest(request, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
|
||||||
super.handleSignContractRequest(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
|
||||||
super.handleSignContractResponse(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
|
||||||
super.handleDepositResponse(response, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Incoming message when buyer has clicked payment started button
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which messages we expect
|
|
||||||
@Override
|
|
||||||
protected void handle(PaymentSentMessage message, NodeAddress peer) {
|
|
||||||
super.handle(message, peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// User interaction
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// We keep the handler here in as well to make it more transparent which events we expect
|
|
||||||
@Override
|
|
||||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
super.onPaymentReceived(resultHandler, errorMessageHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Massage dispatcher
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
|
||||||
super.onTradeMessage(message, peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleError(String errorMessage) {
|
protected void handleError(String errorMessage) {
|
||||||
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());
|
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());
|
||||||
|
|
|
@ -24,10 +24,10 @@ import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
import bisq.core.trade.messages.TradeMessage;
|
||||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
||||||
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
|
|
||||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator;
|
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator;
|
||||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
|
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
|
||||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
||||||
|
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
|
||||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
|
@ -54,18 +54,11 @@ public class SellerProtocol extends DisputeProtocol {
|
||||||
@Override
|
@Override
|
||||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
||||||
super.onTradeMessage(message, peer);
|
super.onTradeMessage(message, peer);
|
||||||
if (message instanceof PaymentSentMessage) {
|
|
||||||
handle((PaymentSentMessage) message, peer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
||||||
super.onMailboxMessage(message, peerNodeAddress);
|
super.onMailboxMessage(message, peerNodeAddress);
|
||||||
|
|
||||||
if (message instanceof PaymentSentMessage) {
|
|
||||||
handle((PaymentSentMessage) message, peerNodeAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -74,52 +67,6 @@ public class SellerProtocol extends DisputeProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Incoming message when buyer has clicked payment started button
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
protected void handle(PaymentSentMessage message, NodeAddress peer) {
|
|
||||||
log.info("SellerProtocol.handle(PaymentSentMessage)");
|
|
||||||
new Thread(() -> {
|
|
||||||
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
|
|
||||||
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
|
|
||||||
// a mailbox message with PaymentSentMessage.
|
|
||||||
// TODO A better fix would be to add a listener for the wallet sync state and process
|
|
||||||
// the mailbox msg once wallet is ready and trade state set.
|
|
||||||
synchronized (trade) {
|
|
||||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
|
||||||
log.warn("Ignoring PaymentSentMessage which was already processed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
latchTrade();
|
|
||||||
expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
|
|
||||||
.with(message)
|
|
||||||
.from(peer)
|
|
||||||
.preCondition(trade.getPayoutTx() == null,
|
|
||||||
() -> {
|
|
||||||
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
|
|
||||||
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
|
||||||
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
|
||||||
sendAckMessage(peer, message, true, null);
|
|
||||||
removeMailboxMessageAfterProcessing(message);
|
|
||||||
}))
|
|
||||||
.setup(tasks(
|
|
||||||
ApplyFilter.class,
|
|
||||||
SellerProcessPaymentSentMessage.class)
|
|
||||||
.using(new TradeTaskRunner(trade,
|
|
||||||
() -> {
|
|
||||||
handleTaskRunnerSuccess(peer, message);
|
|
||||||
},
|
|
||||||
(errorMessage) -> {
|
|
||||||
stopTimeout();
|
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
|
||||||
})))
|
|
||||||
.executeTasks(true);
|
|
||||||
awaitTradeLatch();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// User interaction
|
// User interaction
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -160,7 +107,7 @@ public class SellerProtocol extends DisputeProtocol {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
|
||||||
return new Class[] { SendDepositsConfirmedMessageToBuyer.class };
|
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class, SendDepositsConfirmedMessageToBuyer.class };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import bisq.core.trade.BuyerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.TradeManager;
|
import bisq.core.trade.TradeManager;
|
||||||
import bisq.core.trade.HavenoUtils;
|
import bisq.core.trade.HavenoUtils;
|
||||||
|
import bisq.core.trade.SellerTrade;
|
||||||
import bisq.core.trade.handlers.TradeResultHandler;
|
import bisq.core.trade.handlers.TradeResultHandler;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
import bisq.core.trade.messages.DepositResponse;
|
||||||
|
@ -33,8 +34,10 @@ import bisq.core.trade.messages.SignContractRequest;
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
import bisq.core.trade.messages.TradeMessage;
|
||||||
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
||||||
|
import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage;
|
||||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||||
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
||||||
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
|
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
|
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
|
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
|
||||||
|
@ -92,13 +95,15 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Dispatcher
|
// Message dispatching
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
||||||
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
||||||
if (message instanceof DepositsConfirmedMessage) {
|
if (message instanceof DepositsConfirmedMessage) {
|
||||||
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
||||||
|
} else if (message instanceof PaymentSentMessage) {
|
||||||
|
handle((PaymentSentMessage) message, peerNodeAddress);
|
||||||
} else if (message instanceof PaymentReceivedMessage) {
|
} else if (message instanceof PaymentReceivedMessage) {
|
||||||
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
||||||
}
|
}
|
||||||
|
@ -108,49 +113,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
||||||
if (message instanceof DepositsConfirmedMessage) {
|
if (message instanceof DepositsConfirmedMessage) {
|
||||||
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
||||||
|
} else if (message instanceof PaymentSentMessage) {
|
||||||
|
handle((PaymentSentMessage) message, peerNodeAddress);
|
||||||
} else if (message instanceof PaymentReceivedMessage) {
|
} else if (message instanceof PaymentReceivedMessage) {
|
||||||
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// API
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) {
|
|
||||||
processModel.applyTransient(serviceProvider, tradeManager, offer);
|
|
||||||
onInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onInitialized() {
|
|
||||||
if (!trade.isCompleted()) {
|
|
||||||
processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle trade events
|
|
||||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
|
||||||
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
// initialize trade
|
|
||||||
trade.initialize(processModel.getProvider());
|
|
||||||
|
|
||||||
// process mailbox messages
|
|
||||||
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
|
||||||
mailboxMessageService.addDecryptedMailboxListener(this);
|
|
||||||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onWithdrawCompleted() {
|
|
||||||
log.info("Withdraw completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// DecryptedDirectMessageListener
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
|
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
|
||||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||||
|
@ -176,11 +145,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// DecryptedMailboxListener
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
|
public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
|
||||||
if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return;
|
if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return;
|
||||||
|
@ -240,10 +204,34 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Abstract
|
// API
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public abstract Class<? extends TradeTask>[] getDepsitsConfirmedTasks();
|
public abstract Class<? extends TradeTask>[] getDepositsConfirmedTasks();
|
||||||
|
|
||||||
|
public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) {
|
||||||
|
processModel.applyTransient(serviceProvider, tradeManager, offer);
|
||||||
|
onInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onInitialized() {
|
||||||
|
if (!trade.isCompleted()) {
|
||||||
|
processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle trade events
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize trade
|
||||||
|
trade.initialize(processModel.getProvider());
|
||||||
|
|
||||||
|
// process mailbox messages
|
||||||
|
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
||||||
|
mailboxMessageService.addDecryptedMailboxListener(this);
|
||||||
|
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||||
|
}
|
||||||
|
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
|
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
|
||||||
|
@ -398,6 +386,53 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// received by seller and arbitrator
|
||||||
|
protected void handle(PaymentSentMessage message, NodeAddress peer) {
|
||||||
|
System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage)");
|
||||||
|
if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) {
|
||||||
|
log.warn("Ignoring PaymentSentMessage since not seller or arbitrator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new Thread(() -> {
|
||||||
|
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
|
||||||
|
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
|
||||||
|
// a mailbox message with PaymentSentMessage.
|
||||||
|
// TODO A better fix would be to add a listener for the wallet sync state and process
|
||||||
|
// the mailbox msg once wallet is ready and trade state set.
|
||||||
|
synchronized (trade) {
|
||||||
|
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||||
|
log.warn("Ignoring PaymentSentMessage which was already processed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latchTrade();
|
||||||
|
expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
|
||||||
|
.with(message)
|
||||||
|
.from(peer)
|
||||||
|
.preCondition(trade.getPayoutTx() == null,
|
||||||
|
() -> {
|
||||||
|
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
|
||||||
|
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
||||||
|
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
||||||
|
sendAckMessage(peer, message, true, null);
|
||||||
|
removeMailboxMessageAfterProcessing(message);
|
||||||
|
}))
|
||||||
|
.setup(tasks(
|
||||||
|
ApplyFilter.class,
|
||||||
|
ProcessPaymentSentMessage.class)
|
||||||
|
.using(new TradeTaskRunner(trade,
|
||||||
|
() -> {
|
||||||
|
handleTaskRunnerSuccess(peer, message);
|
||||||
|
},
|
||||||
|
(errorMessage) -> {
|
||||||
|
stopTimeout();
|
||||||
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
|
})))
|
||||||
|
.executeTasks(true);
|
||||||
|
awaitTradeLatch();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
// received by buyer and arbitrator
|
// received by buyer and arbitrator
|
||||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
||||||
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
|
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
|
||||||
|
@ -410,7 +445,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
latchTrade();
|
latchTrade();
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
processModel.setTradeMessage(message);
|
processModel.setTradeMessage(message);
|
||||||
expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED })
|
expect(anyPhase(trade.isBuyer() ? new Trade.Phase[] {Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED} : new Trade.Phase[] {Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT})
|
||||||
.with(message)
|
.with(message)
|
||||||
.from(peer))
|
.from(peer))
|
||||||
.setup(tasks(
|
.setup(tasks(
|
||||||
|
@ -427,6 +462,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onWithdrawCompleted() {
|
||||||
|
log.info("Withdraw completed");
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// FluentProtocol
|
// FluentProtocol
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -590,15 +629,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
// Validation
|
// Validation
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private PubKeyRing getPeersPubKeyRing(NodeAddress peer) {
|
private PubKeyRing getPeersPubKeyRing(NodeAddress address) {
|
||||||
trade.setMyNodeAddress(); // TODO: this is a hack to update my node address before verifying the message
|
trade.setMyNodeAddress(); // TODO: this is a hack to update my node address before verifying the message
|
||||||
if (peer.equals(trade.getArbitrator().getNodeAddress())) return trade.getArbitrator().getPubKeyRing();
|
TradingPeer peer = trade.getTradingPeer(address);
|
||||||
else if (peer.equals(trade.getMaker().getNodeAddress())) return trade.getMaker().getPubKeyRing();
|
if (peer == null) {
|
||||||
else if (peer.equals(trade.getTaker().getNodeAddress())) return trade.getTaker().getPubKeyRing();
|
|
||||||
else {
|
|
||||||
log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + peer);
|
log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + peer);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return peer.getPubKeyRing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPubKeyValid(DecryptedMessageWithPubKey message) {
|
private boolean isPubKeyValid(DecryptedMessageWithPubKey message) {
|
||||||
|
@ -707,7 +745,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
expect(new Condition(trade))
|
expect(new Condition(trade))
|
||||||
.setup(tasks(getDepsitsConfirmedTasks())
|
.setup(tasks(getDepositsConfirmedTasks())
|
||||||
.using(new TradeTaskRunner(trade,
|
.using(new TradeTaskRunner(trade,
|
||||||
() -> {
|
() -> {
|
||||||
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");
|
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");
|
||||||
|
|
|
@ -20,7 +20,9 @@ package bisq.core.trade.protocol;
|
||||||
import bisq.core.btc.model.RawTransactionInput;
|
import bisq.core.btc.model.RawTransactionInput;
|
||||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import bisq.common.app.Version;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.proto.persistable.PersistablePayload;
|
import bisq.common.proto.persistable.PersistablePayload;
|
||||||
|
@ -124,6 +126,8 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
private String depositTxKey;
|
private String depositTxKey;
|
||||||
@Nullable
|
@Nullable
|
||||||
private String updatedMultisigHex;
|
private String updatedMultisigHex;
|
||||||
|
@Nullable
|
||||||
|
private PaymentSentMessage paymentSentMessage;
|
||||||
|
|
||||||
public TradingPeer() {
|
public TradingPeer() {
|
||||||
}
|
}
|
||||||
|
@ -163,6 +167,7 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
|
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
|
||||||
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
|
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
|
||||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||||
|
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||||
|
|
||||||
builder.setCurrentDate(currentDate);
|
builder.setCurrentDate(currentDate);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
@ -211,6 +216,7 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
|
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
|
||||||
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
|
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
|
||||||
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||||
|
tradingPeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null);
|
||||||
return tradingPeer;
|
return tradingPeer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
else throw new RuntimeException("DepositRequest is not from maker or taker");
|
else throw new RuntimeException("DepositRequest is not from maker or taker");
|
||||||
|
|
||||||
// verify deposit tx
|
// verify deposit tx
|
||||||
|
try {
|
||||||
trade.getXmrWalletService().verifyTradeTx(depositAddress,
|
trade.getXmrWalletService().verifyTradeTx(depositAddress,
|
||||||
depositAmount,
|
depositAmount,
|
||||||
tradeFee,
|
tradeFee,
|
||||||
|
@ -87,6 +88,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
request.getDepositTxKey(),
|
request.getDepositTxKey(),
|
||||||
null,
|
null,
|
||||||
false);
|
false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// set deposit info
|
// set deposit info
|
||||||
trader.setDepositTxHex(request.getDepositTxHex());
|
trader.setDepositTxHex(request.getDepositTxHex());
|
||||||
|
|
|
@ -54,6 +54,7 @@ public class ArbitratorProcessReserveTx extends TradeTask {
|
||||||
// process reserve tx with expected terms
|
// process reserve tx with expected terms
|
||||||
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
|
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
|
||||||
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
|
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
|
||||||
|
try {
|
||||||
trade.getXmrWalletService().verifyTradeTx(
|
trade.getXmrWalletService().verifyTradeTx(
|
||||||
request.getPayoutAddress(),
|
request.getPayoutAddress(),
|
||||||
depositAmount,
|
depositAmount,
|
||||||
|
@ -63,6 +64,9 @@ public class ArbitratorProcessReserveTx extends TradeTask {
|
||||||
request.getReserveTxKey(),
|
request.getReserveTxKey(),
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error processing reserve tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// save reserve tx to model
|
// save reserve tx to model
|
||||||
TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker();
|
TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker();
|
||||||
|
|
|
@ -21,11 +21,18 @@ import bisq.core.network.MessageState;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||||
|
import bisq.core.util.JsonUtil;
|
||||||
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
|
||||||
import bisq.common.Timer;
|
import bisq.common.Timer;
|
||||||
|
import bisq.common.crypto.PubKeyRing;
|
||||||
|
import bisq.common.crypto.Sig;
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,8 +45,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
* online he will process it.
|
* online he will process it.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
@EqualsAndHashCode(callSuper = true)
|
||||||
private PaymentSentMessage message;
|
public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
||||||
private ChangeListener<MessageState> listener;
|
private ChangeListener<MessageState> listener;
|
||||||
private Timer timer;
|
private Timer timer;
|
||||||
|
|
||||||
|
@ -47,16 +54,34 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract NodeAddress getReceiverNodeAddress();
|
||||||
|
|
||||||
|
protected abstract PubKeyRing getReceiverPubKeyRing();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void run() {
|
||||||
|
try {
|
||||||
|
runInterceptHook();
|
||||||
|
super.run();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed(t);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||||
if (message == null) {
|
if (trade.getSelf().getPaymentSentMessage() == null) {
|
||||||
|
|
||||||
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
||||||
// peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox
|
// peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox
|
||||||
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
|
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
|
||||||
// other data stays the same when we re-send the message at any time later.
|
// other data stays the same when we re-send the message at any time later.
|
||||||
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
|
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
|
||||||
message = new PaymentSentMessage(
|
|
||||||
|
// create payment sent message
|
||||||
|
PaymentSentMessage message = new PaymentSentMessage(
|
||||||
tradeId,
|
tradeId,
|
||||||
processModel.getMyNodeAddress(),
|
processModel.getMyNodeAddress(),
|
||||||
trade.getCounterCurrencyTxId(),
|
trade.getCounterCurrencyTxId(),
|
||||||
|
@ -66,8 +91,18 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
||||||
trade.getSelf().getUpdatedMultisigHex(),
|
trade.getSelf().getUpdatedMultisigHex(),
|
||||||
trade.getSelf().getPaymentAccountKey()
|
trade.getSelf().getPaymentAccountKey()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// sign message
|
||||||
|
try {
|
||||||
|
String messageAsJson = JsonUtil.objectToJson(message);
|
||||||
|
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||||
|
message.setBuyerSignature(sig);
|
||||||
|
trade.getSelf().setPaymentSentMessage(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException (e);
|
||||||
}
|
}
|
||||||
return message;
|
}
|
||||||
|
return trade.getSelf().getPaymentSentMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -96,18 +131,6 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void run() {
|
|
||||||
try {
|
|
||||||
runInterceptHook();
|
|
||||||
super.run();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
failed(t);
|
|
||||||
} finally {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanup() {
|
private void cleanup() {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.stop();
|
timer.stop();
|
||||||
|
|
|
@ -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);
|
checkNotNull(makerRequest);
|
||||||
checkTradeId(processModel.getOfferId(), makerRequest);
|
checkTradeId(processModel.getOfferId(), makerRequest);
|
||||||
|
|
||||||
// maker signs offer id as nonce to avoid challenge protocol // TODO (woodser): is this necessary?
|
// maker signs offer id as nonce to avoid challenge protocol // TODO: how is this used?
|
||||||
Offer offer = processModel.getOffer();
|
Offer offer = processModel.getOffer();
|
||||||
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));
|
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));
|
||||||
|
|
||||||
|
|
|
@ -37,17 +37,19 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// get sender based on the pub key
|
// get peer
|
||||||
// TODO: trade.getTradingPeer(PubKeyRing)
|
|
||||||
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
|
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
|
||||||
TradingPeer sender;
|
TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing());
|
||||||
if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator();
|
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||||
else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer();
|
|
||||||
else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller();
|
|
||||||
else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
|
||||||
|
|
||||||
// update peer node address
|
// update peer node address
|
||||||
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||||
|
if (sender.getNodeAddress().equals(trade.getBuyer().getNodeAddress()) && sender != trade.getBuyer()) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
|
||||||
|
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
|
||||||
|
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
|
||||||
|
|
||||||
|
// store updated multisig hex for processing on payment sent
|
||||||
|
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||||
|
|
||||||
// decrypt seller payment account payload if key given
|
// decrypt seller payment account payload if key given
|
||||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
|
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
|
||||||
|
@ -55,9 +57,6 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
|
||||||
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
// store updated multisig hex for processing on payment sent
|
|
||||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
|
||||||
|
|
||||||
// persist and complete
|
// persist and complete
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
|
|
|
@ -55,9 +55,6 @@ public class ProcessInitTradeRequest extends TradeTask {
|
||||||
checkNotNull(request);
|
checkNotNull(request);
|
||||||
checkTradeId(processModel.getOfferId(), request);
|
checkTradeId(processModel.getOfferId(), request);
|
||||||
|
|
||||||
System.out.println("PROCESS INIT TRADE REQUEST");
|
|
||||||
System.out.println(request);
|
|
||||||
|
|
||||||
// handle request as arbitrator
|
// handle request as arbitrator
|
||||||
TradingPeer multisigParticipant;
|
TradingPeer multisigParticipant;
|
||||||
if (trade instanceof ArbitratorTrade) {
|
if (trade instanceof ArbitratorTrade) {
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
package bisq.core.trade.protocol.tasks;
|
package bisq.core.trade.protocol.tasks;
|
||||||
|
|
||||||
import bisq.core.account.sign.SignedWitness;
|
import bisq.core.account.sign.SignedWitness;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
|
||||||
import bisq.core.trade.ArbitratorTrade;
|
import bisq.core.trade.ArbitratorTrade;
|
||||||
|
import bisq.core.trade.HavenoUtils;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
|
@ -27,11 +27,13 @@ import common.utils.GenUtils;
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import monero.wallet.MoneroWallet;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProcessPaymentReceivedMessage extends TradeTask {
|
public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||||
|
@ -48,26 +50,34 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
checkNotNull(message);
|
checkNotNull(message);
|
||||||
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
|
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
|
||||||
|
|
||||||
|
// verify signature of payment received message
|
||||||
|
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
|
||||||
|
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
|
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||||
|
|
||||||
// update to the latest peer address of our peer if the message is correct
|
// update to the latest peer address of our peer if the message is correct
|
||||||
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||||
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses
|
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
|
||||||
|
|
||||||
|
// import multisig hex
|
||||||
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
|
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||||
|
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||||
|
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||||
|
|
||||||
|
// sync and save wallet
|
||||||
|
trade.syncWallet();
|
||||||
|
trade.saveWallet();
|
||||||
|
|
||||||
// handle if payout tx not published
|
// handle if payout tx not published
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
|
|
||||||
// import multisig hex
|
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
|
||||||
MoneroWallet multisigWallet = trade.getWallet();
|
|
||||||
if (message.getUpdatedMultisigHex() != null) {
|
|
||||||
multisigWallet.importMultisigHex(message.getUpdatedMultisigHex());
|
|
||||||
trade.saveWallet();
|
|
||||||
}
|
|
||||||
|
|
||||||
// arbitrator waits for buyer to sign and broadcast payout tx if message arrived
|
|
||||||
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
||||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) {
|
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
||||||
log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName());
|
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
GenUtils.waitFor(30000);
|
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||||
multisigWallet.rescanSpent();
|
trade.syncWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify and publish payout tx
|
// verify and publish payout tx
|
||||||
|
@ -77,11 +87,16 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||||
} else {
|
} else {
|
||||||
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
|
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
|
||||||
|
try {
|
||||||
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
||||||
|
else throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId());
|
log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
SignedWitness signedWitness = message.getSignedWitness();
|
SignedWitness signedWitness = message.getSignedWitness();
|
||||||
|
@ -93,7 +108,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
// complete
|
// complete
|
||||||
if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published
|
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|
|
@ -20,14 +20,15 @@ package bisq.core.trade.protocol.tasks;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
import bisq.core.trade.HavenoUtils;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
import bisq.core.trade.messages.PaymentSentMessage;
|
||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SellerProcessPaymentSentMessage extends TradeTask {
|
public class ProcessPaymentSentMessage extends TradeTask {
|
||||||
public SellerProcessPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
public ProcessPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,28 +41,26 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
checkNotNull(message);
|
checkNotNull(message);
|
||||||
|
|
||||||
// store buyer info
|
// verify signature of payment sent message
|
||||||
|
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
||||||
|
|
||||||
|
// update buyer info
|
||||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
|
trade.getBuyer().setPaymentSentMessage(message);
|
||||||
|
|
||||||
// decrypt buyer's payment account payload
|
// if seller, decrypt buyer's payment account payload
|
||||||
trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||||
|
|
||||||
// update latest peer address
|
// update latest peer address
|
||||||
trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||||
|
|
||||||
|
// set state
|
||||||
String counterCurrencyTxId = message.getCounterCurrencyTxId();
|
String counterCurrencyTxId = message.getCounterCurrencyTxId();
|
||||||
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) {
|
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
||||||
trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
|
||||||
}
|
|
||||||
|
|
||||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||||
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) {
|
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
||||||
trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
trade.setStateIfProgress(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||||
}
|
|
||||||
|
|
||||||
trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
|
||||||
|
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
|
@ -107,7 +107,7 @@ public class ProcessSignContractResponse extends TradeTask {
|
||||||
trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST);
|
trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST);
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
} else {
|
} else {
|
||||||
log.info("Waiting for more contract signatures to send deposit request");
|
log.info("Waiting for another contract signatures to send deposit request");
|
||||||
complete(); // does not yet have needed signatures
|
complete(); // does not yet have needed signatures
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|
|
@ -63,11 +63,6 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
||||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||||
trade.setPayoutTx(payoutTx);
|
trade.setPayoutTx(payoutTx);
|
||||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
|
|
||||||
// export multisig hex once
|
|
||||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
|
||||||
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
|
|
|
@ -21,8 +21,10 @@ import bisq.core.account.sign.SignedWitness;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||||
|
import bisq.core.util.JsonUtil;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
|
import bisq.common.crypto.Sig;
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
@ -30,10 +32,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
import com.google.common.base.Charsets;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||||
SignedWitness signedWitness = null;
|
SignedWitness signedWitness = null;
|
||||||
|
PaymentReceivedMessage message = null;
|
||||||
|
|
||||||
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
|
@ -47,13 +52,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||||
protected void run() {
|
protected void run() {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
if (trade.getPayoutTxHex() == null) {
|
|
||||||
log.error("Payout tx is null");
|
|
||||||
failed("Payout tx is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.run();
|
super.run();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
|
@ -63,6 +61,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||||
@Override
|
@Override
|
||||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||||
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
|
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
|
||||||
|
if (message == null) {
|
||||||
|
|
||||||
// TODO: sign witness
|
// TODO: sign witness
|
||||||
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
|
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
|
||||||
|
@ -71,15 +70,28 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||||
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
|
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return new PaymentReceivedMessage(
|
// TODO: create with deterministic id like BuyerSendPaymentSentMessage
|
||||||
|
message = new PaymentReceivedMessage(
|
||||||
tradeId,
|
tradeId,
|
||||||
processModel.getMyNodeAddress(),
|
processModel.getMyNodeAddress(),
|
||||||
signedWitness,
|
signedWitness,
|
||||||
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
|
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
|
||||||
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
|
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
|
||||||
trade.getSelf().getUpdatedMultisigHex(),
|
trade.getSelf().getUpdatedMultisigHex(),
|
||||||
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout
|
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
|
||||||
|
trade.getBuyer().getPaymentSentMessage()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// sign message
|
||||||
|
try {
|
||||||
|
String messageAsJson = JsonUtil.objectToJson(message);
|
||||||
|
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||||
|
message.setSellerSignature(sig);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -65,6 +65,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
|
||||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
|
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
|
||||||
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||||
|
processModel.getTradeManager().requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
||||||
|
|
|
@ -63,6 +63,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
|
||||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
|
TradeTask task = this;
|
||||||
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
|
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
|
||||||
peersNodeAddress,
|
peersNodeAddress,
|
||||||
getReceiverPubKeyRing(),
|
getReceiverPubKeyRing(),
|
||||||
|
@ -72,7 +73,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||||
setStateArrived();
|
setStateArrived();
|
||||||
complete();
|
if (!task.isCompleted()) complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -95,7 +96,7 @@ public abstract class SendMailboxMessageTask extends TradeTask {
|
||||||
|
|
||||||
protected void onStoredInMailbox() {
|
protected void onStoredInMailbox() {
|
||||||
setStateStoredInMailbox();
|
setStateStoredInMailbox();
|
||||||
complete();
|
if (!isCompleted()) complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onFault(String errorMessage, TradeMessage message) {
|
protected void onFault(String errorMessage, TradeMessage message) {
|
||||||
|
|
|
@ -19,11 +19,8 @@ package bisq.core.trade.protocol.tasks;
|
||||||
|
|
||||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
|
||||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.messages.InitTradeRequest;
|
import bisq.core.trade.messages.InitTradeRequest;
|
||||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.SendDirectMessageListener;
|
import bisq.network.p2p.SendDirectMessageListener;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
|
@ -1842,7 +1842,7 @@ disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3}
|
||||||
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
|
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
|
||||||
Open trade and accept or reject suggestion from mediator
|
Open trade and accept or reject suggestion from mediator
|
||||||
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\
|
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\
|
||||||
No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions
|
A dispute has been opened with the arbitrator. You can chat with the arbitrator in the "Support" tab to resolve the dispute.
|
||||||
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
|
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
|
||||||
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
|
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
|
||||||
# suppress inspection "TrailingSpacesInProperty"
|
# suppress inspection "TrailingSpacesInProperty"
|
||||||
|
|
|
@ -212,13 +212,10 @@ public class AccountAgeWitnessServiceTest {
|
||||||
"summary",
|
"summary",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
100000,
|
100000,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
now - 1,
|
now - 1));
|
||||||
false));
|
|
||||||
|
|
||||||
// Filtermanager says nothing is filtered
|
// Filtermanager says nothing is filtered
|
||||||
when(filterManager.isNodeAddressBanned(any())).thenReturn(false);
|
when(filterManager.isNodeAddressBanned(any())).thenReturn(false);
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class GrpcDisputesService extends DisputesImplBase {
|
||||||
},
|
},
|
||||||
(errorMessage, throwable) -> {
|
(errorMessage, throwable) -> {
|
||||||
log.info("Error in openDispute" + errorMessage);
|
log.info("Error in openDispute" + errorMessage);
|
||||||
exceptionHandler.handleException(log, throwable, responseObserver);
|
exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver);
|
||||||
});
|
});
|
||||||
} catch (Throwable cause) {
|
} catch (Throwable cause) {
|
||||||
exceptionHandler.handleException(log, cause, responseObserver);
|
exceptionHandler.handleException(log, cause, responseObserver);
|
||||||
|
@ -82,7 +82,7 @@ public class GrpcDisputesService extends DisputesImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (Throwable cause) {
|
} catch (Throwable cause) {
|
||||||
exceptionHandler.handleException(log, cause, responseObserver);
|
exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".getDispute", cause, responseObserver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ public class GrpcDisputesService extends DisputesImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (Throwable cause) {
|
} catch (Throwable cause) {
|
||||||
exceptionHandler.handleException(log, cause, responseObserver);
|
exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".resolveDispute", cause, responseObserver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ public class GrpcDisputesService extends DisputesImplBase {
|
||||||
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
}}
|
}}
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,7 @@ class GrpcOffersService extends OffersImplBase {
|
||||||
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
}}
|
}}
|
||||||
)));
|
)));
|
||||||
|
|
|
@ -244,9 +244,9 @@ class GrpcTradesService extends TradesImplBase {
|
||||||
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||||
new HashMap<>() {{
|
new HashMap<>() {{
|
||||||
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(30, SECONDS));
|
||||||
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
|
|
|
@ -189,9 +189,7 @@ class GrpcWalletsService extends WalletsImplBase {
|
||||||
.stream()
|
.stream()
|
||||||
.map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount())))
|
.map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount())))
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
log.info("Successfully created XMR tx: hash {}, metadata {}",
|
log.info("Successfully created XMR tx: hash {}", tx.getHash());
|
||||||
tx.getHash(),
|
|
||||||
tx.getMetadata());
|
|
||||||
var reply = CreateXmrTxReply.newBuilder()
|
var reply = CreateXmrTxReply.newBuilder()
|
||||||
.setTx(toXmrTx(tx).toProtoMessage())
|
.setTx(toXmrTx(tx).toProtoMessage())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
|
||||||
import bisq.core.trade.protocol.tasks.MakerSetLockTime;
|
import bisq.core.trade.protocol.tasks.MakerSetLockTime;
|
||||||
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
||||||
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
||||||
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
|
import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage;
|
||||||
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
|
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
|
||||||
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
|
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
|
||||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
||||||
|
@ -100,7 +100,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||||
SellerPublishDepositTx.class,
|
SellerPublishDepositTx.class,
|
||||||
SellerPublishTradeStatistics.class,
|
SellerPublishTradeStatistics.class,
|
||||||
|
|
||||||
SellerProcessPaymentSentMessage.class,
|
ProcessPaymentSentMessage.class,
|
||||||
ApplyFilter.class,
|
ApplyFilter.class,
|
||||||
TakerVerifyMakerFeePayment.class,
|
TakerVerifyMakerFeePayment.class,
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||||
SellerPublishDepositTx.class,
|
SellerPublishDepositTx.class,
|
||||||
SellerPublishTradeStatistics.class,
|
SellerPublishTradeStatistics.class,
|
||||||
|
|
||||||
SellerProcessPaymentSentMessage.class,
|
ProcessPaymentSentMessage.class,
|
||||||
ApplyFilter.class,
|
ApplyFilter.class,
|
||||||
|
|
||||||
ApplyFilter.class,
|
ApplyFilter.class,
|
||||||
|
|
|
@ -92,10 +92,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
private final CoinFormatter formatter;
|
private final CoinFormatter formatter;
|
||||||
private final ArbitrationManager arbitrationManager;
|
private final ArbitrationManager arbitrationManager;
|
||||||
private final MediationManager mediationManager;
|
private final MediationManager mediationManager;
|
||||||
private final XmrWalletService walletService;
|
private final CoreDisputesService disputesService; private Dispute dispute;
|
||||||
private final TradeWalletService tradeWalletService; // TODO (woodser): remove for xmr or adapt to get/create multisig wallets for tx creation utils
|
|
||||||
private final CoreDisputesService disputesService;
|
|
||||||
private Dispute dispute;
|
|
||||||
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
|
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
|
||||||
private DisputeResult disputeResult;
|
private DisputeResult disputeResult;
|
||||||
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
|
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
|
||||||
|
@ -115,7 +112,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
private ChangeListener<Toggle> reasonToggleSelectionListener;
|
private ChangeListener<Toggle> reasonToggleSelectionListener;
|
||||||
private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField;
|
private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField;
|
||||||
private ChangeListener<String> buyerPayoutAmountListener, sellerPayoutAmountListener;
|
private ChangeListener<String> buyerPayoutAmountListener, sellerPayoutAmountListener;
|
||||||
private CheckBox isLoserPublisherCheckBox;
|
|
||||||
private ChangeListener<Toggle> tradeAmountToggleGroupListener;
|
private ChangeListener<Toggle> tradeAmountToggleGroupListener;
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,8 +130,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
this.formatter = formatter;
|
this.formatter = formatter;
|
||||||
this.arbitrationManager = arbitrationManager;
|
this.arbitrationManager = arbitrationManager;
|
||||||
this.mediationManager = mediationManager;
|
this.mediationManager = mediationManager;
|
||||||
this.walletService = walletService;
|
|
||||||
this.tradeWalletService = tradeWalletService;
|
|
||||||
this.disputesService = disputesService;
|
this.disputesService = disputesService;
|
||||||
|
|
||||||
type = Type.Confirmation;
|
type = Type.Confirmation;
|
||||||
|
@ -220,7 +214,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount());
|
disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount());
|
||||||
disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount());
|
disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount());
|
||||||
disputeResult.setWinner(peersDisputeResult.getWinner());
|
disputeResult.setWinner(peersDisputeResult.getWinner());
|
||||||
disputeResult.setLoserPublisher(peersDisputeResult.isLoserPublisher());
|
|
||||||
disputeResult.setReason(peersDisputeResult.getReason());
|
disputeResult.setReason(peersDisputeResult.getReason());
|
||||||
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
|
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
|
||||||
|
|
||||||
|
@ -248,13 +241,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
reasonWasPeerWasLateRadioButton.setDisable(true);
|
reasonWasPeerWasLateRadioButton.setDisable(true);
|
||||||
reasonWasTradeAlreadySettledRadioButton.setDisable(true);
|
reasonWasTradeAlreadySettledRadioButton.setDisable(true);
|
||||||
|
|
||||||
isLoserPublisherCheckBox.setDisable(true);
|
|
||||||
isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher());
|
|
||||||
|
|
||||||
applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get());
|
applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get());
|
||||||
applyTradeAmountRadioButtonStates();
|
applyTradeAmountRadioButtonStates();
|
||||||
} else {
|
|
||||||
isLoserPublisherCheckBox.setSelected(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setReasonRadioButtonState();
|
setReasonRadioButtonState();
|
||||||
|
@ -426,11 +414,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller"));
|
sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller"));
|
||||||
sellerPayoutAmountInputTextField.setEditable(false);
|
sellerPayoutAmountInputTextField.setEditable(false);
|
||||||
|
|
||||||
isLoserPublisherCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.payoutAmount.invert"));
|
|
||||||
|
|
||||||
VBox vBox = new VBox();
|
VBox vBox = new VBox();
|
||||||
vBox.setSpacing(15);
|
vBox.setSpacing(15);
|
||||||
vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField, isLoserPublisherCheckBox);
|
vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField);
|
||||||
GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
|
GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
|
||||||
GridPane.setRowIndex(vBox, rowIndex);
|
GridPane.setRowIndex(vBox, rowIndex);
|
||||||
GridPane.setColumnIndex(vBox, 1);
|
GridPane.setColumnIndex(vBox, 1);
|
||||||
|
@ -590,7 +576,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
Button cancelButton = tuple.second;
|
Button cancelButton = tuple.second;
|
||||||
|
|
||||||
closeTicketButton.setOnAction(e -> {
|
closeTicketButton.setOnAction(e -> {
|
||||||
disputesService.applyDisputePayout(dispute, disputeResult, contract);
|
|
||||||
doClose(closeTicketButton);
|
doClose(closeTicketButton);
|
||||||
|
|
||||||
// if (dispute.getDepositTxSerialized() == null) {
|
// if (dispute.getDepositTxSerialized() == null) {
|
||||||
|
@ -763,19 +748,14 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
|
|
||||||
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
|
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
|
||||||
|
|
||||||
boolean isRefundAgent = disputeManager instanceof RefundManager;
|
|
||||||
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
|
|
||||||
disputeResult.setCloseDate(new Date());
|
disputeResult.setCloseDate(new Date());
|
||||||
disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent);
|
disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> {
|
||||||
|
|
||||||
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
|
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
|
||||||
UserThread.runAfter(() -> new Popup()
|
new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show();
|
||||||
.attention(Res.get("disputeSummaryWindow.close.closePeer"))
|
|
||||||
.show(),
|
|
||||||
200, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disputeManager.requestPersistence();
|
disputeManager.requestPersistence();
|
||||||
|
});
|
||||||
|
|
||||||
closeTicketButton.disableProperty().unbind();
|
closeTicketButton.disableProperty().unbind();
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
|
@ -465,7 +465,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
byte[] payoutTxSerialized = null;
|
byte[] payoutTxSerialized = null;
|
||||||
String payoutTxHashAsString = null;
|
String payoutTxHashAsString = null;
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
|
||||||
if (trade.getPayoutTxId() != null) {
|
if (trade.getPayoutTxId() != null) {
|
||||||
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
|
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
|
||||||
// payoutTxHashAsString = payoutTx.getHashAsString();
|
// payoutTxHashAsString = payoutTx.getHashAsString();
|
||||||
|
@ -477,9 +476,9 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
// If mediation is not activated we use arbitration
|
// If mediation is not activated we use arbitration
|
||||||
if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) {
|
if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) {
|
||||||
// In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or
|
// In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or
|
||||||
useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED;
|
useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED;
|
||||||
// in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED
|
// in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED
|
||||||
useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED;
|
useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED;
|
||||||
} else {
|
} else {
|
||||||
useMediation = false;
|
useMediation = false;
|
||||||
useArbitration = true;
|
useArbitration = true;
|
||||||
|
@ -549,27 +548,27 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||||
|
|
||||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
||||||
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
|
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else if (useArbitration) {
|
} else if (useArbitration) {
|
||||||
// Only if we have completed mediation we allow arbitration
|
// Only if we have completed mediation we allow arbitration
|
||||||
disputeManager = arbitrationManager;
|
disputeManager = arbitrationManager;
|
||||||
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
||||||
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
|
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Invalid dispute state {}", disputeState.name());
|
log.warn("Invalid dispute state {}", disputeState.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
||||||
disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, senderMultisigHex,
|
disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex,
|
||||||
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
|
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
|
||||||
if ((throwable instanceof DisputeAlreadyOpenException)) {
|
if ((throwable instanceof DisputeAlreadyOpenException)) {
|
||||||
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
|
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
|
||||||
new Popup().warning(errorMessage)
|
new Popup().warning(errorMessage)
|
||||||
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
||||||
.onAction(() -> sendOpenNewDisputeMessage(dispute, true, disputeManager, senderMultisigHex))
|
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex))
|
||||||
.closeButtonText(Res.get("shared.cancel")).show();
|
.closeButtonText(Res.get("shared.cancel")).show();
|
||||||
} else {
|
} else {
|
||||||
new Popup().warning(errorMessage).show();
|
new Popup().warning(errorMessage).show();
|
||||||
|
|
|
@ -511,7 +511,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||||
if (trade instanceof ArbitratorTrade) return;
|
if (trade instanceof ArbitratorTrade) return;
|
||||||
|
|
||||||
switch (payoutState) {
|
switch (payoutState) {
|
||||||
case PUBLISHED:
|
case PAYOUT_PUBLISHED:
|
||||||
sellerState.set(SellerState.STEP4);
|
sellerState.set(SellerState.STEP4);
|
||||||
buyerState.set(BuyerState.STEP4);
|
buyerState.set(BuyerState.STEP4);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -31,6 +31,7 @@ import bisq.core.locale.Res;
|
||||||
import bisq.core.support.dispute.Dispute;
|
import bisq.core.support.dispute.Dispute;
|
||||||
import bisq.core.support.dispute.DisputeResult;
|
import bisq.core.support.dispute.DisputeResult;
|
||||||
import bisq.core.support.dispute.mediation.MediationResultState;
|
import bisq.core.support.dispute.mediation.MediationResultState;
|
||||||
|
import bisq.core.trade.ArbitratorTrade;
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
import bisq.core.trade.MakerTrade;
|
import bisq.core.trade.MakerTrade;
|
||||||
import bisq.core.trade.TakerTrade;
|
import bisq.core.trade.TakerTrade;
|
||||||
|
@ -480,31 +481,25 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
switch (disputeState) {
|
switch (disputeState) {
|
||||||
case NO_DISPUTE:
|
case NO_DISPUTE:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DISPUTE_REQUESTED:
|
case DISPUTE_REQUESTED:
|
||||||
|
case DISPUTE_OPENED:
|
||||||
if (tradeStepInfo != null) {
|
if (tradeStepInfo != null) {
|
||||||
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
|
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
applyOnDisputeOpened();
|
||||||
|
|
||||||
|
// update trade view unless arbitrator
|
||||||
|
if (trade instanceof ArbitratorTrade) break;
|
||||||
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
|
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
|
||||||
ownDispute.ifPresent(dispute -> {
|
ownDispute.ifPresent(dispute -> {
|
||||||
if (tradeStepInfo != null)
|
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
case DISPUTE_STARTED_BY_PEER:
|
|
||||||
if (tradeStepInfo != null) {
|
if (tradeStepInfo != null) {
|
||||||
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
|
boolean isOpener = dispute.isDisputeOpenerIsBuyer() ? trade.isBuyer() : trade.isSeller();
|
||||||
|
tradeStepInfo.setState(isOpener ? TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED : TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
|
||||||
|
|
||||||
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
|
|
||||||
ownDispute.ifPresent(dispute -> {
|
|
||||||
if (tradeStepInfo != null)
|
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DISPUTE_CLOSED:
|
case DISPUTE_CLOSED:
|
||||||
break;
|
break;
|
||||||
case MEDIATION_REQUESTED:
|
case MEDIATION_REQUESTED:
|
||||||
|
|
|
@ -190,7 +190,7 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
model.setMessageStateProperty(MessageState.FAILED);
|
model.setMessageStateProperty(MessageState.FAILED);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
|
log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId());
|
||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
|
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
|
||||||
break;
|
break;
|
||||||
|
@ -608,12 +608,6 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
busyAnimation.play();
|
busyAnimation.play();
|
||||||
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
||||||
|
|
||||||
//TODO seems this was a hack to enable repeated confirm???
|
|
||||||
if (trade.isPaymentSent()) {
|
|
||||||
trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
|
||||||
model.dataModel.getTradeManager().requestPersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
model.dataModel.onPaymentStarted(() -> {
|
model.dataModel.onPaymentStarted(() -> {
|
||||||
}, errorMessage -> {
|
}, errorMessage -> {
|
||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
|
|
|
@ -145,6 +145,11 @@ public class SellerStep3View extends TradeStepView {
|
||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
statusLabel.setText("");
|
statusLabel.setText("");
|
||||||
break;
|
break;
|
||||||
|
case TRADE_COMPLETED:
|
||||||
|
if (!trade.isPayoutPublished()) log.warn("Payout is expected to be published for {} {} state {}", trade.getClass().getSimpleName(), trade.getId(), trade.getState());
|
||||||
|
busyAnimation.stop();
|
||||||
|
statusLabel.setText("");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
|
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
|
||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
|
|
|
@ -838,15 +838,19 @@ message TradeInfo {
|
||||||
string phase = 17;
|
string phase = 17;
|
||||||
string period_state = 18;
|
string period_state = 18;
|
||||||
string payout_state = 19;
|
string payout_state = 19;
|
||||||
bool is_deposit_published = 20;
|
string dispute_state = 20;
|
||||||
bool is_deposit_unlocked = 21;
|
bool is_deposit_published = 21;
|
||||||
bool is_payment_sent = 22;
|
bool is_deposit_confirmed = 22;
|
||||||
bool is_payment_received = 23;
|
bool is_deposit_unlocked = 23;
|
||||||
bool is_payout_published = 24;
|
bool is_payment_sent = 24;
|
||||||
bool is_completed = 25;
|
bool is_payment_received = 25;
|
||||||
string contract_as_json = 26;
|
bool is_payout_published = 26;
|
||||||
ContractInfo contract = 27;
|
bool is_payout_confirmed = 27;
|
||||||
string trade_volume = 28;
|
bool is_payout_unlocked = 28;
|
||||||
|
bool is_completed = 29;
|
||||||
|
string contract_as_json = 30;
|
||||||
|
ContractInfo contract = 31;
|
||||||
|
string trade_volume = 32;
|
||||||
|
|
||||||
string maker_deposit_tx_id = 100;
|
string maker_deposit_tx_id = 100;
|
||||||
string taker_deposit_tx_id = 101;
|
string taker_deposit_tx_id = 101;
|
||||||
|
|
|
@ -40,31 +40,29 @@ message NetworkEnvelope {
|
||||||
InputsForDepositTxResponse inputs_for_deposit_tx_response = 18;
|
InputsForDepositTxResponse inputs_for_deposit_tx_response = 18;
|
||||||
DepositTxMessage deposit_tx_message = 19;
|
DepositTxMessage deposit_tx_message = 19;
|
||||||
|
|
||||||
OpenNewDisputeMessage open_new_dispute_message = 20;
|
DisputeOpenedMessage dispute_opened_message = 20;
|
||||||
PeerOpenedDisputeMessage peer_opened_dispute_message = 21;
|
DisputeClosedMessage dispute_closed_message = 21;
|
||||||
ChatMessage chat_message = 22;
|
ChatMessage chat_message = 22;
|
||||||
DisputeResultMessage dispute_result_message = 23;
|
|
||||||
PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 24;
|
|
||||||
|
|
||||||
PrivateNotificationMessage private_notification_message = 25;
|
PrivateNotificationMessage private_notification_message = 23;
|
||||||
|
|
||||||
AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 26;
|
AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 24;
|
||||||
AckMessage ack_message = 27;
|
AckMessage ack_message = 25;
|
||||||
|
|
||||||
BundleOfEnvelopes bundle_of_envelopes = 28;
|
BundleOfEnvelopes bundle_of_envelopes = 26;
|
||||||
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 29;
|
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 27;
|
||||||
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 30;
|
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 28;
|
||||||
|
|
||||||
DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 31;
|
DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 29;
|
||||||
DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 32;
|
DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 30;
|
||||||
DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 33;
|
DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 31;
|
||||||
PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 34;
|
PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 32;
|
||||||
|
|
||||||
RefreshTradeStateRequest refresh_trade_state_request = 35 [deprecated = true];
|
RefreshTradeStateRequest refresh_trade_state_request = 33 [deprecated = true];
|
||||||
TraderSignedWitnessMessage trader_signed_witness_message = 36 [deprecated = true];
|
TraderSignedWitnessMessage trader_signed_witness_message = 34 [deprecated = true];
|
||||||
|
|
||||||
GetInventoryRequest get_inventory_request = 37;
|
GetInventoryRequest get_inventory_request = 35;
|
||||||
GetInventoryResponse get_inventory_response = 38;
|
GetInventoryResponse get_inventory_response = 36;
|
||||||
|
|
||||||
SignOfferRequest sign_offer_request = 1001;
|
SignOfferRequest sign_offer_request = 1001;
|
||||||
SignOfferResponse sign_offer_response = 1002;
|
SignOfferResponse sign_offer_response = 1002;
|
||||||
|
@ -77,8 +75,6 @@ message NetworkEnvelope {
|
||||||
DepositsConfirmedMessage deposits_confirmed_message = 1009;
|
DepositsConfirmedMessage deposits_confirmed_message = 1009;
|
||||||
PaymentSentMessage payment_sent_message = 1010;
|
PaymentSentMessage payment_sent_message = 1010;
|
||||||
PaymentReceivedMessage payment_received_message = 1011;
|
PaymentReceivedMessage payment_received_message = 1011;
|
||||||
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012;
|
|
||||||
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,14 +395,6 @@ message PeerPublishedDelayedPayoutTxMessage {
|
||||||
NodeAddress sender_node_address = 3;
|
NodeAddress sender_node_address = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FinalizePayoutTxRequest {
|
|
||||||
string trade_id = 1;
|
|
||||||
bytes seller_signature = 2;
|
|
||||||
string seller_payout_address = 3;
|
|
||||||
NodeAddress sender_node_address = 4;
|
|
||||||
string uid = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PaymentSentMessage {
|
message PaymentSentMessage {
|
||||||
string trade_id = 1;
|
string trade_id = 1;
|
||||||
NodeAddress sender_node_address = 2;
|
NodeAddress sender_node_address = 2;
|
||||||
|
@ -416,6 +404,7 @@ message PaymentSentMessage {
|
||||||
string payout_tx_hex = 6;
|
string payout_tx_hex = 6;
|
||||||
string updated_multisig_hex = 7;
|
string updated_multisig_hex = 7;
|
||||||
bytes payment_account_key = 8;
|
bytes payment_account_key = 8;
|
||||||
|
bytes buyer_signature = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PaymentReceivedMessage {
|
message PaymentReceivedMessage {
|
||||||
|
@ -426,23 +415,9 @@ message PaymentReceivedMessage {
|
||||||
string unsigned_payout_tx_hex = 5;
|
string unsigned_payout_tx_hex = 5;
|
||||||
string signed_payout_tx_hex = 6;
|
string signed_payout_tx_hex = 6;
|
||||||
string updated_multisig_hex = 7;
|
string updated_multisig_hex = 7;
|
||||||
bool saw_arrived_payment_received_msg = 8;
|
bool defer_publish_payout = 8;
|
||||||
}
|
PaymentSentMessage payment_sent_message = 9;
|
||||||
|
bytes seller_signature = 10;
|
||||||
message ArbitratorPayoutTxRequest {
|
|
||||||
Dispute dispute = 1; // TODO (woodser): replace with trade id
|
|
||||||
NodeAddress sender_node_address = 2;
|
|
||||||
string uid = 3;
|
|
||||||
SupportType type = 4;
|
|
||||||
string updated_multisig_hex = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ArbitratorPayoutTxResponse {
|
|
||||||
string trade_id = 1;
|
|
||||||
NodeAddress sender_node_address = 2;
|
|
||||||
string uid = 3;
|
|
||||||
SupportType type = 4;
|
|
||||||
string arbitrator_signed_payout_tx_hex = 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message MediatedPayoutTxPublishedMessage {
|
message MediatedPayoutTxPublishedMessage {
|
||||||
|
@ -474,30 +449,6 @@ message TraderSignedWitnessMessage {
|
||||||
SignedWitness signed_witness = 4 [deprecated = true];
|
SignedWitness signed_witness = 4 [deprecated = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispute
|
|
||||||
|
|
||||||
enum SupportType {
|
|
||||||
ARBITRATION = 0;
|
|
||||||
MEDIATION = 1;
|
|
||||||
TRADE = 2;
|
|
||||||
REFUND = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message OpenNewDisputeMessage {
|
|
||||||
Dispute dispute = 1;
|
|
||||||
NodeAddress sender_node_address = 2;
|
|
||||||
string uid = 3;
|
|
||||||
SupportType type = 4;
|
|
||||||
string updated_multisig_hex = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PeerOpenedDisputeMessage {
|
|
||||||
Dispute dispute = 1;
|
|
||||||
NodeAddress sender_node_address = 2;
|
|
||||||
string uid = 3;
|
|
||||||
SupportType type = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatMessage {
|
message ChatMessage {
|
||||||
int64 date = 1;
|
int64 date = 1;
|
||||||
string trade_id = 2;
|
string trade_id = 2;
|
||||||
|
@ -517,21 +468,32 @@ message ChatMessage {
|
||||||
bool was_displayed = 16;
|
bool was_displayed = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DisputeResultMessage {
|
// dispute
|
||||||
|
|
||||||
|
enum SupportType {
|
||||||
|
ARBITRATION = 0;
|
||||||
|
MEDIATION = 1;
|
||||||
|
TRADE = 2;
|
||||||
|
REFUND = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DisputeOpenedMessage {
|
||||||
|
Dispute dispute = 1;
|
||||||
|
NodeAddress sender_node_address = 2;
|
||||||
|
string uid = 3;
|
||||||
|
SupportType type = 4;
|
||||||
|
string updated_multisig_hex = 5;
|
||||||
|
PaymentSentMessage payment_sent_message = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DisputeClosedMessage {
|
||||||
string uid = 1;
|
string uid = 1;
|
||||||
DisputeResult dispute_result = 2;
|
DisputeResult dispute_result = 2;
|
||||||
NodeAddress sender_node_address = 3;
|
NodeAddress sender_node_address = 3;
|
||||||
SupportType type = 4;
|
SupportType type = 4;
|
||||||
}
|
string updated_multisig_hex = 5;
|
||||||
|
string unsigned_payout_tx_hex = 6;
|
||||||
message PeerPublishedDisputePayoutTxMessage {
|
bool defer_publish_payout = 7;
|
||||||
string uid = 1;
|
|
||||||
reserved 2; // was bytes transaction = 2;
|
|
||||||
string trade_id = 3;
|
|
||||||
NodeAddress sender_node_address = 4;
|
|
||||||
SupportType type = 5;
|
|
||||||
string updated_multisig_hex = 6;
|
|
||||||
string payout_tx_hex = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message PrivateNotificationMessage {
|
message PrivateNotificationMessage {
|
||||||
|
@ -944,8 +906,6 @@ message DisputeResult {
|
||||||
bytes arbitrator_pub_key = 13;
|
bytes arbitrator_pub_key = 13;
|
||||||
int64 close_date = 14;
|
int64 close_date = 14;
|
||||||
bool is_loser_publisher = 15;
|
bool is_loser_publisher = 15;
|
||||||
string arbitrator_signed_payout_tx_hex = 16;
|
|
||||||
string arbitrator_updated_multisig_hex = 17;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1640,24 +1600,28 @@ message Trade {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PayoutState {
|
enum PayoutState {
|
||||||
UNPUBLISHED = 0;
|
PAYOUT_UNPUBLISHED = 0;
|
||||||
PUBLISHED = 1;
|
PAYOUT_PUBLISHED = 1;
|
||||||
CONFIRMED = 2;
|
PAYOUT_CONFIRMED = 2;
|
||||||
UNLOCKED = 3;
|
PAYOUT_UNLOCKED = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisputeState {
|
enum DisputeState {
|
||||||
PB_ERROR_DISPUTE_STATE = 0;
|
PB_ERROR_DISPUTE_STATE = 0;
|
||||||
NO_DISPUTE = 1;
|
NO_DISPUTE = 1;
|
||||||
DISPUTE_REQUESTED = 2; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
DISPUTE_REQUESTED = 2;
|
||||||
DISPUTE_STARTED_BY_PEER = 3; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
DISPUTE_OPENED = 3;
|
||||||
DISPUTE_CLOSED = 4; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG = 4;
|
||||||
MEDIATION_REQUESTED = 5;
|
ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG = 5;
|
||||||
MEDIATION_STARTED_BY_PEER = 6;
|
ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG = 6;
|
||||||
MEDIATION_CLOSED = 7;
|
ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG = 7;
|
||||||
REFUND_REQUESTED = 8;
|
DISPUTE_CLOSED = 8;
|
||||||
REFUND_REQUEST_STARTED_BY_PEER = 9;
|
MEDIATION_REQUESTED = 9;
|
||||||
REFUND_REQUEST_CLOSED = 10;
|
MEDIATION_STARTED_BY_PEER = 10;
|
||||||
|
MEDIATION_CLOSED = 11;
|
||||||
|
REFUND_REQUESTED = 12;
|
||||||
|
REFUND_REQUEST_STARTED_BY_PEER = 13;
|
||||||
|
REFUND_REQUEST_CLOSED = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TradePeriodState {
|
enum TradePeriodState {
|
||||||
|
@ -1782,6 +1746,7 @@ message TradingPeer {
|
||||||
string deposit_tx_hex = 1009;
|
string deposit_tx_hex = 1009;
|
||||||
string deposit_tx_key = 1010;
|
string deposit_tx_key = 1010;
|
||||||
string updated_multisig_hex = 1011;
|
string updated_multisig_hex = 1011;
|
||||||
|
PaymentSentMessage payment_sent_message = 1012;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue