set buyer and seller payout tx fee and amount, fix csv export #720

This commit is contained in:
woodser 2023-12-08 05:45:27 -05:00
parent 846a8634e5
commit 8800d9ea46
18 changed files with 374 additions and 247 deletions

View file

@ -163,9 +163,6 @@ public class CoreDisputesService {
} }
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
// create dispute payout tx
trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), winnerDisputeResult, false));
// close winning dispute ticket // close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
@ -180,8 +177,8 @@ public class CoreDisputesService {
if (!loserDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute"); if (!loserDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute");
var loserDispute = loserDisputeOptional.get(); var loserDispute = loserDisputeOptional.get();
var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate); var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate);
loserDisputeResult.setBuyerPayoutAmount(winnerDisputeResult.getBuyerPayoutAmount()); loserDisputeResult.setBuyerPayoutAmountBeforeCost(winnerDisputeResult.getBuyerPayoutAmountBeforeCost());
loserDisputeResult.setSellerPayoutAmount(winnerDisputeResult.getSellerPayoutAmount()); loserDisputeResult.setSellerPayoutAmountBeforeCost(winnerDisputeResult.getSellerPayoutAmountBeforeCost());
loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom()); loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom());
closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> { closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
@ -217,31 +214,23 @@ public class CoreDisputesService {
BigInteger tradeAmount = contract.getTradeAmount(); BigInteger tradeAmount = contract.getTradeAmount();
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) { if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmount(sellerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
} else if (payout == DisputePayout.BUYER_GETS_ALL) { } else if (payout == DisputePayout.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmount(tradeAmount disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
.add(buyerSecurityDeposit) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.valueOf(0));
.add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmount(BigInteger.valueOf(0));
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit)); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
} else if (payout == DisputePayout.SELLER_GETS_ALL) { } else if (payout == DisputePayout.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(0)); disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.valueOf(0));
disputeResult.setSellerPayoutAmount(tradeAmount disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
.add(sellerSecurityDeposit)
.add(buyerSecurityDeposit));
} else if (payout == DisputePayout.CUSTOM) { } else if (payout == DisputePayout.CUSTOM) {
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) { if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
throw new RuntimeException("Winner payout is more than the trade wallet's balance");
}
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) { if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
throw new RuntimeException("Loser payout cannot be negative"); disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
} disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount));
disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount));
disputeResult.setSubtractFeeFrom(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? SubtractFeeFrom.SELLER_ONLY : SubtractFeeFrom.BUYER_ONLY); // winner gets exact amount, loser pays mining fee disputeResult.setSubtractFeeFrom(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? SubtractFeeFrom.SELLER_ONLY : SubtractFeeFrom.BUYER_ONLY); // winner gets exact amount, loser pays mining fee
} }
} }
@ -263,8 +252,8 @@ public class CoreDisputesService {
currencyCode, currencyCode,
Res.get("disputeSummaryWindow.reason." + reason.name()), Res.get("disputeSummaryWindow.reason." + reason.name()),
amount, amount,
HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount(), true), HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true),
HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount(), true), HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true),
disputeResult.summaryNotesProperty().get() disputeResult.summaryNotesProperty().get()
); );

View file

@ -71,6 +71,12 @@ public class TradeInfo implements Payload {
private final long amount; private final long amount;
private final long buyerSecurityDeposit; private final long buyerSecurityDeposit;
private final long sellerSecurityDeposit; private final long sellerSecurityDeposit;
private final long buyerDepositTxFee;
private final long sellerDepositTxFee;
private final long buyerPayoutTxFee;
private final long sellerPayoutTxFee;
private final long buyerPayoutAmount;
private final long sellerPayoutAmount;
private final String price; private final String price;
private final String volume; private final String volume;
private final String arbitratorNodeAddress; private final String arbitratorNodeAddress;
@ -105,6 +111,12 @@ public class TradeInfo implements Payload {
this.amount = builder.getAmount(); this.amount = builder.getAmount();
this.buyerSecurityDeposit = builder.getBuyerSecurityDeposit(); this.buyerSecurityDeposit = builder.getBuyerSecurityDeposit();
this.sellerSecurityDeposit = builder.getSellerSecurityDeposit(); this.sellerSecurityDeposit = builder.getSellerSecurityDeposit();
this.buyerDepositTxFee = builder.getBuyerDepositTxFee();
this.sellerDepositTxFee = builder.getSellerDepositTxFee();
this.buyerPayoutTxFee = builder.getBuyerPayoutTxFee();
this.sellerPayoutTxFee = builder.getSellerPayoutTxFee();
this.buyerPayoutAmount = builder.getBuyerPayoutAmount();
this.sellerPayoutAmount = builder.getSellerPayoutAmount();
this.price = builder.getPrice(); this.price = builder.getPrice();
this.volume = builder.getVolume(); this.volume = builder.getVolume();
this.arbitratorNodeAddress = builder.getArbitratorNodeAddress(); this.arbitratorNodeAddress = builder.getArbitratorNodeAddress();
@ -161,6 +173,13 @@ public class TradeInfo implements Payload {
.withAmount(trade.getAmount().longValueExact()) .withAmount(trade.getAmount().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact()) .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact()) .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
.withPrice(toPreciseTradePrice.apply(trade)) .withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade)) .withVolume(toRoundedVolume.apply(trade))
.withArbitratorNodeAddress(toArbitratorNodeAddress.apply(trade)) .withArbitratorNodeAddress(toArbitratorNodeAddress.apply(trade))
@ -204,6 +223,12 @@ public class TradeInfo implements Payload {
.setAmount(amount) .setAmount(amount)
.setBuyerSecurityDeposit(buyerSecurityDeposit) .setBuyerSecurityDeposit(buyerSecurityDeposit)
.setSellerSecurityDeposit(sellerSecurityDeposit) .setSellerSecurityDeposit(sellerSecurityDeposit)
.setBuyerDepositTxFee(buyerDepositTxFee)
.setSellerDepositTxFee(sellerDepositTxFee)
.setBuyerPayoutTxFee(buyerPayoutTxFee)
.setSellerPayoutTxFee(sellerPayoutTxFee)
.setBuyerPayoutAmount(buyerPayoutAmount)
.setSellerPayoutAmount(sellerPayoutAmount)
.setPrice(price) .setPrice(price)
.setTradeVolume(volume) .setTradeVolume(volume)
.setArbitratorNodeAddress(arbitratorNodeAddress) .setArbitratorNodeAddress(arbitratorNodeAddress)
@ -241,6 +266,12 @@ public class TradeInfo implements Payload {
.withAmount(proto.getAmount()) .withAmount(proto.getAmount())
.withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit()) .withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit())
.withSellerSecurityDeposit(proto.getSellerSecurityDeposit()) .withSellerSecurityDeposit(proto.getSellerSecurityDeposit())
.withBuyerDepositTxFee(proto.getBuyerDepositTxFee())
.withSellerDepositTxFee(proto.getSellerDepositTxFee())
.withBuyerPayoutTxFee(proto.getBuyerPayoutTxFee())
.withSellerPayoutTxFee(proto.getSellerPayoutTxFee())
.withBuyerPayoutAmount(proto.getBuyerPayoutAmount())
.withSellerPayoutAmount(proto.getSellerPayoutAmount())
.withPrice(proto.getPrice()) .withPrice(proto.getPrice())
.withVolume(proto.getTradeVolume()) .withVolume(proto.getTradeVolume())
.withPeriodState(proto.getPeriodState()) .withPeriodState(proto.getPeriodState())
@ -278,6 +309,12 @@ public class TradeInfo implements Payload {
", amount='" + amount + '\'' + "\n" + ", amount='" + amount + '\'' + "\n" +
", buyerSecurityDeposit='" + buyerSecurityDeposit + '\'' + "\n" + ", buyerSecurityDeposit='" + buyerSecurityDeposit + '\'' + "\n" +
", sellerSecurityDeposit='" + sellerSecurityDeposit + '\'' + "\n" + ", sellerSecurityDeposit='" + sellerSecurityDeposit + '\'' + "\n" +
", buyerDepositTxFee='" + buyerDepositTxFee + '\'' + "\n" +
", sellerDepositTxFee='" + sellerDepositTxFee + '\'' + "\n" +
", buyerPayoutTxFee='" + buyerPayoutTxFee + '\'' + "\n" +
", sellerPayoutTxFee='" + sellerPayoutTxFee + '\'' + "\n" +
", buyerPayoutAmount='" + buyerPayoutAmount + '\'' + "\n" +
", sellerPayoutAmount='" + sellerPayoutAmount + '\'' + "\n" +
", price='" + price + '\'' + "\n" + ", price='" + price + '\'' + "\n" +
", arbitratorNodeAddress='" + arbitratorNodeAddress + '\'' + "\n" + ", arbitratorNodeAddress='" + arbitratorNodeAddress + '\'' + "\n" +
", tradePeerNodeAddress='" + tradePeerNodeAddress + '\'' + "\n" + ", tradePeerNodeAddress='" + tradePeerNodeAddress + '\'' + "\n" +

View file

@ -41,6 +41,12 @@ public final class TradeInfoV1Builder {
private long takerFee; private long takerFee;
private long buyerSecurityDeposit; private long buyerSecurityDeposit;
private long sellerSecurityDeposit; private long sellerSecurityDeposit;
private long buyerDepositTxFee;
private long sellerDepositTxFee;
private long buyerPayoutTxFee;
private long sellerPayoutTxFee;
private long buyerPayoutAmount;
private long sellerPayoutAmount;
private String makerDepositTxId; private String makerDepositTxId;
private String takerDepositTxId; private String takerDepositTxId;
private String payoutTxId; private String payoutTxId;
@ -117,6 +123,36 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withBuyerDepositTxFee(long buyerDepositTxFee) {
this.buyerDepositTxFee = buyerDepositTxFee;
return this;
}
public TradeInfoV1Builder withSellerDepositTxFee(long sellerDepositTxFee) {
this.sellerDepositTxFee = sellerDepositTxFee;
return this;
}
public TradeInfoV1Builder withBuyerPayoutTxFee(long buyerPayoutTxFee) {
this.buyerPayoutTxFee = buyerPayoutTxFee;
return this;
}
public TradeInfoV1Builder withSellerPayoutTxFee(long sellerPayoutTxFee) {
this.sellerPayoutTxFee = sellerPayoutTxFee;
return this;
}
public TradeInfoV1Builder withBuyerPayoutAmount(long buyerPayoutAmount) {
this.buyerPayoutAmount = buyerPayoutAmount;
return this;
}
public TradeInfoV1Builder withSellerPayoutAmount(long sellerPayoutAmount) {
this.sellerPayoutAmount = sellerPayoutAmount;
return this;
}
public TradeInfoV1Builder withMakerDepositTxId(String makerDepositTxId) { public TradeInfoV1Builder withMakerDepositTxId(String makerDepositTxId) {
this.makerDepositTxId = makerDepositTxId; this.makerDepositTxId = makerDepositTxId;
return this; return this;

View file

@ -69,7 +69,6 @@ import java.math.BigInteger;
import java.security.KeyPair; import java.security.KeyPair;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -729,23 +728,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
} }
// create dispute payout tx if not published // create dispute payout tx once per trader if we have their updated multisig hex
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing()); TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) { if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
trade.getProcessModel().setUnsignedPayoutTx(createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false)); // can be null if we don't have receiver's multisig hex createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
} }
// create dispute closed message // create dispute closed message
MoneroTxWallet unsignedPayoutTx = receiver.getUpdatedMultisigHex() == null ? null : trade.getProcessModel().getUnsignedPayoutTx();
String unsignedPayoutTxHex = unsignedPayoutTx == null ? null : unsignedPayoutTx.getTxSet().getMultisigTxHex();
TradePeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); TradePeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = !exists && unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal(); boolean deferPublishPayout = !exists && receiver.getUnsignedPayoutTxHex() != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal();
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
p2PService.getAddress(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
getSupportType(), getSupportType(),
trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
unsignedPayoutTxHex, // include dispute payout tx if arbitrator has their updated multisig info receiver.getUnsignedPayoutTxHex(), // include dispute payout tx if arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
// send dispute closed message // send dispute closed message
@ -812,12 +809,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
} }
); );
// save state
if (unsignedPayoutTx != null) {
trade.setPayoutTx(unsignedPayoutTx);
trade.setPayoutTxHex(unsignedPayoutTx.getTxSet().getMultisigTxHex());
}
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence();
} catch (Exception e) { } catch (Exception e) {
@ -829,7 +820,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// Utils // Utils
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public MoneroTxWallet createDisputePayoutTx(Trade trade, Contract contract, DisputeResult disputeResult, boolean skipMultisigImport) { public MoneroTxWallet createDisputePayoutTx(Trade trade, Contract contract, DisputeResult disputeResult, boolean updateState) {
// import multisig hex // import multisig hex
trade.importMultisigHex(); trade.importMultisigHex();
@ -848,42 +839,34 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// trade wallet must be synced // trade wallet must be synced
if (trade.getWallet().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 " + trade.getId()); if (trade.getWallet().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 " + trade.getId());
// collect winner and loser payout address and amounts // check amounts
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ? if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative");
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative");
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost())));
BigInteger winnerPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount();
BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount();
// check sufficient balance
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Winner payout cannot be negative");
if (loserPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Loser payout cannot be negative");
if (winnerPayoutAmount.add(loserPayoutAmount).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + winnerPayoutAmount + " + " + loserPayoutAmount + " = " + (winnerPayoutAmount.add(loserPayoutAmount)));
} }
// add any loss of precision to winner payout
winnerPayoutAmount = winnerPayoutAmount.add(trade.getWallet().getUnlockedBalance().subtract(winnerPayoutAmount.add(loserPayoutAmount)));
// create dispute payout tx config // create dispute payout tx config
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString();
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount); if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost());
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount); if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost());
// configure who pays mining fee // configure who pays mining fee
BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost();
if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0
else { else {
switch (disputeResult.getSubtractFeeFrom()) { switch (disputeResult.getSubtractFeeFrom()) {
case BUYER_AND_SELLER: case BUYER_AND_SELLER:
txConfig.setSubtractFeeFrom(Arrays.asList(0, 1)); txConfig.setSubtractFeeFrom(0, 1);
break; break;
case BUYER_ONLY: case BUYER_ONLY:
txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.BUYER ? 0 : 1); txConfig.setSubtractFeeFrom(0);
break; break;
case SELLER_ONLY: case SELLER_ONLY:
txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.SELLER ? 0 : 1); txConfig.setSubtractFeeFrom(1);
break; break;
} }
} }
@ -897,8 +880,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
throw new RuntimeException("Loser payout is too small to cover the mining fee"); throw new RuntimeException("Loser payout is too small to cover the mining fee");
} }
// save updated multisig hex // update trade state
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
return payoutTx; return payoutTx;
} catch (Exception e) { } catch (Exception e) {
trade.syncAndPollWallet(); trade.syncAndPollWallet();

View file

@ -86,8 +86,8 @@ public final class DisputeResult implements NetworkPayload {
@Setter @Setter
@Nullable @Nullable
private byte[] arbitratorSignature; private byte[] arbitratorSignature;
private long buyerPayoutAmount; private long buyerPayoutAmountBeforeCost;
private long sellerPayoutAmount; private long sellerPayoutAmountBeforeCost;
@Setter @Setter
@Nullable @Nullable
private byte[] arbitratorPubKey; private byte[] arbitratorPubKey;
@ -109,8 +109,8 @@ public final class DisputeResult implements NetworkPayload {
String summaryNotes, String summaryNotes,
@Nullable ChatMessage chatMessage, @Nullable ChatMessage chatMessage,
@Nullable byte[] arbitratorSignature, @Nullable byte[] arbitratorSignature,
long buyerPayoutAmount, long buyerPayoutAmountBeforeCost,
long sellerPayoutAmount, long sellerPayoutAmountBeforeCost,
@Nullable byte[] arbitratorPubKey, @Nullable byte[] arbitratorPubKey,
long closeDate) { long closeDate) {
this.tradeId = tradeId; this.tradeId = tradeId;
@ -124,8 +124,8 @@ public final class DisputeResult implements NetworkPayload {
this.summaryNotesProperty.set(summaryNotes); this.summaryNotesProperty.set(summaryNotes);
this.chatMessage = chatMessage; this.chatMessage = chatMessage;
this.arbitratorSignature = arbitratorSignature; this.arbitratorSignature = arbitratorSignature;
this.buyerPayoutAmount = buyerPayoutAmount; this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost;
this.sellerPayoutAmount = sellerPayoutAmount; this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost;
this.arbitratorPubKey = arbitratorPubKey; this.arbitratorPubKey = arbitratorPubKey;
this.closeDate = closeDate; this.closeDate = closeDate;
} }
@ -147,8 +147,8 @@ public final class DisputeResult implements NetworkPayload {
proto.getSummaryNotes(), proto.getSummaryNotes(),
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
proto.getArbitratorSignature().toByteArray(), proto.getArbitratorSignature().toByteArray(),
proto.getBuyerPayoutAmount(), proto.getBuyerPayoutAmountBeforeCost(),
proto.getSellerPayoutAmount(), proto.getSellerPayoutAmountBeforeCost(),
proto.getArbitratorPubKey().toByteArray(), proto.getArbitratorPubKey().toByteArray(),
proto.getCloseDate()); proto.getCloseDate());
} }
@ -163,8 +163,8 @@ public final class DisputeResult implements NetworkPayload {
.setIdVerification(idVerificationProperty.get()) .setIdVerification(idVerificationProperty.get())
.setScreenCast(screenCastProperty.get()) .setScreenCast(screenCastProperty.get())
.setSummaryNotes(summaryNotesProperty.get()) .setSummaryNotes(summaryNotesProperty.get())
.setBuyerPayoutAmount(buyerPayoutAmount) .setBuyerPayoutAmountBeforeCost(buyerPayoutAmountBeforeCost)
.setSellerPayoutAmount(sellerPayoutAmount) .setSellerPayoutAmountBeforeCost(sellerPayoutAmountBeforeCost)
.setCloseDate(closeDate); .setCloseDate(closeDate);
Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature)));
@ -213,22 +213,22 @@ public final class DisputeResult implements NetworkPayload {
return summaryNotesProperty; return summaryNotesProperty;
} }
public void setBuyerPayoutAmount(BigInteger buyerPayoutAmount) { public void setBuyerPayoutAmountBeforeCost(BigInteger buyerPayoutAmountBeforeCost) {
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmount cannot be negative"); if (buyerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmountBeforeCost cannot be negative");
this.buyerPayoutAmount = buyerPayoutAmount.longValueExact(); this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost.longValueExact();
} }
public BigInteger getBuyerPayoutAmount() { public BigInteger getBuyerPayoutAmountBeforeCost() {
return BigInteger.valueOf(buyerPayoutAmount); return BigInteger.valueOf(buyerPayoutAmountBeforeCost);
} }
public void setSellerPayoutAmount(BigInteger sellerPayoutAmount) { public void setSellerPayoutAmountBeforeCost(BigInteger sellerPayoutAmountBeforeCost) {
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmount cannot be negative"); if (sellerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmountBeforeCost cannot be negative");
this.sellerPayoutAmount = sellerPayoutAmount.longValueExact(); this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost.longValueExact();
} }
public BigInteger getSellerPayoutAmount() { public BigInteger getSellerPayoutAmountBeforeCost() {
return BigInteger.valueOf(sellerPayoutAmount); return BigInteger.valueOf(sellerPayoutAmountBeforeCost);
} }
public void setCloseDate(Date closeDate) { public void setCloseDate(Date closeDate) {
@ -253,8 +253,8 @@ public final class DisputeResult implements NetworkPayload {
",\n summaryNotesProperty=" + summaryNotesProperty + ",\n summaryNotesProperty=" + summaryNotesProperty +
",\n chatMessage=" + chatMessage + ",\n chatMessage=" + chatMessage +
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\n buyerPayoutAmount=" + buyerPayoutAmount + ",\n buyerPayoutAmountBeforeCost=" + buyerPayoutAmountBeforeCost +
",\n sellerPayoutAmount=" + sellerPayoutAmount + ",\n sellerPayoutAmountBeforeCost=" + sellerPayoutAmountBeforeCost +
",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) +
",\n closeDate=" + closeDate + ",\n closeDate=" + closeDate +
"\n}"; "\n}";

View file

@ -380,42 +380,22 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// get actual payout amounts // get actual payout amounts
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount(); BigInteger actualBuyerAmount = buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount();
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount(); BigInteger actualSellerAmount = sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount();
// verify payouts sum to unlocked balance within loss of precision due to conversion to centineros // verify payouts sum to unlocked balance within loss of precision due to conversion to centineros
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // cost = fee + lost dust change
if (trade.getWallet().getUnlockedBalance().subtract(actualWinnerAmount.add(actualLoserAmount).add(txCost)).compareTo(BigInteger.valueOf(0)) > 0) { if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), trade.getId(), arbitratorSignedPayoutTx.getChangeAmount());
throw new RuntimeException("The dispute payout amounts do not sum to the wallet's unlocked balance while verifying the dispute payout tx, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs sum payout amount=" + actualWinnerAmount.add(actualLoserAmount) + ", winner payout=" + actualWinnerAmount + ", loser payout=" + actualLoserAmount); if (trade.getWallet().getUnlockedBalance().subtract(actualBuyerAmount.add(actualSellerAmount).add(txCost)).compareTo(BigInteger.valueOf(0)) > 0) {
throw new RuntimeException("The dispute payout amounts do not sum to the wallet's unlocked balance while verifying the dispute payout tx, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs sum payout amount=" + actualBuyerAmount.add(actualSellerAmount) + ", buyer payout=" + actualBuyerAmount + ", seller payout=" + actualSellerAmount);
} }
// get expected payout amounts // verify payout amounts
BigInteger expectedWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount(); BigInteger[] buyerSellerPayoutTxCost = getBuyerSellerPayoutTxCost(disputeResult, txCost);
BigInteger expectedLoserAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount(); BigInteger expectedBuyerAmount = disputeResult.getBuyerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[0]);
BigInteger expectedSellerAmount = disputeResult.getSellerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[1]);
// subtract mining fee from expected payouts if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new RuntimeException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount);
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner pays fee if loser gets 0 if (!expectedSellerAmount.equals(actualSellerAmount)) throw new RuntimeException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount);
else {
switch (disputeResult.getSubtractFeeFrom()) {
case BUYER_AND_SELLER:
BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
expectedWinnerAmount = expectedWinnerAmount.subtract(txCostSplit);
expectedLoserAmount = expectedLoserAmount.subtract(txCostSplit);
break;
case BUYER_ONLY:
expectedWinnerAmount = expectedWinnerAmount.subtract(disputeResult.getWinner() == Winner.BUYER ? txCost : BigInteger.ZERO);
expectedLoserAmount = expectedLoserAmount.subtract(disputeResult.getWinner() == Winner.BUYER ? BigInteger.ZERO : txCost);
break;
case SELLER_ONLY:
expectedWinnerAmount = expectedWinnerAmount.subtract(disputeResult.getWinner() == Winner.BUYER ? BigInteger.ZERO : txCost);
expectedLoserAmount = expectedLoserAmount.subtract(disputeResult.getWinner() == Winner.BUYER ? txCost : BigInteger.ZERO);
break;
}
}
// verify winner and loser payout amounts
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);
// check wallet's daemon connection // check wallet's daemon connection
trade.checkDaemonConnection(); trade.checkDaemonConnection();
@ -431,7 +411,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex()); boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex());
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
if (!signed) { if (signed) disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
else {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
@ -443,8 +424,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
MoneroTxWallet feeEstimateTx = null; MoneroTxWallet feeEstimateTx = null;
try { try {
feeEstimateTx = createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); feeEstimateTx = createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage()); log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage());
} }
if (feeEstimateTx != null) { if (feeEstimateTx != null) {
@ -453,8 +435,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee()); if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff); log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
} }
} else {
disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
} }
// submit fully signed payout tx to the network // submit fully signed payout tx to the network
@ -468,4 +448,26 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash()); dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
return disputeTxSet; return disputeTxSet;
} }
public static BigInteger[] getBuyerSellerPayoutTxCost(DisputeResult disputeResult, BigInteger payoutTxCost) {
boolean isBuyerWinner = disputeResult.getWinner() == Winner.BUYER;
BigInteger loserAmount = isBuyerWinner ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost();
if (loserAmount.equals(BigInteger.valueOf(0))) {
BigInteger buyerPayoutTxFee = isBuyerWinner ? payoutTxCost : BigInteger.ZERO;
BigInteger sellerPayoutTxFee = isBuyerWinner ? BigInteger.ZERO : payoutTxCost;
return new BigInteger[] { buyerPayoutTxFee, sellerPayoutTxFee };
} else {
switch (disputeResult.getSubtractFeeFrom()) {
case BUYER_AND_SELLER:
BigInteger payoutTxFeeSplit = payoutTxCost.divide(BigInteger.valueOf(2));
return new BigInteger[] { payoutTxFeeSplit, payoutTxFeeSplit };
case BUYER_ONLY:
return new BigInteger[] { payoutTxCost, BigInteger.ZERO };
case SELLER_ONLY:
return new BigInteger[] { BigInteger.ZERO, payoutTxCost };
default:
throw new RuntimeException("Unsupported subtract fee from: " + disputeResult.getSubtractFeeFrom());
}
}
}
} }

View file

@ -188,8 +188,8 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||
trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) { trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) {
trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmount().longValueExact()); trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmountBeforeCost().longValueExact());
trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmount().longValueExact()); trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmountBeforeCost().longValueExact());
trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED);
@ -222,8 +222,8 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
Optional<Dispute> optionalDispute = findDispute(tradeId); Optional<Dispute> optionalDispute = findDispute(tradeId);
checkArgument(optionalDispute.isPresent(), "dispute must be present"); checkArgument(optionalDispute.isPresent(), "dispute must be present");
DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get();
BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost();
BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost();
ProcessModel processModel = trade.getProcessModel(); ProcessModel processModel = trade.getProcessModel();
processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.longValueExact()); processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.longValueExact());
processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.longValueExact()); processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.longValueExact());

View file

@ -58,6 +58,9 @@ import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroTxWallet;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
/** /**
@ -543,4 +546,12 @@ public class HavenoUtils {
if (c1 == null) return false; if (c1 == null) return false;
return c1.equals(c2); // equality considers uri, username, and password return c1.equals(c2); // equality considers uri, username, and password
} }
// TODO: move to monero-java MoneroTxWallet
public static MoneroDestination getDestination(String address, MoneroTxWallet tx) {
for (MoneroDestination destination : tx.getOutgoingTransfer().getDestinations()) {
if (address.equals(destination.getAddress())) return destination;
}
return null;
}
} }

View file

@ -37,6 +37,8 @@ import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.proto.CoreProtoResolver; import haveno.core.proto.CoreProtoResolver;
import haveno.core.proto.network.CoreNetworkProtoResolver; import haveno.core.proto.network.CoreNetworkProtoResolver;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.support.dispute.mediation.MediationResultState;
import haveno.core.support.dispute.refund.RefundResultState; import haveno.core.support.dispute.refund.RefundResultState;
import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.ChatMessage;
@ -323,7 +325,6 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
private final Offer offer; private final Offer offer;
private final long takerFee; private final long takerFee;
private final long totalTxFee;
// Added in 1.5.1 // Added in 1.5.1
@Getter @Getter
@ -451,6 +452,7 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
@Setter @Setter
private String payoutTxKey; private String payoutTxKey;
private long payoutTxFee;
private Long payoutHeight; private Long payoutHeight;
private IdlePayoutSyncer idlePayoutSyncer; private IdlePayoutSyncer idlePayoutSyncer;
@ -472,7 +474,6 @@ public abstract class Trade implements Tradable, Model {
this.offer = offer; this.offer = offer;
this.amount = tradeAmount.longValueExact(); this.amount = tradeAmount.longValueExact();
this.takerFee = takerFee.longValueExact(); this.takerFee = takerFee.longValueExact();
this.totalTxFee = 0l; // TODO: sum tx fees
this.price = tradePrice; this.price = tradePrice;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
this.processModel = processModel; this.processModel = processModel;
@ -941,20 +942,25 @@ public abstract class Trade implements Tradable, Model {
.setRelay(false) .setRelay(false)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
// save updated multisig hex // update state
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(payoutTxFeeSplit);
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
getSeller().setPayoutTxFee(payoutTxFeeSplit);
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
return payoutTx; return payoutTx;
} }
/** /**
* Verify a payout tx. * Process a payout tx.
* *
* @param payoutTxHex is the payout tx hex to verify * @param payoutTxHex is the payout tx hex to verify
* @param sign signs the payout tx if true * @param sign signs the payout tx if true
* @param publish publishes the signed payout tx if true * @param publish publishes the signed payout tx if true
*/ */
public void verifyPayoutTx(String payoutTxHex, boolean sign, boolean publish) { public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
log.info("Verifying payout tx"); log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
// gather relevant info // gather relevant info
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
@ -981,6 +987,7 @@ public abstract class Trade implements Tradable, Model {
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract"); if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
// verify change address is multisig's primary address // verify change address is multisig's primary address
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount());
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address"); if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount // verify sum of outputs = destination amounts + change amount
@ -988,11 +995,12 @@ public abstract class Trade implements Tradable, Model {
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs // verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount()); BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2))); BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit);
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout); if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs // verify seller destination amount is deposit amount - this amount - 1/2 tx costs
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2))); BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit);
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// check wallet connection // check wallet connection
@ -1025,7 +1033,6 @@ public abstract class Trade implements Tradable, Model {
// submit payout tx // submit payout tx
if (publish) { if (publish) {
//if (true) throw new RuntimeException("Let's pretend there's an error last second submitting tx to daemon, so we need to resubmit payout hex");
wallet.submitMultisigTxHex(payoutTxHex); wallet.submitMultisigTxHex(payoutTxHex);
pollWallet(); pollWallet();
} }
@ -1296,9 +1303,47 @@ public abstract class Trade implements Tradable, Model {
public void setPayoutTx(MoneroTxWallet payoutTx) { public void setPayoutTx(MoneroTxWallet payoutTx) {
this.payoutTx = payoutTx; this.payoutTx = payoutTx;
payoutTxId = payoutTx.getHash(); payoutTxId = payoutTx.getHash();
if ("".equals(payoutTxId)) payoutTxId = null; // tx hash is empty until signed if ("".equals(payoutTxId)) payoutTxId = null; // tx id is empty until signed
payoutTxKey = payoutTx.getKey(); payoutTxKey = payoutTx.getKey();
payoutTxFee = payoutTx.getFee().longValueExact();
for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId);
// set final payout amounts
if (getDisputeState().isNotDisputed()) {
BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(splitTxFee);
getSeller().setPayoutTxFee(splitTxFee);
getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount()));
getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee()));
} else if (getDisputeState().isClosed()) {
DisputeResult disputeResult = getDisputeResult();
BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee());
getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]);
getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]);
getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee()));
getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee()));
}
}
public DisputeResult getDisputeResult() {
if (getDisputes().isEmpty()) return null;
return getDisputes().get(getDisputes().size() - 1).getDisputeResultProperty().get();
}
@Nullable
public MoneroTx getPayoutTx() {
if (payoutTx == null) {
payoutTx = payoutTxId == null ? null : (this instanceof ArbitratorTrade) ? xmrWalletService.getTxWithCache(payoutTxId) : xmrWalletService.getWallet().getTx(payoutTxId);
}
return payoutTx;
}
public void setPayoutTxFee(BigInteger payoutTxFee) {
this.payoutTxFee = payoutTxFee.longValueExact();
}
public BigInteger getPayoutTxFee() {
return BigInteger.valueOf(payoutTxFee);
} }
public void setErrorMessage(String errorMessage) { public void setErrorMessage(String errorMessage) {
@ -1653,15 +1698,7 @@ public abstract class Trade implements Tradable, Model {
@Override @Override
public BigInteger getTotalTxFee() { public BigInteger getTotalTxFee() {
return BigInteger.valueOf(totalTxFee); return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees
}
@Nullable
public MoneroTx getPayoutTx() {
if (payoutTx == null) {
payoutTx = payoutTxId == null ? null : (this instanceof ArbitratorTrade) ? xmrWalletService.getTxWithCache(payoutTxId) : xmrWalletService.getWallet().getTx(payoutTxId);
}
return payoutTx;
} }
public boolean hasErrorMessage() { public boolean hasErrorMessage() {
@ -2019,7 +2056,6 @@ public abstract class Trade implements Tradable, Model {
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
.setOffer(offer.toProtoMessage()) .setOffer(offer.toProtoMessage())
.setTakerFee(takerFee) .setTakerFee(takerFee)
.setTotalTxFee(totalTxFee)
.setTakeOfferDate(takeOfferDate) .setTakeOfferDate(takeOfferDate)
.setProcessModel(processModel.toProtoMessage()) .setProcessModel(processModel.toProtoMessage())
.setAmount(amount) .setAmount(amount)
@ -2081,7 +2117,7 @@ public abstract class Trade implements Tradable, Model {
return "Trade{" + return "Trade{" +
"\n offer=" + offer + "\n offer=" + offer +
",\n takerFee=" + takerFee + ",\n takerFee=" + takerFee +
",\n totalTxFee=" + totalTxFee + ",\n totalTxFee=" + getTotalTxFee() +
",\n takeOfferDate=" + takeOfferDate + ",\n takeOfferDate=" + takeOfferDate +
",\n processModel=" + processModel + ",\n processModel=" + processModel +
",\n payoutTxId='" + payoutTxId + '\'' + ",\n payoutTxId='" + payoutTxId + '\'' +
@ -2098,7 +2134,6 @@ public abstract class Trade implements Tradable, Model {
",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' + ",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' +
",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' + ",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' +
",\n chatMessages=" + chatMessages + ",\n chatMessages=" + chatMessages +
",\n totalTxFee=" + totalTxFee +
",\n takerFee=" + takerFee + ",\n takerFee=" + takerFee +
",\n xmrWalletService=" + xmrWalletService + ",\n xmrWalletService=" + xmrWalletService +
",\n stateProperty=" + stateProperty + ",\n stateProperty=" + stateProperty +

View file

@ -136,6 +136,11 @@ public final class TradePeer implements PersistablePayload {
private long depositTxFee; private long depositTxFee;
private long securityDeposit; private long securityDeposit;
@Nullable @Nullable
@Setter
private String unsignedPayoutTxHex;
private long payoutTxFee;
private long payoutAmount;
@Nullable
private String updatedMultisigHex; private String updatedMultisigHex;
@Getter @Getter
@Setter @Setter
@ -161,6 +166,22 @@ public final class TradePeer implements PersistablePayload {
this.securityDeposit = securityDeposit.longValueExact(); this.securityDeposit = securityDeposit.longValueExact();
} }
public BigInteger getPayoutTxFee() {
return BigInteger.valueOf(payoutTxFee);
}
public void setPayoutTxFee(BigInteger payoutTxFee) {
this.payoutTxFee = payoutTxFee.longValueExact();
}
public BigInteger getPayoutAmount() {
return BigInteger.valueOf(payoutAmount);
}
public void setPayoutAmount(BigInteger payoutAmount) {
this.payoutAmount = payoutAmount.longValueExact();
}
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder();
@ -191,12 +212,15 @@ public final class TradePeer implements PersistablePayload {
Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex)); Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex));
Optional.ofNullable(exchangedMultisigHex).ifPresent(e -> builder.setExchangedMultisigHex(exchangedMultisigHex)); Optional.ofNullable(exchangedMultisigHex).ifPresent(e -> builder.setExchangedMultisigHex(exchangedMultisigHex));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash)); Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash));
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
Optional.ofNullable(depositTxFee).ifPresent(e -> builder.setDepositTxFee(depositTxFee)); Optional.ofNullable(depositTxFee).ifPresent(e -> builder.setDepositTxFee(depositTxFee));
Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit)); Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee));
Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount));
builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked);
builder.setCurrentDate(currentDate); builder.setCurrentDate(currentDate);
@ -237,13 +261,16 @@ public final class TradePeer implements PersistablePayload {
tradePeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex())); tradePeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
tradePeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex())); tradePeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
tradePeer.setExchangedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getExchangedMultisigHex())); tradePeer.setExchangedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getExchangedMultisigHex()));
tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked());
tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()));
tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
tradePeer.setDepositTxFee(BigInteger.valueOf(proto.getDepositTxFee())); tradePeer.setDepositTxFee(BigInteger.valueOf(proto.getDepositTxFee()));
tradePeer.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit())); tradePeer.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit()));
tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()));
tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked()); tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee()));
tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount()));
return tradePeer; return tradePeer;
} }
} }

View file

@ -136,17 +136,17 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
if (!trade.isPayoutPublished()) { if (!trade.isPayoutPublished()) {
if (isSigned) { if (isSigned) {
log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId()); log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId());
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);
} else { } else {
try { try {
PaymentSentMessage paymentSentMessage = (trade.isArbitrator() ? trade.getBuyer() : trade.getArbitrator()).getPaymentSentMessage(); PaymentSentMessage paymentSentMessage = (trade.isArbitrator() ? trade.getBuyer() : trade.getArbitrator()).getPaymentSentMessage();
if (paymentSentMessage == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId()); if (paymentSentMessage == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId());
if (StringUtils.equals(trade.getPayoutTxHex(), paymentSentMessage.getPayoutTxHex())) { // unsigned if (StringUtils.equals(trade.getPayoutTxHex(), paymentSentMessage.getPayoutTxHex())) { // unsigned
log.info("{} {} verifying, signing, and publishing payout tx", trade.getClass().getSimpleName(), trade.getId()); log.info("{} {} verifying, signing, and publishing payout tx", trade.getClass().getSimpleName(), trade.getId());
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); trade.processPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
} else { } else {
log.info("{} {} re-verifying and publishing payout tx", trade.getClass().getSimpleName(), trade.getId()); log.info("{} {} re-verifying and publishing payout tx", trade.getClass().getSimpleName(), trade.getId());
trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true); trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
} }
} catch (Exception e) { } catch (Exception e) {
trade.syncAndPollWallet(); trade.syncAndPollWallet();
@ -157,7 +157,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
} }
} else { } else {
log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);
} }
} }
} }

View file

@ -48,7 +48,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
if (trade.getPayoutTxHex() != null) { if (trade.getPayoutTxHex() != null) {
try { try {
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId()); log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true); trade.processPayoutTx(trade.getPayoutTxHex(), true, true);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}. Creating unsigned payout tx", trade.getId(), e.getMessage()); log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}. Creating unsigned payout tx", trade.getId(), e.getMessage());
createUnsignedPayoutTx(); createUnsignedPayoutTx();
@ -60,7 +60,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
// republish payout tx from previous message // republish payout tx from previous message
log.info("Seller re-verifying and publishing payout tx for trade {}", trade.getId()); log.info("Seller re-verifying and publishing payout tx for trade {}", trade.getId());
trade.verifyPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true); trade.processPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true);
} }
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();

View file

@ -211,8 +211,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
if (applyPeersDisputeResult) { if (applyPeersDisputeResult) {
// If the other peers dispute has been closed we apply the result to ourselves // If the other peers dispute has been closed we apply the result to ourselves
DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get();
disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); disputeResult.setBuyerPayoutAmountBeforeCost(peersDisputeResult.getBuyerPayoutAmountBeforeCost());
disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); disputeResult.setSellerPayoutAmountBeforeCost(peersDisputeResult.getSellerPayoutAmountBeforeCost());
disputeResult.setWinner(peersDisputeResult.getWinner()); disputeResult.setWinner(peersDisputeResult.getWinner());
disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setReason(peersDisputeResult.getReason());
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
@ -402,8 +402,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount);
} }
disputeResult.setBuyerPayoutAmount(buyerAmount); disputeResult.setBuyerPayoutAmountBeforeCost(buyerAmount);
disputeResult.setSellerPayoutAmount(sellerAmount); disputeResult.setSellerPayoutAmountBeforeCost(sellerAmount);
disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.Winner.BUYER : DisputeResult.Winner.SELLER); // TODO: UI should allow selection of receiver of exact custom amount, otherwise defaulting to bigger receiver. could extend API to specify who pays payout tx fee: buyer, seller, or both disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.Winner.BUYER : DisputeResult.Winner.SELLER); // TODO: UI should allow selection of receiver of exact custom amount, otherwise defaulting to bigger receiver. could extend API to specify who pays payout tx fee: buyer, seller, or both
disputeResult.setSubtractFeeFrom(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.SubtractFeeFrom.SELLER_ONLY : DisputeResult.SubtractFeeFrom.BUYER_ONLY); disputeResult.setSubtractFeeFrom(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.SubtractFeeFrom.SELLER_ONLY : DisputeResult.SubtractFeeFrom.BUYER_ONLY);
} }
@ -587,8 +587,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
!trade.isPayoutPublished()) { !trade.isPayoutPublished()) {
// create payout tx // create payout tx
MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false); MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
trade.getProcessModel().setUnsignedPayoutTx((MoneroTxWallet) payoutTx);
// show confirmation // show confirmation
showPayoutTxConfirmation(contract, showPayoutTxConfirmation(contract,
@ -709,8 +708,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
throw new IllegalStateException("Unknown radio button"); throw new IllegalStateException("Unknown radio button");
} }
disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1); disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1);
buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount())); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()));
sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount())); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()));
} }
private void applyTradeAmountRadioButtonStates() { private void applyTradeAmountRadioButtonStates() {
@ -719,8 +718,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
BigInteger tradeAmount = contract.getTradeAmount(); BigInteger tradeAmount = contract.getTradeAmount();
BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost();
BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost();
buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount)); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount));
sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount)); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount));

View file

@ -91,8 +91,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
VOLUME(Res.get("shared.amount")), VOLUME(Res.get("shared.amount")),
VOLUME_CURRENCY(Res.get("shared.currency")), VOLUME_CURRENCY(Res.get("shared.currency")),
TX_FEE(Res.get("shared.txFee")), TX_FEE(Res.get("shared.txFee")),
TRADE_FEE_BTC(Res.get("shared.tradeFee") + " BTC"), TRADE_FEE(Res.get("shared.tradeFee")),
TRADE_FEE_BSQ(Res.get("shared.tradeFee") + " BSQ"),
BUYER_SEC(Res.get("shared.buyerSecurityDeposit")), BUYER_SEC(Res.get("shared.buyerSecurityDeposit")),
SELLER_SEC(Res.get("shared.sellerSecurityDeposit")), SELLER_SEC(Res.get("shared.sellerSecurityDeposit")),
OFFER_TYPE(Res.get("shared.offerType")), OFFER_TYPE(Res.get("shared.offerType")),
@ -158,7 +157,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
@Override @Override
public void initialize() { public void initialize() {
widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue);
tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE_BTC.toString().replace(" BTC", ""))); tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString().replace(" BTC", "")));
buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString())); buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString()));
sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString())); sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString()));
priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString()));
@ -275,11 +274,9 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = item.getVolumeCurrencyAsString(); columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = item.getVolumeCurrencyAsString();
columns[ColumnNames.TX_FEE.ordinal()] = item.getTxFeeAsString(); columns[ColumnNames.TX_FEE.ordinal()] = item.getTxFeeAsString();
if (model.dataModel.isCurrencyForTradeFeeBtc(item.getTradable())) { if (model.dataModel.isCurrencyForTradeFeeBtc(item.getTradable())) {
columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = item.getTradeFeeAsString(false); columns[ColumnNames.TRADE_FEE.ordinal()] = item.getTradeFeeAsString(false);
columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = "";
} else { } else {
columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = ""; columns[ColumnNames.TRADE_FEE.ordinal()] = "";
columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = item.getTradeFeeAsString(false);
} }
columns[ColumnNames.BUYER_SEC.ordinal()] = item.getBuyerSecurityDepositAsString(); columns[ColumnNames.BUYER_SEC.ordinal()] = item.getBuyerSecurityDepositAsString();
columns[ColumnNames.SELLER_SEC.ordinal()] = item.getSellerSecurityDepositAsString(); columns[ColumnNames.SELLER_SEC.ordinal()] = item.getSellerSecurityDepositAsString();

View file

@ -679,8 +679,8 @@ public abstract class TradeStepView extends AnchorPane {
DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get();
Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); Contract contract = checkNotNull(trade.getContract(), "contract must not be null");
boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRingProvider().get()); boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRingProvider().get());
String buyerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount(), true); String buyerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true);
String sellerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount(), true); String sellerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true);
String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount; String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount;
String peersPayoutAmount = isMyRoleBuyer ? sellerPayoutAmount : buyerPayoutAmount; String peersPayoutAmount = isMyRoleBuyer ? sellerPayoutAmount : buyerPayoutAmount;

View file

@ -639,10 +639,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
String sellersRole = contract.isBuyerMakerAndSellerTaker() ? "Seller as taker" : "Seller as maker"; String sellersRole = contract.isBuyerMakerAndSellerTaker() ? "Seller as taker" : "Seller as maker";
String opener = firstDispute.isDisputeOpenerIsBuyer() ? buyersRole : sellersRole; String opener = firstDispute.isDisputeOpenerIsBuyer() ? buyersRole : sellersRole;
DisputeResult disputeResult = firstDispute.getDisputeResultProperty().get(); DisputeResult disputeResult = firstDispute.getDisputeResultProperty().get();
String winner = disputeResult != null && String winner = disputeResult != null && disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller";
disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; String buyerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true) : "";
String buyerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount(), true) : ""; String sellerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true) : "";
String sellerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount(), true) : "";
int index = disputeIndex.incrementAndGet(); int index = disputeIndex.incrementAndGet();
String tradeDateString = dateFormatter.format(firstDispute.getTradeDate()); String tradeDateString = dateFormatter.format(firstDispute.getTradeDate());

View file

@ -831,33 +831,38 @@ message TradeInfo {
uint64 date = 4; uint64 date = 4;
string role = 5; string role = 5;
uint64 taker_fee = 6 [jstype = JS_STRING]; uint64 taker_fee = 6 [jstype = JS_STRING];
string taker_fee_tx_id = 7; uint64 amount = 7 [jstype = JS_STRING];
string payout_tx_id = 8; uint64 buyer_security_deposit = 8 [jstype = JS_STRING];
uint64 amount = 9 [jstype = JS_STRING]; uint64 seller_security_deposit = 9 [jstype = JS_STRING];
uint64 buyer_security_deposit = 10 [jstype = JS_STRING]; uint64 buyer_deposit_tx_fee = 10 [jstype = JS_STRING];
uint64 seller_security_deposit = 11 [jstype = JS_STRING]; uint64 seller_deposit_tx_fee = 11 [jstype = JS_STRING];
string price = 12; uint64 buyer_payout_tx_fee = 12 [jstype = JS_STRING];
string arbitrator_node_address = 13; uint64 seller_payout_tx_fee = 13 [jstype = JS_STRING];
string trade_peer_node_address = 14; uint64 buyer_payout_amount = 14 [jstype = JS_STRING];
string state = 15; uint64 seller_payout_amount = 15 [jstype = JS_STRING];
string phase = 16; string price = 16;
string period_state = 17; string arbitrator_node_address = 17;
string payout_state = 18; string trade_peer_node_address = 18;
string dispute_state = 19; string state = 19;
bool is_deposits_published = 20; string phase = 20;
bool is_deposits_confirmed = 21; string period_state = 21;
bool is_deposits_unlocked = 22; string payout_state = 22;
bool is_payment_sent = 23; string dispute_state = 23;
bool is_payment_received = 24; bool is_deposits_published = 24;
bool is_payout_published = 25; bool is_deposits_confirmed = 25;
bool is_payout_confirmed = 26; bool is_deposits_unlocked = 26;
bool is_payout_unlocked = 27; bool is_payment_sent = 27;
bool is_completed = 28; bool is_payment_received = 28;
string contract_as_json = 29; bool is_payout_published = 29;
ContractInfo contract = 30; bool is_payout_confirmed = 30;
string trade_volume = 31; bool is_payout_unlocked = 31;
string maker_deposit_tx_id = 32; bool is_completed = 32;
string taker_deposit_tx_id = 33; string contract_as_json = 33;
ContractInfo contract = 34;
string trade_volume = 35;
string maker_deposit_tx_id = 36;
string taker_deposit_tx_id = 37;
string payout_tx_id = 38;
} }
message ContractInfo { message ContractInfo {

View file

@ -758,8 +758,8 @@ message DisputeResult {
string summary_notes = 8; string summary_notes = 8;
ChatMessage chat_message = 9; ChatMessage chat_message = 9;
bytes arbitrator_signature = 10; bytes arbitrator_signature = 10;
int64 buyer_payout_amount = 11; int64 buyer_payout_amount_before_cost = 11;
int64 seller_payout_amount = 12; int64 seller_payout_amount_before_cost = 12;
SubtractFeeFrom subtract_fee_from = 13; SubtractFeeFrom subtract_fee_from = 13;
bytes arbitrator_pub_key = 14; bytes arbitrator_pub_key = 14;
int64 close_date = 15; int64 close_date = 15;
@ -1488,28 +1488,27 @@ message Trade {
string payout_tx_key = 5; string payout_tx_key = 5;
int64 amount = 6; int64 amount = 6;
int64 taker_fee = 8; int64 taker_fee = 8;
int64 total_tx_fee = 9; int64 take_offer_date = 9;
int64 take_offer_date = 10; int64 price = 10;
int64 price = 11; State state = 11;
State state = 12; PayoutState payout_state = 12;
PayoutState payout_state = 13; DisputeState dispute_state = 13;
DisputeState dispute_state = 14; TradePeriodState period_state = 14;
TradePeriodState period_state = 15; Contract contract = 15;
Contract contract = 16; string contract_as_json = 16;
string contract_as_json = 17; bytes contract_hash = 17;
bytes contract_hash = 18; NodeAddress arbitrator_node_address = 18;
NodeAddress arbitrator_node_address = 19; NodeAddress mediator_node_address = 19;
NodeAddress mediator_node_address = 20; string error_message = 20;
string error_message = 21; string counter_currency_tx_id = 21;
string counter_currency_tx_id = 22; repeated ChatMessage chat_message = 22;
repeated ChatMessage chat_message = 23; MediationResultState mediation_result_state = 23;
MediationResultState mediation_result_state = 24; int64 lock_time = 24;
int64 lock_time = 25; int64 start_time = 25;
int64 start_time = 26; NodeAddress refund_agent_node_address = 26;
NodeAddress refund_agent_node_address = 27; RefundResultState refund_result_state = 27;
RefundResultState refund_result_state = 28; string counter_currency_extra_data = 28;
string counter_currency_extra_data = 29; string uid = 29;
string uid = 30;
} }
message BuyerAsMakerTrade { message BuyerAsMakerTrade {
@ -1572,21 +1571,23 @@ message TradePeer {
PaymentSentMessage payment_sent_message = 23; PaymentSentMessage payment_sent_message = 23;
PaymentReceivedMessage payment_received_message = 24; PaymentReceivedMessage payment_received_message = 24;
DisputeClosedMessage dispute_closed_message = 25; DisputeClosedMessage dispute_closed_message = 25;
string reserve_tx_hash = 26;
string reserve_tx_hash = 1001; string reserve_tx_hex = 27;
string reserve_tx_hex = 1002; string reserve_tx_key = 28;
string reserve_tx_key = 1003; repeated string reserve_tx_key_images = 29;
repeated string reserve_tx_key_images = 1004; string prepared_multisig_hex = 30;
string prepared_multisig_hex = 1005; string made_multisig_hex = 31;
string made_multisig_hex = 1006; string exchanged_multisig_hex = 32;
string exchanged_multisig_hex = 1007; string updated_multisig_hex = 33;
string deposit_tx_hash = 1008; bool deposits_confirmed_message_acked = 34;
string deposit_tx_hex = 1009; string deposit_tx_hash = 35;
string deposit_tx_key = 1010; string deposit_tx_hex = 36;
int64 deposit_tx_fee = 1011; string deposit_tx_key = 37;
int64 security_deposit = 1012; int64 deposit_tx_fee = 38;
string updated_multisig_hex = 1013; int64 security_deposit = 39;
bool deposits_confirmed_message_acked = 1014; string unsigned_payout_tx_hex = 40;
int64 payout_tx_fee = 41;
int64 payout_amount = 42;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////