refactor arbitration protocol

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

View file

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

View file

@ -23,7 +23,6 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import com.google.inject.name.Named;
@ -40,8 +39,6 @@ import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@Singleton
@Slf4j
@ -101,9 +98,7 @@ public class CoreDisputesService {
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.exportMultisigHex();
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler);
tradeManager.requestPersistence();
}
}
@ -141,26 +136,26 @@ public class CoreDisputesService {
isSupportTicket,
SupportType.ARBITRATION);
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
return dispute;
}
}
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try {
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
.filter(d -> tradeId.equals(d.getTradeId()))
.findFirst();
Dispute dispute;
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
// get winning dispute
Dispute winningDispute;
Trade trade = tradeManager.getTrade(tradeId);
var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
.filter(d -> tradeId.equals(d.getTradeId()))
.filter(d -> trade.getTradingPeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
.findFirst();
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (trade) {
var closeDate = new Date();
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
var contract = dispute.getContract();
var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
DisputePayout payout;
if (customWinnerAmount > 0) {
@ -172,30 +167,28 @@ public class CoreDisputesService {
} else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
}
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
// apply dispute payout
applyDisputePayout(dispute, disputeResult, contract);
applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount);
// close dispute ticket
closeDispute(arbitrationManager, dispute, disputeResult, false);
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
arbitrationManager.requestPersistence();
// close dispute ticket for peer
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
.findFirst();
if (peersDisputeOptional.isPresent()) {
var peerDispute = peersDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
} else {
throw new IllegalStateException("could not find peer dispute");
}
arbitrationManager.requestPersistence();
// close peer's dispute ticket
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId())
.findFirst();
if (peersDisputeOptional.isPresent()) {
var peerDispute = peersDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> {
arbitrationManager.requestPersistence();
});
} else {
throw new IllegalStateException("could not find peer dispute");
}
});
}
} catch (Exception e) {
throw new IllegalStateException(e);
@ -246,49 +239,13 @@ public class CoreDisputesService {
}
}
public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
if (!dispute.isMediationDispute()) {
try {
synchronized (tradeManager.getTrade(dispute.getTradeId())) {
System.out.println(disputeResult);
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
// determine if dispute is in context of publisher
boolean isOpener = dispute.isOpener();
boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER);
boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner;
// open multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
// if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx
if (isOpener) {
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
// if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex
if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
}
// send arbitrator's updated multisig hex with dispute result
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex());
}
} catch (AddressFormatException e2) {
log.error("Error at close dispute", e2);
return;
}
}
}
// From DisputeSummaryWindow.java
public void closeDispute(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, boolean isRefundAgent) {
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler) {
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
DisputeResult.Reason reason = disputeResult.getReason();
String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator");
String role = Res.get("shared.arbitrator");
String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
Contract contract = dispute.getContract();
String currencyCode = contract.getOfferPayload().getCurrencyCode();
@ -314,13 +271,8 @@ public class CoreDisputesService {
}
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
if (isRefundAgent) {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
} else {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
}
disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler);
}
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {

View file

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

View file

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

View file

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

View file

@ -95,17 +95,14 @@ public class Balances {
}
private void updatedBalances() {
// Need to delay a bit to get the balances correct
UserThread.execute(() -> { // TODO (woodser): running on user thread because JFX properties updated for legacy app
updateAvailableBalance();
updatePendingBalance();
updateReservedOfferBalance();
updateReservedTradeBalance();
updateReservedBalance();
});
updateAvailableBalance();
updatePendingBalance();
updateReservedOfferBalance();
updateReservedTradeBalance();
updateReservedBalance();
}
// TODO (woodser): balances being set as Coin from BigInteger.longValue(), which can lose precision. should be in centineros for consistency with the rest of the application
// TODO (woodser): converting to long should generally be avoided since can lose precision, but in practice these amounts are below max value
private void updateAvailableBalance() {
availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact()));

View file

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

View file

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

View file

@ -43,6 +43,7 @@ public class TradeEvents {
private final PubKeyRingProvider pubKeyRingProvider;
private final TradeManager tradeManager;
private final MobileNotificationService mobileNotificationService;
private boolean isInitialized = false;
@Inject
public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) {
@ -59,10 +60,11 @@ public class TradeEvents {
}
});
tradeManager.getObservableList().forEach(this::setTradePhaseListener);
isInitialized = true;
}
private void setTradePhaseListener(Trade trade) {
log.info("We got a new trade. id={}", trade.getId());
if (isInitialized) log.info("We got a new trade. id={}", trade.getId());
if (!trade.isPayoutPublished()) {
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
String msg = null;

View file

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

View file

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

View file

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

View file

@ -62,7 +62,6 @@ public class PlaceOfferProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void placeOffer() {
log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId());
timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing"));
@ -96,10 +95,12 @@ public class PlaceOfferProtocol {
// ignore if timer already stopped
if (timeoutTimer == null) {
log.warn("Ignoring sign offer response from arbitrator because timeout has expired");
log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOffer().getId());
return;
}
// reset timer
stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_TIMEOUT);

View file

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

View file

@ -79,8 +79,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
sendSignOfferRequests(request, () -> {
complete();
}, (errorMessage) -> {
log.warn("Error signing offer: " + errorMessage);
appendToErrorMessage("Error signing offer: " + errorMessage);
appendToErrorMessage("Error signing offer " + request.getOfferId() + ": " + errorMessage);
failed(errorMessage);
});
} catch (Throwable t) {
@ -94,7 +93,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager());
if (leastUsedArbitrator == null) {
errorMessageHandler.handleErrorMessage("Could not get least used arbitrator");
errorMessageHandler.handleErrorMessage("Could not get least used arbitrator to send " + request.getClass().getSimpleName() + " for offer " + request.getOfferId());
return;
}
sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler);
@ -102,7 +101,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// complete on successful ack message
// complete on successful ack message, fail on first nack
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
@Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
@ -117,8 +116,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
resultHandler.handleResult();
} else {
log.warn("Arbitrator nacked request: {}", errorMessage);
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
}
}
};
@ -135,7 +133,14 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
@Override
public void onFault(String errorMessage) {
log.warn("Arbitrator unavailable: {}", errorMessage);
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
excludedArbitrators.add(arbitratorNodeAddress);
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators);
if (altArbitrator == null) {
errorMessageHandler.handleErrorMessage("Offer " + request.getOfferId() + " could not be signed by any arbitrator");
return;
}
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
}
});
}
@ -156,15 +161,4 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
listener
);
}
private void handleArbitratorFailure(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
excludedArbitrators.add(arbitratorNodeAddress);
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators);
if (altArbitrator == null) {
errorMessageHandler.handleErrorMessage("Offer could not be signed by any arbitrator");
return;
}
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -31,15 +31,19 @@ import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportManager;
import bisq.core.support.dispute.messages.DisputeResultMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.dispute.DisputeResult.Winner;
import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.ParsingUtils;
import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
@ -63,8 +67,9 @@ import javafx.beans.property.IntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.math.BigInteger;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@ -75,6 +80,10 @@ import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet;
import javax.annotation.Nullable;
@ -82,8 +91,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
@Slf4j
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
protected final TradeWalletService tradeWalletService;
@ -197,12 +204,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
///////////////////////////////////////////////////////////////////////////////////////////
// We get that message at both peers. The dispute object is in context of the trader
public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage);
public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage);
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
protected abstract Trade.DisputeState getDisputeStateStartedByPeer();
public abstract void cleanupDisputes();
protected abstract String getDisputeInfo(Dispute dispute);
@ -299,157 +304,26 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
// Dispute handling
///////////////////////////////////////////////////////////////////////////////////////////
// arbitrator receives that from trader who opens dispute
protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return;
}
// trader sends message to arbitrator to open dispute
public void sendDisputeOpenedMessage(Dispute dispute,
boolean reOpen,
String updatedMultisigHex,
ResultHandler resultHandler,
FaultHandler faultHandler) {
Dispute dispute = openNewDisputeMessage.getDispute();
log.info("{}.onOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
dispute.setSupportType(openNewDisputeMessage.getSupportType());
// disputes from clients < 1.6.0 have state not set as the field didn't exist before
dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
synchronized (trade) {
String errorMessage = null;
Contract contract = dispute.getContract();
addPriceInfoMessage(dispute, 0);
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
if (isAgent(dispute)) {
// update arbitrator's multisig wallet
trade.syncWallet();
trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
trade.saveWallet();
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing);
} else {
// valid case if both have opened a dispute and agent was not online.
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
dispute.getTradeId());
}
} else {
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
log.warn(errorMessage);
}
}
} else {
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
log.error(errorMessage);
}
// We use the ChatMessage not the openNewDisputeMessage for the ACK
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0);
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
}
addMediationResultMessage(dispute);
try {
TradeDataValidation.validatePaymentAccountPayload(dispute);
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
} catch (TradeDataValidation.AddressException |
TradeDataValidation.NodeAddressException |
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
log.error(e.toString());
validationExceptions.add(e);
}
requestPersistence();
}
}
// Not-dispute-requester receives that msg from dispute agent
protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return;
}
String errorMessage = null;
Dispute dispute = peerOpenedDisputeMessage.getDispute();
log.info("{}.onPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
Optional<Trade> optionalTrade = tradeManager.getOpenTrade(dispute.getTradeId());
if (!optionalTrade.isPresent()) {
return;
}
Trade trade = optionalTrade.get();
synchronized (trade) {
if (!isAgent(dispute)) {
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.setDisputeState(getDisputeStateStartedByPeer());
tradeManager.requestPersistence();
errorMessage = null;
} else {
// valid case if both have opened a dispute and agent was not online.
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
dispute.getTradeId());
}
} else {
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
log.warn(errorMessage);
}
}
} else {
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
log.error(errorMessage);
}
// We use the ChatMessage not the peerOpenedDisputeMessage for the ACK
ObservableList<ChatMessage> messages = peerOpenedDisputeMessage.getDispute().getChatMessages();
if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0);
sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
}
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
requestPersistence();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Send message
///////////////////////////////////////////////////////////////////////////////////////////
public void sendOpenNewDisputeMessage(Dispute dispute,
boolean reOpen,
String updatedMultisigHex,
ResultHandler resultHandler,
FaultHandler faultHandler) {
log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
log.info("Sending {} for {} {}, dispute {}",
DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(),
dispute.getTradeId(), dispute.getId());
T disputeList = getDisputeList();
if (disputeList == null) {
@ -469,8 +343,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (!storedDisputeOptional.isPresent() || reOpen) {
String disputeInfo = getDisputeInfo(dispute);
String sysMsg = dispute.isSupportTicket() ?
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
@ -486,31 +360,33 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute,
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType(),
updatedMultisigHex);
updatedMultisigHex,
trade.getBuyer().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
"chatMessage.uid={}",
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
dispute.getAgentPubKeyRing(),
openNewDisputeMessage,
disputeOpenedMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
"chatMessage.uid={}",
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence();
resultHandler.handleResult();
}
@ -519,13 +395,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
public void onStoredInMailbox() {
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence();
resultHandler.handleResult();
}
@ -533,9 +410,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}",
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
"chatMessage.uid={}, errorMessage={}",
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
chatMessage.getUid(), errorMessage);
// We use the chatMessage wrapped inside the openNewDisputeMessage for
@ -545,8 +422,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
faultHandler.handleFault("Sending dispute message failed: " +
errorMessage, new DisputeMessageDeliveryFailedException());
}
}
);
});
} else {
String msg = "We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId();
@ -558,10 +434,111 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
requestPersistence();
}
// Dispute agent sends that to trading peer when he received openDispute request
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
Dispute dispute = message.getDispute();
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
// intialize
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return;
}
dispute.setSupportType(message.getSupportType());
dispute.setState(Dispute.State.NEW); // TODO: unused, remove?
Contract contract = dispute.getContract();
// validate dispute
try {
TradeDataValidation.validatePaymentAccountPayload(dispute);
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config);
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config);
} catch (TradeDataValidation.AddressException |
TradeDataValidation.NodeAddressException |
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
log.error(e.toString());
validationExceptions.add(e);
}
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
// get sender
PubKeyRing senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// message to trader is expected from arbitrator
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
}
// arbitrator verifies signature of payment sent message if given
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
trade.setStateIfProgress(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
}
// update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// update peer node address
// TODO: tests can reuse the same addresses so nullify equal peer
sender.setNodeAddress(message.getSenderNodeAddress());
// add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
// add dispute
String errorMessage = null;
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
// send dispute opened message to peer if arbitrator
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
tradeManager.requestPersistence();
errorMessage = null;
} else {
// valid case if both have opened a dispute and agent was not online
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
dispute.getTradeId());
}
} else {
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
log.warn(errorMessage);
}
}
// use chat message instead of open dispute message for the ack
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0);
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
}
// add chat message with mediation info if applicable // TODO: not applicable in haveno
addMediationResultMessage(dispute);
requestPersistence();
}
// arbitrator sends dispute opened message to opener's peer
private void sendDisputeOpenedMessageToPeer(Dispute disputeFromOpener,
Contract contractFromOpener,
PubKeyRing pubKeyRing) {
PubKeyRing pubKeyRing,
String updatedMultisigHex) {
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
@ -569,13 +546,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// from the code below.
UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener,
contractFromOpener,
pubKeyRing),
pubKeyRing,
updatedMultisigHex),
100, TimeUnit.MILLISECONDS);
}
private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
Contract contractFromOpener,
PubKeyRing pubKeyRing) {
PubKeyRing pubKeyRing,
String updatedMultisigHex) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
@ -638,14 +617,23 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeList.add(dispute);
}
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
// We mirrored dispute already!
Contract contract = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute,
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType());
getSupportType(),
updatedMultisigHex,
trade.getSelf().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
@ -701,8 +689,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
requestPersistence();
}
// arbitrator send result to trader
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) {
// arbitrator sends result to trader when their dispute is closed
public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
@ -720,75 +708,114 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage);
NodeAddress peersNodeAddress;
Contract contract = dispute.getContract();
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()))
peersNodeAddress = contract.getBuyerNodeAddress();
else
peersNodeAddress = contract.getSellerNodeAddress();
DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult,
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
// create unsigned dispute payout tx if not already published and arbitrator has trader's updated multisig info
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) {
// import multisig hex
MoneroWallet multisigWallet = trade.getWallet();
List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
if (!updatedMultisigHexes.isEmpty()) {
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
trade.syncWallet();
trade.saveWallet();
}
// create unsigned dispute payout tx
if (!trade.isPayoutPublished()) {
log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId());
try {
MoneroTxWallet payoutTx = createDisputePayoutTx(trade, dispute, disputeResult, multisigWallet);
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} catch (Exception e) {
if (!trade.isPayoutPublished()) throw e;
}
}
}
// create dispute closed message
String unsignedPayoutTxHex = receiver.getUpdatedMultisigHex() == null ? null : trade.getPayoutTxHex();
TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType());
log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(),
disputeResultMessage.getUid(), chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
getSupportType(),
trade.getSelf().getUpdatedMultisigHex(),
trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
// send dispute closed message
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(),
disputeClosedMessage.getUid(), chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(),
dispute.getTraderPubKeyRing(),
disputeResultMessage,
disputeClosedMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " +
log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// TODO: hack to sync wallet after dispute message received in order to detect payout published
Trade trade = tradeManager.getTrade(dispute.getTradeId());
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
for (int i = 0; i < 3; i++) {
UserThread.runAfter(() -> {
if (!trade.isPayoutUnlocked()) trade.syncWallet();
}, defaultRefreshPeriod / 1000 * (i + 1));
}
// We use the chatMessage wrapped inside the disputeResultMessage for
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.syncWalletNormallyForMs(30000);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onStoredInMailbox() {
log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " +
log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the disputeResultMessage for
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " +
log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}",
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid(), errorMessage);
// We use the chatMessage wrapped inside the disputeResultMessage for
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence();
resultHandler.handleResult();
}
}
);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence();
}
@ -796,6 +823,52 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
// multisig wallet must be synced
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
// collect winner and loser payout address and amounts
Contract contract = dispute.getContract();
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
// create transaction to get fee estimate
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++;
try {
payoutTx = multisigWallet.createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
log.info("Dispute payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
return payoutTx;
}
private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
PubKeyRing receiverPubKeyRing = null;
NodeAddress peerNodeAddress = null;
@ -878,15 +951,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
if (dispute.getMediatorsDisputeResult() != null) {
String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult());
ChatMessage mediatorsDisputeResultMessage = new ChatMessage(
ChatMessage mediatorsDisputeClosedMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
pubKeyRing.hashCode(),
false,
mediatorsDisputeResult,
p2PService.getAddress());
mediatorsDisputeResultMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage);
mediatorsDisputeClosedMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(mediatorsDisputeClosedMessage);
requestPersistence();
}
}

View file

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

View file

@ -22,7 +22,6 @@ import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportType;
@ -30,17 +29,12 @@ import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeResult.Winner;
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
import bisq.core.support.dispute.messages.ArbitratorPayoutTxRequest;
import bisq.core.support.dispute.messages.ArbitratorPayoutTxResponse;
import bisq.core.support.dispute.messages.DisputeResultMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.dispute.messages.DisputeClosedMessage;
import bisq.core.support.dispute.messages.DisputeOpenedMessage;
import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract;
import bisq.core.trade.Tradable;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.util.ParsingUtils;
@ -48,24 +42,20 @@ import bisq.core.util.ParsingUtils;
import bisq.network.p2p.AckMessageSourceType;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.network.p2p.SendMailboxMessageListener;
import common.utils.GenUtils;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@ -73,11 +63,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.common.MoneroError;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet;
@ -122,20 +110,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Received {} from {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
if (message instanceof OpenNewDisputeMessage) {
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
} else if (message instanceof PeerOpenedDisputeMessage) {
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
if (message instanceof DisputeOpenedMessage) {
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof ChatMessage) {
onChatMessage((ChatMessage) message);
} else if (message instanceof DisputeResultMessage) {
onDisputeResultMessage((DisputeResultMessage) message);
} else if (message instanceof PeerPublishedDisputePayoutTxMessage) {
onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message);
} else if (message instanceof ArbitratorPayoutTxRequest) {
onArbitratorPayoutTxRequest((ArbitratorPayoutTxRequest) message);
} else if (message instanceof ArbitratorPayoutTxResponse) {
onArbitratorPayoutTxResponse((ArbitratorPayoutTxResponse) message);
handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeClosedMessage) {
handleDisputeClosedMessage((DisputeClosedMessage) message);
} else {
log.warn("Unsupported message at dispatchMessage. message={}", message);
}
@ -147,11 +127,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
return dispute.getContract().getArbitratorNodeAddress();
}
@Override
protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.DISPUTE_STARTED_BY_PEER;
}
@Override
protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.ARBITRATION_MESSAGE;
@ -159,7 +134,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
@Override
public void cleanupDisputes() {
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED));
// no action
}
@Override
@ -185,43 +160,52 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
// Dispute handling
///////////////////////////////////////////////////////////////////////////////////////////
// received by both peers when arbitrator closes disputes
@Override
// We get that message at both peers. The dispute object is in context of the trader
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null");
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId());
String tradeId = disputeResult.getTradeId();
log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId());
// get trade
Trade trade = tradeManager.getTrade(tradeId);
if (trade == null) {
log.warn("Dispute trade {} does not exist", tradeId);
return;
}
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
// get dispute
Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeResultMessage.getUid();
String uid = disputeClosedMessage.getUid();
if (!disputeOptional.isPresent()) {
log.warn("We got a dispute result msg but we don't have a matching dispute. " +
"That might happen when we get the disputeResultMessage before the dispute was created. " +
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
"That might happen when we get the DisputeClosedMessage before the dispute was created. " +
"We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2);
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeClosedMessage), 2);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
return;
}
Dispute dispute = disputeOptional.get();
// verify that arbitrator does not get DisputeResultMessage
// verify that arbitrator does not get DisputeClosedMessage
if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) {
log.error("Arbitrator received disputeResultMessage. That must never happen.");
return;
log.error("Arbitrator received disputeResultMessage. That should never happen.");
return;
}
// set dispute state
cleanupRetryMap(uid);
if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage);
@ -229,492 +213,139 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
}
dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) {
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
"again because the first close did not succeed. TradeId = " + tradeId);
}
dispute.setDisputeResult(disputeResult);
String errorMessage = null;
boolean success = true;
boolean requestUpdatedPayoutTx = false;
Contract contract = dispute.getContract();
try {
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb)
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
// more BTC as he has deposited
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
DisputeResult.Winner publisher = disputeResult.getWinner();
// Sometimes the user who receives the trade amount is never online, so we might want to
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
// Default isLoserPublisher is set to false
if (disputeResult.isLoserPublisher()) {
// we invert the logic
if (publisher == DisputeResult.Winner.BUYER)
publisher = DisputeResult.Winner.SELLER;
else if (publisher == DisputeResult.Winner.SELLER)
publisher = DisputeResult.Winner.BUYER;
}
// import multisig hex
List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)) {
// sync and save wallet
trade.syncWallet();
trade.saveWallet();
MoneroTxWallet payoutTx = null;
if (tradeOptional.isPresent()) {
payoutTx = tradeOptional.get().getPayoutTx();
} else {
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); // TODO (woodser): payout tx is transient so won't exist after restart?
}
// run off main thread
new Thread(() -> {
String errorMessage = null;
boolean success = true;
// attempt to sign and publish dispute payout tx if given and not already published
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
// wait to sign and publish payout tx if defer flag set
if (disputeClosedMessage.isDeferPublishPayout()) {
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
trade.syncWallet();
}
// sign and publish dispute payout tx if peer still has not published
if (!trade.isPayoutPublished()) {
try {
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
signAndPublishDisputePayoutTx(trade, disputeClosedMessage.getUnsignedPayoutTxHex());
} catch (Exception e) {
if (payoutTx == null) {
// gather relevant info
String arbitratorSignedPayoutTxHex = disputeResult.getArbitratorSignedPayoutTxHex();
if (arbitratorSignedPayoutTxHex != null) {
if (!tradeOptional.isPresent()) throw new RuntimeException("Trade must not be null when trader signs arbitrator's payout tx");
try {
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
onTraderSignedDisputePayoutTx(tradeId, txSet);
} catch (Exception e) {
e.printStackTrace();
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId + " SignedPayoutTx = " + arbitratorSignedPayoutTxHex;
log.warn(errorMessage);
success = false;
}
} else {
requestUpdatedPayoutTx = true;
}
// check if payout published again
trade.syncWallet();
if (trade.isPayoutPublished()) {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else {
e.printStackTrace();
errorMessage = "Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
log.warn(errorMessage);
success = false;
}
}
} else {
log.warn("We already got a payout tx. That might be the case if the other peer did not get the " +
"payout tx and opened a dispute. TradeId = " + tradeId);
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
}
} else {
log.trace("We don't publish the tx as we are not the winning party.");
// Clean up tangling trades
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
closeTradeOrOffer(tradeId);
}
if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getName(), trade.getId());
}
}
// catch (TransactionVerificationException e) {
// errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
// log.error(errorMessage, e);
// success = false;
//
// // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
// // we get a TransactionVerificationException. No reason to keep that dispute open...
// updateTradeOrOpenOfferManager(tradeId);
//
// throw new RuntimeException(errorMessage);
// }
// catch (AddressFormatException | WalletException e) {
catch (Exception e) {
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
log.error(errorMessage, e);
success = false;
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
// we get a TransactionVerificationException. No reason to keep that dispute open...
closeTradeOrOffer(tradeId); // TODO (woodser): only close in case of verification exception?
throw new RuntimeException(errorMessage);
} finally {
// We use the chatMessage as we only persist those not the disputeResultMessage.
// If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage.
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
// If dispute opener's peer is co-signer, send updated multisig hex to arbitrator to receive updated payout tx
if (requestUpdatedPayoutTx) {
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // TODO (woodser): this is closed after sending ArbitratorPayoutTxRequest to arbitrator which opens and syncs multisig and responds with signed dispute tx. more efficient way is to include with arbitrator-signed dispute tx with dispute result?
sendArbitratorPayoutTxRequest(multisigWallet.exportMultisigHex(), dispute, contract);
}
}
}
requestPersistence();
}
// Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
String uid = peerPublishedDisputePayoutTxMessage.getUid();
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
// get dispute and trade
Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) {
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 3 sec. to be sure the close msg gets added first
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
return;
}
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
cleanupRetryMap(uid);
// update trade wallet
MoneroWallet wallet = trade.getWallet();
if (wallet != null) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
trade.syncWallet();
wallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
trade.saveWallet();
MoneroTxWallet parsedPayoutTx = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
}
// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs());
// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx");
// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet());
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
requestPersistence();
}
}).start();
}
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
// TODO: this should be invoked from mailbox message and send mailbox message response to support offline arbitrator
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
String tradeId = request.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
Contract contract = dispute.getContract();
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) {
// verify sender is co-signer and receiver is arbitrator
// System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first
// System.out.println(disputeResult);
// System.out.println(disputeResult.getWinner());
// System.out.println(contract.getBuyerNodeAddress());
// System.out.println(contract.getSellerNodeAddress());
boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress()));
boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher();
boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing());
if (!senderIsCosigner) {
log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId);
return;
}
if (!receiverIsArbitrator) {
log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId);
return;
}
// update arbitrator's multisig wallet with co-signer's multisig hex
trade.syncWallet();
MoneroWallet multisigWallet = trade.getWallet();
try {
multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
trade.saveWallet();
} catch (Exception e) {
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
return;
}
// create updated payout tx
MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
System.out.println("Arbitrator created updated payout tx for co-signer!!!");
System.out.println(payoutTx);
// send updated payout tx to sender
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
tradeId,
p2PService.getAddress(),
UUID.randomUUID().toString(),
SupportType.ARBITRATION,
payoutTx.getTxSet().getMultisigTxHex());
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(),
senderPubKeyRing,
response,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}",
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
// TODO: hack to sync wallet after dispute message received in order to detect payout published
Trade trade = tradeManager.getTrade(dispute.getTradeId());
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
for (int i = 0; i < 3; i++) {
UserThread.runAfter(() -> {
if (!trade.isPayoutUnlocked()) trade.syncWallet();
}, defaultRefreshPeriod / 1000 * (i + 1));
}
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage);
}
}
);
}
}
// Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer)
private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) {
log.info("{}.onArbitratorPayoutTxResponse()", getClass().getSimpleName());
// gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc
String tradeId = response.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
// verify and sign dispute payout tx
MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex());
// process fully signed payout tx (publish, notify peer, etc)
onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx);
}
}
private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) {
// gather trade info
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
multisigWallet.sync();
Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
// gather trade info
MoneroWallet multisigWallet = trade.getWallet();
Optional<Dispute> disputeOptional = findDispute(trade.getId());
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + trade.getId());
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
// parse arbitrator-signed payout tx
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0);
log.info("Received updated multisig hex and partially signed payout tx from arbitrator:\n" + arbitratorSignedPayoutTx);
// parse arbitrator-signed payout tx
MoneroTxSet signedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0);
// verify payout tx has 1 or 2 destinations
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations");
// verify payout tx has 1 or 2 destinations
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations");
// get buyer and seller destinations (order not preserved)
List<MoneroDestination> destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations();
boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null;
MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0);
// get buyer and seller destinations (order not preserved)
List<MoneroDestination> destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations();
boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null;
MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0);
// verify payout addresses
if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify payout addresses
if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify change address is multisig's primary address
if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// verify sum of outputs = destination amounts + change amount
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
// verify winner and loser payout amounts
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
// verify winner and loser payout amounts
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
// update multisig wallet from arbitrator
multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex());
xmrWalletService.saveWallet(multisigWallet);
// sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex();
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
// sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex();
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
return parsedTxSet;
}
// submit fully signed payout tx to the network
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) {
// gather trade info
Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) {
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
return;
}
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
Trade trade = tradeManager.getOpenTrade(tradeId).get();
// submit fully signed payout tx to the network
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // closed when trade completed (TradeManager.onTradeCompleted())
List<String> txHashes = multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex());
txSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
// update state
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
trade.setPayoutTxId(txSet.getTxs().get(0).getHash());
trade.setPayoutState(Trade.PayoutState.PUBLISHED);
dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash());
sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract);
closeTradeOrOffer(tradeId);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Send messages
///////////////////////////////////////////////////////////////////////////////////////////
// winner (or buyer in case of 50/50) sends tx to other peer
private void sendPeerPublishedPayoutTxMessage(String updatedMultisigHex, String payoutTxHex, Dispute dispute, Contract contract) {
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress);
PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(updatedMultisigHex,
payoutTxHex,
dispute.getTradeId(),
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType());
log.info("Send {} to peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
peersPubKeyRing,
message,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
}
@Override
public void onStoredInMailbox() {
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
}
}
);
}
public void closeTradeOrOffer(String tradeId) {
// set state after payout as we call swapTradeEntryToAvailableEntry
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
}
// dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx
private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) {
ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest(
dispute,
p2PService.getAddress(),
UUID.randomUUID().toString(),
SupportType.ARBITRATION,
updatedMultisigHex);
log.info("Send {} to peer {}. tradeId={}, uid={}",
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid());
p2PService.sendEncryptedDirectMessage(contract.getArbitratorNodeAddress(),
dispute.getAgentPubKeyRing(),
request,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}",
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid());
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
request.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), request.getUid(), errorMessage);
}
}
);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Disputed payout tx signing
///////////////////////////////////////////////////////////////////////////////////////////
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
// multisig wallet must be synced
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
// collect winner and loser payout address and amounts
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
// create transaction to get fee estimate
// TODO (woodser): include arbitration fee
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++;
try {
payoutTx = multisigWallet.createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
return payoutTx;
// update state
trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash());
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash());
return signedTxSet;
}
}

View file

@ -1,112 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.arbitration.messages;
import bisq.core.support.SupportType;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public final class PeerPublishedDisputePayoutTxMessage extends ArbitrationMessage {
private final String updatedMultisigHex;
private final String payoutTxHex;
private final String tradeId;
private final NodeAddress senderNodeAddress;
public PeerPublishedDisputePayoutTxMessage(String updatedMultisigHex,
String payoutTxHex,
String tradeId,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType) {
this(updatedMultisigHex,
payoutTxHex,
tradeId,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PeerPublishedDisputePayoutTxMessage(String updatedMultisigHex,
String payoutTxHex,
String tradeId,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType) {
super(messageVersion, uid, supportType);
this.updatedMultisigHex = updatedMultisigHex;
this.payoutTxHex = payoutTxHex;
this.tradeId = tradeId;
this.senderNodeAddress = senderNodeAddress;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setPeerPublishedDisputePayoutTxMessage(protobuf.PeerPublishedDisputePayoutTxMessage.newBuilder()
.setUpdatedMultisigHex(updatedMultisigHex)
.setPayoutTxHex(payoutTxHex)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid)
.setType(SupportType.toProtoMessage(supportType)))
.build();
}
public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto,
String messageVersion) {
return new PeerPublishedDisputePayoutTxMessage(proto.getUpdatedMultisigHex(),
proto.getPayoutTxHex(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()));
}
@Override
public String getTradeId() {
return tradeId;
}
@Override
public String toString() {
return "PeerPublishedDisputePayoutTxMessage{" +
"\n updatedMultisigHex=" + updatedMultisigHex +
"\n payoutTxHex=" + payoutTxHex +
",\n tradeId='" + tradeId + '\'' +
",\n senderNodeAddress=" + senderNodeAddress +
",\n PeerPublishedDisputePayoutTxMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
"\n} " + super.toString();
}
}

View file

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

View file

@ -1,107 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import lombok.EqualsAndHashCode;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Value
public final class ArbitratorPayoutTxRequest extends DisputeMessage {
private final Dispute dispute;
private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex;
public ArbitratorPayoutTxRequest(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType,
String updatedMultisigHex) {
this(dispute,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType,
updatedMultisigHex);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private ArbitratorPayoutTxRequest(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType,
String updatedMultisigHex) {
super(messageVersion, uid, supportType);
this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setArbitratorPayoutTxRequest(protobuf.ArbitratorPayoutTxRequest.newBuilder()
.setUid(uid)
.setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex))
.build();
}
public static ArbitratorPayoutTxRequest fromProto(protobuf.ArbitratorPayoutTxRequest proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new ArbitratorPayoutTxRequest(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()),
proto.getUpdatedMultisigHex());
}
@Override
public String getTradeId() {
return dispute.getTradeId();
}
@Override
public String toString() {
return "ArbitratorPayoutTxRequest{" +
"\n dispute=" + dispute +
",\n senderNodeAddress=" + senderNodeAddress +
",\n ArbitratorPayoutTxRequest.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
",\n updatedMultisigHex=" + updatedMultisigHex +
"\n} " + super.toString();
}
}

View file

@ -1,101 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import lombok.EqualsAndHashCode;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Value
public final class ArbitratorPayoutTxResponse extends DisputeMessage {
private final String tradeId;
private final NodeAddress senderNodeAddress;
private final String arbitratorSignedPayoutTxHex;
public ArbitratorPayoutTxResponse(String tradeId,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType,
String arbitratorSignedPayoutTxHex) {
this(tradeId,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType,
arbitratorSignedPayoutTxHex);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private ArbitratorPayoutTxResponse(String tradeId,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType,
String arbitratorSignedPayoutTxHex) {
super(messageVersion, uid, supportType);
this.tradeId = tradeId;
this.senderNodeAddress = senderNodeAddress;
this.arbitratorSignedPayoutTxHex = arbitratorSignedPayoutTxHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setArbitratorPayoutTxResponse(protobuf.ArbitratorPayoutTxResponse.newBuilder()
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType))
.setArbitratorSignedPayoutTxHex(arbitratorSignedPayoutTxHex))
.build();
}
public static ArbitratorPayoutTxResponse fromProto(protobuf.ArbitratorPayoutTxResponse proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new ArbitratorPayoutTxResponse(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()),
proto.getArbitratorSignedPayoutTxHex());
}
@Override
public String toString() {
return "ArbitratorPayoutTxResponse{" +
"\n tradeId=" + tradeId +
",\n senderNodeAddress=" + senderNodeAddress +
",\n ArbitratorPayoutTxResponse.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
",\n updatedMultisigHex=" + arbitratorSignedPayoutTxHex +
"\n} " + super.toString();
}
}

View file

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

View file

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

View file

@ -1,97 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public final class PeerOpenedDisputeMessage extends DisputeMessage {
private final Dispute dispute;
private final NodeAddress senderNodeAddress;
public PeerOpenedDisputeMessage(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType) {
this(dispute,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PeerOpenedDisputeMessage(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType) {
super(messageVersion, uid, supportType);
this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setPeerOpenedDisputeMessage(protobuf.PeerOpenedDisputeMessage.newBuilder()
.setUid(uid)
.setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType)))
.build();
}
public static PeerOpenedDisputeMessage fromProto(protobuf.PeerOpenedDisputeMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) {
return new PeerOpenedDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()));
}
@Override
public String getTradeId() {
return dispute.getTradeId();
}
@Override
public String toString() {
return "PeerOpenedDisputeMessage{" +
"\n dispute=" + dispute +
",\n senderNodeAddress=" + senderNodeAddress +
",\n PeerOpenedDisputeMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
"\n} " + super.toString();
}
}

View file

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

View file

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

View file

@ -24,7 +24,11 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
@ -32,9 +36,12 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Charsets;
/**
* Collection of utilities.
*/
@Slf4j
public class HavenoUtils {
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
@ -73,10 +80,10 @@ public class HavenoUtils {
}
/**
* Check if the arbitrator signature for an offer is valid.
* Check if the arbitrator signature is valid for an offer.
*
* @param offer is a signed offer with payload
* @param arbitrator is the possible original arbitrator
* @param arbitrator is the original signing arbitrator
* @return true if the arbitrator's signature is valid for the offer
*/
public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) {
@ -92,15 +99,11 @@ public class HavenoUtils {
String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy);
// verify arbitrator signature
boolean isValid = true;
try {
isValid = Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
} catch (Exception e) {
isValid = false;
return false;
}
// return result
return isValid;
}
/**
@ -149,6 +152,71 @@ public class HavenoUtils {
}
}
/**
* Verify the buyer signature for a PaymentSentMessage.
*
* @param trade - the trade to verify
* @param message - signed payment sent message to verify
* @return true if the buyer's signature is valid for the message
*/
public static void verifyPaymentSentMessage(Trade trade, PaymentSentMessage message) {
// remove signature from message
byte[] signature = message.getBuyerSignature();
message.setBuyerSignature(null);
// get unsigned message as json string
String unsignedMessageAsJson = JsonUtil.objectToJson(message);
// replace signature
message.setBuyerSignature(signature);
// verify signature
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId();
try {
if (!Sig.verify(trade.getBuyer().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
} catch (Exception e) {
throw new RuntimeException(errMessage);
}
// verify trade id
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
}
/**
* Verify the seller signature for a PaymentReceivedMessage.
*
* @param trade - the trade to verify
* @param message - signed payment received message to verify
* @return true if the seller's signature is valid for the message
*/
public static void verifyPaymentReceivedMessage(Trade trade, PaymentReceivedMessage message) {
// remove signature from message
byte[] signature = message.getSellerSignature();
message.setSellerSignature(null);
// get unsigned message as json string
String unsignedMessageAsJson = JsonUtil.objectToJson(message);
// replace signature
message.setSellerSignature(signature);
// verify signature
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for trade " + trade.getId();
try {
if (!Sig.verify(trade.getSeller().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
} catch (Exception e) {
throw new RuntimeException(errMessage);
}
// verify trade id
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
// verify buyer signature of payment sent message
verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
}
public static void awaitLatch(CountDownLatch latch) {
try {
latch.await();
@ -156,14 +224,18 @@ public class HavenoUtils {
throw new RuntimeException(e);
}
}
public static void awaitTasks(Collection<Runnable> tasks) {
public static void executeTasks(Collection<Runnable> tasks) {
executeTasks(tasks, tasks.size());
}
public static void executeTasks(Collection<Runnable> tasks, int poolSize) {
if (tasks.isEmpty()) return;
ExecutorService pool = Executors.newFixedThreadPool(tasks.size());
ExecutorService pool = Executors.newFixedThreadPool(poolSize);
for (Runnable task : tasks) pool.submit(task);
pool.shutdown();
try {
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow();
} catch (InterruptedException e) {
pool.shutdownNow();
throw new RuntimeException(e);

View file

@ -73,7 +73,11 @@ public abstract class SellerTrade extends Trade {
return true;
case DISPUTE_REQUESTED:
case DISPUTE_STARTED_BY_PEER:
case DISPUTE_OPENED:
case ARBITRATOR_SENT_DISPUTE_CLOSED_MSG:
case ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG:
case ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG:
case ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG:
case DISPUTE_CLOSED:
case MEDIATION_REQUESTED:
case MEDIATION_STARTED_BY_PEER:

View file

@ -35,6 +35,7 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.ProcessModel;
import bisq.core.trade.protocol.ProcessModelServiceProvider;
import bisq.core.trade.protocol.TradeListener;
import bisq.core.trade.protocol.TradeProtocol;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.trade.txproof.AssetTxProofResult;
import bisq.core.util.ParsingUtils;
@ -44,6 +45,7 @@ import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.crypto.Encryption;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import bisq.common.taskrunner.Model;
import bisq.common.util.Utilities;
@ -72,6 +74,7 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import lombok.Getter;
@ -214,10 +217,10 @@ public abstract class Trade implements Tradable, Model {
}
public enum PayoutState {
UNPUBLISHED,
PUBLISHED,
CONFIRMED,
UNLOCKED;
PAYOUT_UNPUBLISHED,
PAYOUT_PUBLISHED,
PAYOUT_CONFIRMED,
PAYOUT_UNLOCKED;
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
@ -234,9 +237,12 @@ public abstract class Trade implements Tradable, Model {
public enum DisputeState {
NO_DISPUTE,
// arbitration
DISPUTE_REQUESTED,
DISPUTE_STARTED_BY_PEER,
DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager
DISPUTE_OPENED,
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG,
ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG,
ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG,
DISPUTE_CLOSED,
// mediation
@ -268,12 +274,12 @@ public abstract class Trade implements Tradable, Model {
}
public boolean isArbitrated() {
return this == Trade.DisputeState.DISPUTE_REQUESTED ||
this == Trade.DisputeState.DISPUTE_STARTED_BY_PEER ||
this == Trade.DisputeState.DISPUTE_CLOSED ||
this == Trade.DisputeState.REFUND_REQUESTED ||
this == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER ||
this == Trade.DisputeState.REFUND_REQUEST_CLOSED;
if (isMediated()) return false; // TODO: remove mediation?
return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
}
public boolean isClosed() {
return this == DisputeState.DISPUTE_CLOSED;
}
}
@ -324,7 +330,7 @@ public abstract class Trade implements Tradable, Model {
@Getter
private State state = State.PREPARATION;
@Getter
private PayoutState payoutState = PayoutState.UNPUBLISHED;
private PayoutState payoutState = PayoutState.PAYOUT_UNPUBLISHED;
@Getter
private DisputeState disputeState = DisputeState.NO_DISPUTE;
@Getter
@ -365,11 +371,13 @@ public abstract class Trade implements Tradable, Model {
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
transient private Subscription tradePhaseSubscription = null;
transient private Subscription payoutStateSubscription = null;
transient private Subscription tradePhaseSubscription;
transient private Subscription payoutStateSubscription;
transient private TaskLooper tradeTxsLooper;
transient private Long lastWalletRefreshPeriod;
transient private Long walletRefreshPeriod;
transient private Long syncNormalStartTime;
private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds
// Mutable
@Getter
@ -435,8 +443,9 @@ public abstract class Trade implements Tradable, Model {
private String payoutTxKey;
private Long startTime; // cache
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization
// Constructors
///////////////////////////////////////////////////////////////////////////////////////////
// maker
@ -530,96 +539,56 @@ public abstract class Trade implements Tradable, Model {
setAmount(tradeAmount);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public Message toProtoMessage() {
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
.setOffer(offer.toProtoMessage())
.setTxFeeAsLong(txFeeAsLong)
.setTakerFeeAsLong(takerFeeAsLong)
.setTakeOfferDate(takeOfferDate)
.setProcessModel(processModel.toProtoMessage())
.setAmountAsLong(amountAsLong)
.setPrice(price)
.setState(Trade.State.toProtoMessage(state))
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
.addAllChatMessage(chatMessages.stream()
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
.collect(Collectors.toList()))
.setLockTime(lockTime)
.setUid(uid);
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage()));
Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson);
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash)));
Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage);
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
return builder.build();
public void addListener(TradeListener listener) {
tradeListeners.add(listener);
}
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
trade.setTakeOfferDate(proto.getTakeOfferDate());
trade.setState(State.fromProto(proto.getState()));
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()));
trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey()));
trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null);
trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()));
trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()));
trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()));
trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId());
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
trade.setLockTime(proto.getLockTime());
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
// We do not want to show the user the last pending state when he starts up the app again, so we clear it.
if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) {
persistedAssetTxProofResult = null;
public void removeListener(TradeListener listener) {
if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered");
}
// notified from TradeProtocol of verified trade messages
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onVerifiedTradeMessage(message, sender);
}
trade.setAssetTxProofResult(persistedAssetTxProofResult);
trade.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList()));
return trade;
}
// notified from TradeProtocol of ack messages
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onAckMessage(ackMessage, sender);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(ProcessModelServiceProvider serviceProvider) {
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
});
isInitialized = true; // TODO: move to end?
// listen to daemon connection
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
// done if payout unlocked
// check if done
if (isPayoutUnlocked()) return;
// handle trade state events
if (isDepositPublished()) listenToTradeTxs();
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
updateTxListenerRefreshPeriod();
if (isDepositPublished()) listenToTradeTxs();
if (!isInitialized) return;
if (isDepositPublished() && !isPayoutUnlocked()) {
updateWalletRefreshPeriod();
listenToTradeTxs();
}
if (isCompleted()) {
UserThread.execute(() -> {
if (tradePhaseSubscription != null) {
@ -632,17 +601,23 @@ public abstract class Trade implements Tradable, Model {
// handle payout state events
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
updateTxListenerRefreshPeriod();
if (!isInitialized) return;
if (isPayoutPublished()) updateWalletRefreshPeriod();
// cleanup when payout published
if (isPayoutPublished()) {
if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) {
log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published
// complete disputed trade
if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) processModel.getTradeManager().closeDisputedTrade(getId(), Trade.DisputeState.DISPUTE_CLOSED);
// complete arbitrator trade
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this);
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
}
// cleanup when payout unlocks
if (isPayoutUnlocked()) {
if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) {
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
deleteWallet();
if (tradeTxsLooper != null) {
@ -657,12 +632,24 @@ public abstract class Trade implements Tradable, Model {
});
}
});
isInitialized = true;
// start listening to trade wallet
if (isDepositPublished()) {
updateWalletRefreshPeriod();
listenToTradeTxs();
// allow state notifications to process before returning
CountDownLatch latch = new CountDownLatch(1);
UserThread.execute(() -> latch.countDown());
HavenoUtils.awaitLatch(latch);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public TradeProtocol getProtocol() {
return processModel.getTradeManager().getTradeProtocol(this);
}
public void setMyNodeAddress() {
getSelf().setNodeAddress(P2PService.getMyNodeAddress());
@ -755,9 +742,11 @@ public abstract class Trade implements Tradable, Model {
// exception expected
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
log.info("Payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex
getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
return payoutTx;
}
@ -827,7 +816,7 @@ public abstract class Trade implements Tradable, Model {
// submit payout tx
if (publish) {
multisigWallet.submitMultisigTxHex(payoutTxHex);
setPayoutState(Trade.PayoutState.PUBLISHED);
setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
}
}
@ -921,10 +910,22 @@ public abstract class Trade implements Tradable, Model {
}
public void syncWallet() {
if (getWallet() == null) {
log.warn("Cannot sync multisig wallet because it doesn't exist for {}, {}", getClass().getSimpleName(), getId());
return;
}
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
getWallet().sync();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
pollWallet();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
}
public void syncWalletNormallyForMs(long syncNormalDuration) {
syncNormalStartTime = System.currentTimeMillis();
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
UserThread.runAfter(() -> {
if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
}, syncNormalDuration);
}
public void saveWallet() {
@ -938,6 +939,12 @@ public abstract class Trade implements Tradable, Model {
public void shutDown() {
isInitialized = false;
if (tradeTxsLooper != null) {
tradeTxsLooper.stop();
tradeTxsLooper = null;
}
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -958,32 +965,6 @@ public abstract class Trade implements Tradable, Model {
public abstract boolean confirmPermitted();
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners
///////////////////////////////////////////////////////////////////////////////////////////
public void addListener(TradeListener listener) {
tradeListeners.add(listener);
}
public void removeListener(TradeListener listener) {
if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered");
}
// notified from TradeProtocol of verified trade messages
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onVerifiedTradeMessage(message, sender);
}
}
// notified from TradeProtocol of ack messages
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onAckMessage(ackMessage, sender);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
@ -1031,7 +1012,7 @@ public abstract class Trade implements Tradable, Model {
public void setPayoutState(PayoutState payoutState) {
if (isInitialized) {
// We don't want to log at startup the setState calls from all persisted trades
log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState);
log.info("Set new payout state for {} {}: {}", this.getClass().getSimpleName(), getId(), payoutState);
}
if (payoutState.ordinal() < this.payoutState.ordinal()) {
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
@ -1046,8 +1027,24 @@ public abstract class Trade implements Tradable, Model {
}
public void setDisputeState(DisputeState disputeState) {
if (isInitialized) {
// We don't want to log at startup the setState calls from all persisted trades
log.info("Set new dispute state for {} {}: {}", this.getClass().getSimpleName(), getShortId(), disputeState);
}
if (disputeState.ordinal() < this.disputeState.ordinal()) {
String message = "We got a dispute state change to a previous state (id=" + getShortId() + ").\n" +
"Old dispute state is: " + this.disputeState + ". New dispute state is: " + disputeState;
log.warn(message);
}
this.disputeState = disputeState;
disputeStateProperty.set(disputeState);
UserThread.execute(() -> {
disputeStateProperty.set(disputeState);
});
}
public void setDisputeStateIfProgress(DisputeState disputeState) {
if (disputeState.ordinal() > getDisputeState().ordinal()) setDisputeState(disputeState);
}
public void setMediationResultState(MediationResultState mediationResultState) {
@ -1140,31 +1137,27 @@ public abstract class Trade implements Tradable, Model {
return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker();
}
/**
* Get the taker if maker, maker if taker, null if arbitrator.
*
* @return the trade peer
*/
// get the taker if maker, maker if taker, null if arbitrator
public TradingPeer getTradingPeer() {
if (this instanceof MakerTrade) return processModel.getTaker();
else if (this instanceof TakerTrade) return processModel.getMaker();
else if (this instanceof ArbitratorTrade) return null;
else throw new RuntimeException("Unknown trade type: " + getClass().getName());
if (this instanceof MakerTrade) return processModel.getTaker();
else if (this instanceof TakerTrade) return processModel.getMaker();
else if (this instanceof ArbitratorTrade) return null;
else throw new RuntimeException("Unknown trade type: " + getClass().getName());
}
/**
* Get the peer with the given address which can be self.
*
* TODO (woodser): this naming convention is confusing
*
* @param address is the address of the peer to get
* @return the trade peer
*/
// TODO (woodser): this naming convention is confusing
public TradingPeer getTradingPeer(NodeAddress address) {
if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker();
if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker();
if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator();
throw new RuntimeException("No trade participant with the given address. Their address might have changed: " + address);
return null;
}
public TradingPeer getTradingPeer(PubKeyRing pubKeyRing) {
if (getMaker() != null && getMaker().getPubKeyRing().equals(pubKeyRing)) return getMaker();
if (getTaker() != null && getTaker().getPubKeyRing().equals(pubKeyRing)) return getTaker();
if (getArbitrator() != null && getArbitrator().getPubKeyRing().equals(pubKeyRing)) return getArbitrator();
return null;
}
public Date getTakeOfferDate() {
@ -1210,12 +1203,10 @@ public abstract class Trade implements Tradable, Model {
private long getStartTime() {
if (startTime != null) return startTime;
long now = System.currentTimeMillis();
final MoneroTx takerDepositTx = getTakerDepositTx();
final MoneroTx makerDepositTx = getMakerDepositTx();
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
if (isDepositConfirmed() && getTakeOfferDate() != null) {
if (isDepositUnlocked()) {
final long tradeTime = getTakeOfferDate().getTime();
long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight());
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
@ -1233,7 +1224,7 @@ public abstract class Trade implements Tradable, Model {
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
new Date(startTime), new Date(tradeTime), new Date(blockTime));
} else {
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", makerDepositTx.getHash(), takerDepositTx.getHash());
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash());
startTime = now;
}
} else {
@ -1259,34 +1250,7 @@ public abstract class Trade implements Tradable, Model {
}
public boolean isFundsLockedIn() {
// If no deposit tx was published we have no funds locked in
if (!isDepositPublished()) {
return false;
}
// If we have the payout tx published (non disputed case) we have no funds locked in. Here we might have more
// complex cases where users open a mediation but continue the trade to finalize it without mediated payout.
// The trade state handles that but does not handle mediated payouts or refund agents payouts.
if (isPayoutPublished()) {
return false;
}
// check for closed disputed case
if (disputeState == DisputeState.DISPUTE_CLOSED) return false;
// In mediation case we check for the mediationResultState. As there are multiple sub-states we use ordinal.
if (disputeState == DisputeState.MEDIATION_CLOSED) {
if (mediationResultState != null &&
mediationResultState.ordinal() >= MediationResultState.PAYOUT_TX_PUBLISHED.ordinal()) {
return false;
}
}
// In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as
// locked in funds.
return disputeState != DisputeState.REFUND_REQUESTED &&
disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER &&
disputeState != DisputeState.REFUND_REQUEST_CLOSED;
return isDepositPublished() && !isPayoutPublished();
}
public boolean isDepositConfirmed() {
@ -1310,15 +1274,15 @@ public abstract class Trade implements Tradable, Model {
}
public boolean isPayoutPublished() {
return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal();
return getPayoutState().ordinal() >= PayoutState.PAYOUT_PUBLISHED.ordinal();
}
public boolean isPayoutConfirmed() {
return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal();
return getPayoutState().ordinal() >= PayoutState.PAYOUT_CONFIRMED.ordinal();
}
public boolean isPayoutUnlocked() {
return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal();
return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal();
}
public ReadOnlyObjectProperty<State> stateProperty() {
@ -1439,80 +1403,81 @@ public abstract class Trade implements Tradable, Model {
// poll wallet for tx state
pollWallet();
tradeTxsLooper = new TaskLooper(() -> {
try {
pollWallet();
} catch (Exception e) {
if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage());
}
});
tradeTxsLooper.start(getWalletRefreshPeriod());
tradeTxsLooper = new TaskLooper(() -> { pollWallet(); });
tradeTxsLooper.start(walletRefreshPeriod);
}
private void pollWallet() {
try {
// skip if payout unlocked
if (isPayoutUnlocked()) return;
// skip if payout unlocked
if (isPayoutUnlocked()) return;
// rescan spent if deposits unlocked
if (isDepositUnlocked()) getWallet().rescanSpent();
// rescan spent if deposits unlocked
if (isDepositUnlocked()) getWallet().rescanSpent();
// get txs with outputs
List<MoneroTxWallet> txs = getWallet().getTxs(new MoneroTxQuery()
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
.setIncludeOutputs(true));
// get txs with outputs
List<MoneroTxWallet> txs = getWallet().getTxs(new MoneroTxQuery()
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
.setIncludeOutputs(true));
// check deposit txs
if (!isDepositUnlocked()) {
if (txs.size() == 2) {
setStateDepositsPublished();
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// check deposit txs
if (!isDepositUnlocked()) {
if (txs.size() == 2) {
setStateDepositsPublished();
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// check if deposit txs confirmed
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
}
}
// check payout tx
else {
// check if deposit txs spent (appears on payout published)
for (MoneroTxWallet tx : txs) {
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (Boolean.TRUE.equals(output.isSpent())) {
setPayoutStatePublished();
}
// check if deposit txs confirmed
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
}
}
// check for outgoing txs (appears on payout confirmed)
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
if (!outgoingTxs.isEmpty()) {
MoneroTxWallet payoutTx = outgoingTxs.get(0);
setPayoutTx(payoutTx);
setPayoutStatePublished();
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
// check payout tx
else {
// check if deposit txs spent (appears on payout published)
for (MoneroTxWallet tx : txs) {
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (Boolean.TRUE.equals(output.isSpent())) {
setPayoutStatePublished();
}
}
}
// check for outgoing txs (appears on payout confirmed)
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
if (!outgoingTxs.isEmpty()) {
MoneroTxWallet payoutTx = outgoingTxs.get(0);
setPayoutTx(payoutTx);
setPayoutStatePublished();
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
}
}
} catch (Exception e) {
if (isInitialized) log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); // TODO (monero-java): poller.isPolling() and then don't need to use isInitialized here as shutdown flag
}
}
private void setDaemonConnection(MoneroRpcConnection connection) {
if (getWallet() == null) return;
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId());
multisigWallet.setDaemonConnection(connection);
multisigWallet.startSyncing(getWalletRefreshPeriod());
updateTxListenerRefreshPeriod();
getWallet().setDaemonConnection(connection);
updateWalletRefreshPeriod();
}
private void updateTxListenerRefreshPeriod() {
long walletRefreshPeriod = getWalletRefreshPeriod();
if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return;
log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod);
lastWalletRefreshPeriod = walletRefreshPeriod;
private void updateWalletRefreshPeriod() {
setWalletRefreshPeriod(getWalletRefreshPeriod());
}
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod);
this.walletRefreshPeriod = walletRefreshPeriod;
getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period
if (tradeTxsLooper != null) {
tradeTxsLooper.stop();
tradeTxsLooper = null;
@ -1521,8 +1486,8 @@ public abstract class Trade implements Tradable, Model {
}
private long getWalletRefreshPeriod() {
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // slow arbitrator trade wallet after deposits confirm
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // else sync at default rate
}
private void setStateDepositsPublished() {
@ -1538,15 +1503,87 @@ public abstract class Trade implements Tradable, Model {
}
private void setPayoutStatePublished() {
if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED);
if (!isPayoutPublished()) setPayoutState(PayoutState.PAYOUT_PUBLISHED);
}
private void setPayoutStateConfirmed() {
if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED);
if (!isPayoutConfirmed()) setPayoutState(PayoutState.PAYOUT_CONFIRMED);
}
private void setPayoutStateUnlocked() {
if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED);
if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public Message toProtoMessage() {
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
.setOffer(offer.toProtoMessage())
.setTxFeeAsLong(txFeeAsLong)
.setTakerFeeAsLong(takerFeeAsLong)
.setTakeOfferDate(takeOfferDate)
.setProcessModel(processModel.toProtoMessage())
.setAmountAsLong(amountAsLong)
.setPrice(price)
.setState(Trade.State.toProtoMessage(state))
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
.addAllChatMessage(chatMessages.stream()
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
.collect(Collectors.toList()))
.setLockTime(lockTime)
.setUid(uid);
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage()));
Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson);
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash)));
Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage);
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
return builder.build();
}
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
trade.setTakeOfferDate(proto.getTakeOfferDate());
trade.setState(State.fromProto(proto.getState()));
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()));
trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey()));
trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null);
trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()));
trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()));
trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()));
trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId());
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
trade.setLockTime(proto.getLockTime());
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
// We do not want to show the user the last pending state when he starts up the app again, so we clear it.
if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) {
persistedAssetTxProofResult = null;
}
trade.setAssetTxProofResult(persistedAssetTxProofResult);
trade.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList()));
return trade;
}
@Override

View file

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

View file

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

View file

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

View file

@ -25,12 +25,13 @@ import bisq.common.proto.ProtoUtil;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
@Getter
public final class PaymentSentMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress;
@Nullable
@ -41,6 +42,9 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
private final String updatedMultisigHex;
@Nullable
private final byte[] paymentAccountKey;
@Setter
@Nullable
private byte[] buyerSignature;
// Added after v1.3.7
// We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets.
@ -101,13 +105,14 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
Optional.ofNullable(buyerSignature).ifPresent(e -> builder.setBuyerSignature(ByteString.copyFrom(e)));
return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
}
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
String messageVersion) {
return new PaymentSentMessage(proto.getTradeId(),
PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
@ -117,6 +122,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())
);
message.setBuyerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getBuyerSignature()));
return message;
}
@ -130,6 +137,7 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
",\n payoutTxHex=" + payoutTxHex +
",\n updatedMultisigHex=" + updatedMultisigHex +
",\n paymentAccountKey=" + paymentAccountKey +
",\n buyerSignature=" + buyerSignature +
"\n} " + super.toString();
}
}

View file

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

View file

@ -19,19 +19,11 @@ package bisq.core.trade.protocol;
import bisq.core.trade.BuyerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.taskrunner.Task;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -45,10 +37,6 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
super(trade);
}
///////////////////////////////////////////////////////////////////////////////////////////
// MakerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
@ -80,49 +68,4 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}
}).start();
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
super.handleInitMultisigRequest(request, sender);
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
super.handleSignContractRequest(message, sender);
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
super.handleSignContractResponse(message, sender);
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
super.handleDepositResponse(response, sender);
}
@Override
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
super.handle(request, sender);
}
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which events we expect
@Override
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
super.onPaymentStarted(resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message Payout tx
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
super.handle(message, peer);
}
}

View file

@ -92,60 +92,6 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}).start();
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
super.handleInitMultisigRequest(request, sender);
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
super.handleSignContractRequest(message, sender);
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
super.handleSignContractResponse(message, sender);
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
super.handleDepositResponse(response, sender);
}
@Override
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
super.handle(request, sender);
}
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which events we expect
@Override
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
super.onPaymentStarted(resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message Payout tx
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
super.handle(message, peer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message dispatcher
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
}
@Override
protected void handleError(String errorMessage) {
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());

View file

@ -25,7 +25,8 @@ import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress;
@ -58,7 +59,9 @@ public class BuyerProtocol extends DisputeProtocol {
given(anyPhase(Trade.Phase.PAYMENT_SENT)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
.with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSendPaymentSentMessage.class))
.setup(tasks(
BuyerSendPaymentSentMessageToSeller.class,
BuyerSendPaymentSentMessageToArbitrator.class))
.executeTasks();
}
@ -93,10 +96,9 @@ public class BuyerProtocol extends DisputeProtocol {
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(ApplyFilter.class,
//UpdateMultisigWithTradingPeer.class, // TODO (woodser): can use this to test protocol with updated multisig from peer. peer should attempt to send updated multisig hex earlier as part of protocol. cannot use with countdown latch because response comes back in a separate thread and blocks on trade
BuyerPreparePaymentSentMessage.class,
//BuyerSetupPayoutTxListener.class,
BuyerSendPaymentSentMessage.class) // don't latch trade because this blocks and runs in background
BuyerSendPaymentSentMessageToSeller.class,
BuyerSendPaymentSentMessageToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
this.errorMessageHandler = null;
@ -119,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked")
@Override
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
}
}

View file

@ -20,18 +20,11 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.MakerSendInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
@ -81,53 +74,4 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}
}).start();
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
super.handleInitMultisigRequest(request, sender);
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
super.handleSignContractRequest(message, sender);
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
super.handleSignContractResponse(message, sender);
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
super.handleDepositResponse(response, sender);
}
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which events we expect
@Override
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
super.onPaymentReceived(resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Massage dispatcher
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message when buyer has clicked payment started button
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PaymentSentMessage message, NodeAddress peer) {
super.handle(message, peer);
}
}

View file

@ -22,18 +22,10 @@ import bisq.core.offer.Offer;
import bisq.core.trade.SellerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds;
import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
@ -90,55 +82,6 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}).start();
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
super.handleInitMultisigRequest(request, sender);
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
super.handleSignContractRequest(message, sender);
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
super.handleSignContractResponse(message, sender);
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
super.handleDepositResponse(response, sender);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message when buyer has clicked payment started button
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PaymentSentMessage message, NodeAddress peer) {
super.handle(message, peer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction
///////////////////////////////////////////////////////////////////////////////////////////
// We keep the handler here in as well to make it more transparent which events we expect
@Override
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
super.onPaymentReceived(resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Massage dispatcher
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
}
@Override
protected void handleError(String errorMessage) {
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId());

View file

@ -24,10 +24,10 @@ import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
@ -54,18 +54,11 @@ public class SellerProtocol extends DisputeProtocol {
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peer);
}
}
@Override
public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
super.onMailboxMessage(message, peerNodeAddress);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
}
}
@Override
@ -74,52 +67,6 @@ public class SellerProtocol extends DisputeProtocol {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message when buyer has clicked payment started button
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(PaymentSentMessage message, NodeAddress peer) {
log.info("SellerProtocol.handle(PaymentSentMessage)");
new Thread(() -> {
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
// a mailbox message with PaymentSentMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
synchronized (trade) {
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
log.warn("Ignoring PaymentSentMessage which was already processed");
return;
}
latchTrade();
expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,
() -> {
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
"so we ignore the message. This can happen if the ACK message to the peer did not " +
"arrive and the peer repeats sending us the message. We send another ACK msg.");
sendAckMessage(peer, message, true, null);
removeMailboxMessageAfterProcessing(message);
}))
.setup(tasks(
ApplyFilter.class,
SellerProcessPaymentSentMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, message);
},
(errorMessage) -> {
stopTimeout();
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction
///////////////////////////////////////////////////////////////////////////////////////////
@ -160,7 +107,7 @@ public class SellerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked")
@Override
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToBuyer.class };
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class, SendDepositsConfirmedMessageToBuyer.class };
}
}

View file

@ -23,6 +23,7 @@ import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse;
@ -33,8 +34,10 @@ import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.RemoveOffer;
import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
@ -92,13 +95,15 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
///////////////////////////////////////////////////////////////////////////////////////////
// Dispatcher
// Message dispatching
///////////////////////////////////////////////////////////////////////////////////////////
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress);
}
@ -108,49 +113,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) {
processModel.applyTransient(serviceProvider, tradeManager, offer);
onInitialized();
}
protected void onInitialized() {
if (!trade.isCompleted()) {
processModel.getP2PService().addDecryptedDirectMessageListener(this);
}
// handle trade events
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
});
// initialize trade
trade.initialize(processModel.getProvider());
// process mailbox messages
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
mailboxMessageService.addDecryptedMailboxListener(this);
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
}
public void onWithdrawCompleted() {
log.info("Withdraw completed");
}
///////////////////////////////////////////////////////////////////////////////////////////
// DecryptedDirectMessageListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
@ -176,11 +145,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// DecryptedMailboxListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) {
if (!isPubKeyValid(decryptedMessageWithPubKey, peer)) return;
@ -240,10 +204,34 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract
// API
///////////////////////////////////////////////////////////////////////////////////////////
public abstract Class<? extends TradeTask>[] getDepsitsConfirmedTasks();
public abstract Class<? extends TradeTask>[] getDepositsConfirmedTasks();
public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager, Offer offer) {
processModel.applyTransient(serviceProvider, tradeManager, offer);
onInitialized();
}
protected void onInitialized() {
if (!trade.isCompleted()) {
processModel.getP2PService().addDecryptedDirectMessageListener(this);
}
// handle trade events
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
});
// initialize trade
trade.initialize(processModel.getProvider());
// process mailbox messages
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
mailboxMessageService.addDecryptedMailboxListener(this);
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
}
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
@ -398,6 +386,53 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}).start();
}
// received by seller and arbitrator
protected void handle(PaymentSentMessage message, NodeAddress peer) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage)");
if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) {
log.warn("Ignoring PaymentSentMessage since not seller or arbitrator");
return;
}
new Thread(() -> {
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
// a mailbox message with PaymentSentMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
synchronized (trade) {
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
log.warn("Ignoring PaymentSentMessage which was already processed");
return;
}
latchTrade();
expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,
() -> {
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
"so we ignore the message. This can happen if the ACK message to the peer did not " +
"arrive and the peer repeats sending us the message. We send another ACK msg.");
sendAckMessage(peer, message, true, null);
removeMailboxMessageAfterProcessing(message);
}))
.setup(tasks(
ApplyFilter.class,
ProcessPaymentSentMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, message);
},
(errorMessage) -> {
stopTimeout();
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
// received by buyer and arbitrator
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
@ -410,7 +445,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED })
expect(anyPhase(trade.isBuyer() ? new Trade.Phase[] {Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED} : new Trade.Phase[] {Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT})
.with(message)
.from(peer))
.setup(tasks(
@ -427,6 +462,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
}
public void onWithdrawCompleted() {
log.info("Withdraw completed");
}
///////////////////////////////////////////////////////////////////////////////////////////
// FluentProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@ -590,15 +629,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// Validation
///////////////////////////////////////////////////////////////////////////////////////////
private PubKeyRing getPeersPubKeyRing(NodeAddress peer) {
private PubKeyRing getPeersPubKeyRing(NodeAddress address) {
trade.setMyNodeAddress(); // TODO: this is a hack to update my node address before verifying the message
if (peer.equals(trade.getArbitrator().getNodeAddress())) return trade.getArbitrator().getPubKeyRing();
else if (peer.equals(trade.getMaker().getNodeAddress())) return trade.getMaker().getPubKeyRing();
else if (peer.equals(trade.getTaker().getNodeAddress())) return trade.getTaker().getPubKeyRing();
else {
TradingPeer peer = trade.getTradingPeer(address);
if (peer == null) {
log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + peer);
return null;
}
return peer.getPubKeyRing();
}
private boolean isPubKeyValid(DecryptedMessageWithPubKey message) {
@ -707,7 +745,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
synchronized (trade) {
latchTrade();
expect(new Condition(trade))
.setup(tasks(getDepsitsConfirmedTasks())
.setup(tasks(getDepositsConfirmedTasks())
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");

View file

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

View file

@ -48,83 +48,87 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
@Override
protected void run() {
try {
runInterceptHook();
runInterceptHook();
// get contract and signature
String contractAsJson = trade.getContractAsJson();
DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response
String signature = request.getContractSignature();
// get peer info
TradingPeer peer = trade.getTradingPeer(request.getSenderNodeAddress());
if (peer == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
PubKeyRing peerPubKeyRing = peer.getPubKeyRing();
// verify signature
if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid");
// get contract and signature
String contractAsJson = trade.getContractAsJson();
DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response
String signature = request.getContractSignature();
// set peer's signature
peer.setContractSignature(signature);
// get peer info
TradingPeer peer = trade.getTradingPeer(request.getSenderNodeAddress());
if (peer == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
PubKeyRing peerPubKeyRing = peer.getPubKeyRing();
// collect expected values of deposit tx
Offer offer = trade.getOffer();
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress());
boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY;
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
String depositAddress = processModel.getMultisigAddress();
BigInteger tradeFee;
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
else throw new RuntimeException("DepositRequest is not from maker or taker");
// verify signature
if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid");
// verify deposit tx
try {
trade.getXmrWalletService().verifyTradeTx(depositAddress,
depositAmount,
tradeFee,
trader.getDepositTxHash(),
request.getDepositTxHex(),
request.getDepositTxKey(),
null,
false);
} catch (Exception e) {
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// set peer's signature
peer.setContractSignature(signature);
// set deposit info
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
// collect expected values of deposit tx
Offer offer = trade.getOffer();
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress());
boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY;
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
String depositAddress = processModel.getMultisigAddress();
BigInteger tradeFee;
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
else throw new RuntimeException("DepositRequest is not from maker or taker");
// relay deposit txs when both available
// TODO (woodser): add small delay so tx has head start against double spend attempts?
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
// verify deposit tx
trade.getXmrWalletService().verifyTradeTx(depositAddress,
depositAmount,
tradeFee,
trader.getDepositTxHash(),
request.getDepositTxHex(),
request.getDepositTxKey(),
null,
false);
// set deposit info
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
// relay deposit txs when both available
// TODO (woodser): add small delay so tx has head start against double spend attempts?
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
// relay txs
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
// relay txs
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
// update trade state
log.info("Arbitrator submitted deposit txs for trade " + trade.getId());
trade.setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS);
// update trade state
log.info("Arbitrator submitted deposit txs for trade " + trade.getId());
trade.setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS);
// create deposit response
DepositResponse response = new DepositResponse(
trade.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime());
// create deposit response
DepositResponse response = new DepositResponse(
trade.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime());
// send deposit response to maker and taker
sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response);
sendDepositResponse(trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), response);
} else {
if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId());
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
}
// send deposit response to maker and taker
sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response);
sendDepositResponse(trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), response);
} else {
if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId());
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
}
// TODO (woodser): request persistence?
complete();
// TODO (woodser): request persistence?
complete();
} catch (Throwable t) {
failed(t);
}

View file

@ -54,7 +54,8 @@ public class ArbitratorProcessReserveTx extends TradeTask {
// process reserve tx with expected terms
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
trade.getXmrWalletService().verifyTradeTx(
try {
trade.getXmrWalletService().verifyTradeTx(
request.getPayoutAddress(),
depositAmount,
tradeFee,
@ -63,6 +64,9 @@ public class ArbitratorProcessReserveTx extends TradeTask {
request.getReserveTxKey(),
null,
true);
} catch (Exception e) {
throw new RuntimeException("Error processing reserve tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// save reserve tx to model
TradingPeer trader = isFromTaker ? processModel.getTaker() : processModel.getMaker();

View file

@ -75,7 +75,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// create payout tx if we have seller's updated multisig hex
if (trade.getSeller().getUpdatedMultisigHex() != null) {
// create payout tx
// create payout tx
log.info("Buyer creating unsigned payout tx");
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx);

View file

@ -21,11 +21,18 @@ import bisq.core.network.MessageState;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.NodeAddress;
import com.google.common.base.Charsets;
import bisq.common.Timer;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import javafx.beans.value.ChangeListener;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
/**
@ -38,8 +45,8 @@ import lombok.extern.slf4j.Slf4j;
* online he will process it.
*/
@Slf4j
public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
private PaymentSentMessage message;
@EqualsAndHashCode(callSuper = true)
public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
private ChangeListener<MessageState> listener;
private Timer timer;
@ -47,16 +54,34 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
super(taskHandler, trade);
}
protected abstract NodeAddress getReceiverNodeAddress();
protected abstract PubKeyRing getReceiverPubKeyRing();
@Override
protected void run() {
try {
runInterceptHook();
super.run();
} catch (Throwable t) {
failed(t);
} finally {
cleanup();
}
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
if (message == null) {
if (trade.getSelf().getPaymentSentMessage() == null) {
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
// peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
// other data stays the same when we re-send the message at any time later.
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
message = new PaymentSentMessage(
// create payment sent message
PaymentSentMessage message = new PaymentSentMessage(
tradeId,
processModel.getMyNodeAddress(),
trade.getCounterCurrencyTxId(),
@ -66,8 +91,18 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
trade.getSelf().getUpdatedMultisigHex(),
trade.getSelf().getPaymentAccountKey()
);
// sign message
try {
String messageAsJson = JsonUtil.objectToJson(message);
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
message.setBuyerSignature(sig);
trade.getSelf().setPaymentSentMessage(message);
} catch (Exception e) {
throw new RuntimeException (e);
}
}
return message;
return trade.getSelf().getPaymentSentMessage();
}
@Override
@ -96,18 +131,6 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
processModel.getTradeManager().requestPersistence();
}
@Override
protected void run() {
try {
runInterceptHook();
super.run();
} catch (Throwable t) {
failed(t);
} finally {
cleanup();
}
}
private void cleanup() {
if (timer != null) {
timer.stop();

View file

@ -0,0 +1,63 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSentMessage {
public BuyerSendPaymentSentMessageToArbitrator(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
protected NodeAddress getReceiverNodeAddress() {
return trade.getArbitrator().getNodeAddress();
}
protected PubKeyRing getReceiverPubKeyRing() {
return trade.getArbitrator().getPubKeyRing();
}
@Override
protected void setStateSent() {
complete(); // don't wait for message to arbitrator
}
@Override
protected void setStateFault() {
// state only updated on seller message
}
@Override
protected void setStateStoredInMailbox() {
// state only updated on seller message
}
@Override
protected void setStateArrived() {
// state only updated on seller message
}
}

View file

@ -0,0 +1,52 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.TradeMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMessage {
public BuyerSendPaymentSentMessageToSeller(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
protected NodeAddress getReceiverNodeAddress() {
return trade.getSeller().getNodeAddress();
}
protected PubKeyRing getReceiverPubKeyRing() {
return trade.getSeller().getPubKeyRing();
}
// continue execution on fault so payment sent message is sent to arbitrator
@Override
protected void onFault(String errorMessage, TradeMessage message) {
setStateFault();
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
complete();
}
}

View file

@ -53,7 +53,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
checkNotNull(makerRequest);
checkTradeId(processModel.getOfferId(), makerRequest);
// maker signs offer id as nonce to avoid challenge protocol // TODO (woodser): is this necessary?
// maker signs offer id as nonce to avoid challenge protocol // TODO: how is this used?
Offer offer = processModel.getOffer();
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));

View file

@ -35,34 +35,33 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
@Override
protected void run() {
try {
runInterceptHook();
runInterceptHook();
// get sender based on the pub key
// TODO: trade.getTradingPeer(PubKeyRing)
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
TradingPeer sender;
if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator();
else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer();
else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller();
else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// update peer node address
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
// get peer
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing());
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// update peer node address
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
if (sender.getNodeAddress().equals(trade.getBuyer().getNodeAddress()) && sender != trade.getBuyer()) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
// decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey());
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
// store updated multisig hex for processing on payment sent
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey());
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}
// persist and complete
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
}
// store updated multisig hex for processing on payment sent
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// persist and complete
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

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

View file

@ -18,8 +18,8 @@
package bisq.core.trade.protocol.tasks;
import bisq.core.account.sign.SignedWitness;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.util.Validator;
@ -27,11 +27,13 @@ import common.utils.GenUtils;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ProcessPaymentReceivedMessage extends TradeTask {
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
@ -48,26 +50,34 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
checkNotNull(message);
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
// verify signature of payment received message
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
// update to the latest peer address of our peer if the message is correct
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
// import multisig hex
List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
// sync and save wallet
trade.syncWallet();
trade.saveWallet();
// handle if payout tx not published
if (!trade.isPayoutPublished()) {
// import multisig hex
MoneroWallet multisigWallet = trade.getWallet();
if (message.getUpdatedMultisigHex() != null) {
multisigWallet.importMultisigHex(message.getUpdatedMultisigHex());
trade.saveWallet();
}
// arbitrator waits for buyer to sign and broadcast payout tx if message arrived
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
boolean isSigned = message.getSignedPayoutTxHex() != null;
if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) {
log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName());
GenUtils.waitFor(30000);
multisigWallet.rescanSpent();
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
trade.syncWallet();
}
// verify and publish payout tx
@ -77,11 +87,16 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
} else {
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
try {
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
} catch (Exception e) {
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
else throw e;
}
}
}
} else {
log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId());
log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
}
SignedWitness signedWitness = message.getSignedWitness();
@ -93,7 +108,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
}
// complete
if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {

View file

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

View file

@ -107,7 +107,7 @@ public class ProcessSignContractResponse extends TradeTask {
trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST);
processModel.getTradeManager().requestPersistence();
} else {
log.info("Waiting for more contract signatures to send deposit request");
log.info("Waiting for another contract signatures to send deposit request");
complete(); // does not yet have needed signatures
}
} catch (Throwable t) {

View file

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

View file

@ -21,8 +21,10 @@ import bisq.core.account.sign.SignedWitness;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode;
@ -30,10 +32,13 @@ import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@EqualsAndHashCode(callSuper = true)
import com.google.common.base.Charsets;
@Slf4j
@EqualsAndHashCode(callSuper = true)
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
SignedWitness signedWitness = null;
PaymentReceivedMessage message = null;
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
@ -47,13 +52,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
protected void run() {
try {
runInterceptHook();
if (trade.getPayoutTxHex() == null) {
log.error("Payout tx is null");
failed("Payout tx is null");
return;
}
super.run();
} catch (Throwable t) {
failed(t);
@ -63,23 +61,37 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
if (message == null) {
// TODO: sign witness
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
// if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// // Broadcast is done in accountAgeWitness domain.
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
// }
// TODO: sign witness
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
// if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// // Broadcast is done in accountAgeWitness domain.
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
// }
return new PaymentReceivedMessage(
tradeId,
processModel.getMyNodeAddress(),
signedWitness,
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout
);
// TODO: create with deterministic id like BuyerSendPaymentSentMessage
message = new PaymentReceivedMessage(
tradeId,
processModel.getMyNodeAddress(),
signedWitness,
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
trade.getBuyer().getPaymentSentMessage()
);
// sign message
try {
String messageAsJson = JsonUtil.objectToJson(message);
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
message.setSellerSignature(sig);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return message;
}
@Override

View file

@ -65,6 +65,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
processModel.getTradeManager().requestPersistence();
}
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the

View file

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

View file

@ -19,11 +19,8 @@ package bisq.core.trade.protocol.tasks;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.HashSet;

View file

@ -1842,7 +1842,7 @@ disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3}
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
Open trade and accept or reject suggestion from mediator
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\
No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions
A dispute has been opened with the arbitrator. You can chat with the arbitrator in the "Support" tab to resolve the dispute.
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
# suppress inspection "TrailingSpacesInProperty"

View file

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

View file

@ -65,7 +65,7 @@ public class GrpcDisputesService extends DisputesImplBase {
},
(errorMessage, throwable) -> {
log.info("Error in openDispute" + errorMessage);
exceptionHandler.handleException(log, throwable, responseObserver);
exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
@ -82,7 +82,7 @@ public class GrpcDisputesService extends DisputesImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".getDispute", cause, responseObserver);
}
}
@ -115,7 +115,7 @@ public class GrpcDisputesService extends DisputesImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".resolveDispute", cause, responseObserver);
}
}
@ -149,7 +149,7 @@ public class GrpcDisputesService extends DisputesImplBase {
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
}}
)));
}

View file

@ -206,7 +206,7 @@ class GrpcOffersService extends OffersImplBase {
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
}}
)));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -190,7 +190,7 @@ public class BuyerStep2View extends TradeStepView {
model.setMessageStateProperty(MessageState.FAILED);
break;
default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId());
busyAnimation.stop();
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
break;
@ -608,12 +608,6 @@ public class BuyerStep2View extends TradeStepView {
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
//TODO seems this was a hack to enable repeated confirm???
if (trade.isPaymentSent()) {
trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
model.dataModel.getTradeManager().requestPersistence();
}
model.dataModel.onPaymentStarted(() -> {
}, errorMessage -> {
busyAnimation.stop();

View file

@ -145,6 +145,11 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.stop();
statusLabel.setText("");
break;
case TRADE_COMPLETED:
if (!trade.isPayoutPublished()) log.warn("Payout is expected to be published for {} {} state {}", trade.getClass().getSimpleName(), trade.getId(), trade.getState());
busyAnimation.stop();
statusLabel.setText("");
break;
default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
busyAnimation.stop();

View file

@ -838,15 +838,19 @@ message TradeInfo {
string phase = 17;
string period_state = 18;
string payout_state = 19;
bool is_deposit_published = 20;
bool is_deposit_unlocked = 21;
bool is_payment_sent = 22;
bool is_payment_received = 23;
bool is_payout_published = 24;
bool is_completed = 25;
string contract_as_json = 26;
ContractInfo contract = 27;
string trade_volume = 28;
string dispute_state = 20;
bool is_deposit_published = 21;
bool is_deposit_confirmed = 22;
bool is_deposit_unlocked = 23;
bool is_payment_sent = 24;
bool is_payment_received = 25;
bool is_payout_published = 26;
bool is_payout_confirmed = 27;
bool is_payout_unlocked = 28;
bool is_completed = 29;
string contract_as_json = 30;
ContractInfo contract = 31;
string trade_volume = 32;
string maker_deposit_tx_id = 100;
string taker_deposit_tx_id = 101;

View file

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