diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index 0cb12417..a72e85ff 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -163,9 +163,6 @@ public class CoreDisputesService { } applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount); - // create dispute payout tx - trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), winnerDisputeResult, false)); - // close winning dispute ticket closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { arbitrationManager.requestPersistence(); @@ -180,8 +177,8 @@ public class CoreDisputesService { if (!loserDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute"); var loserDispute = loserDisputeOptional.get(); var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate); - loserDisputeResult.setBuyerPayoutAmount(winnerDisputeResult.getBuyerPayoutAmount()); - loserDisputeResult.setSellerPayoutAmount(winnerDisputeResult.getSellerPayoutAmount()); + loserDisputeResult.setBuyerPayoutAmountBeforeCost(winnerDisputeResult.getBuyerPayoutAmountBeforeCost()); + loserDisputeResult.setSellerPayoutAmountBeforeCost(winnerDisputeResult.getSellerPayoutAmountBeforeCost()); loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom()); closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> { arbitrationManager.requestPersistence(); @@ -217,31 +214,23 @@ public class CoreDisputesService { BigInteger tradeAmount = contract.getTradeAmount(); disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) { - disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); - disputeResult.setSellerPayoutAmount(sellerSecurityDeposit); + disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit)); + disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit); } else if (payout == DisputePayout.BUYER_GETS_ALL) { - disputeResult.setBuyerPayoutAmount(tradeAmount - .add(buyerSecurityDeposit) - .add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) - disputeResult.setSellerPayoutAmount(BigInteger.valueOf(0)); + disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) + disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.valueOf(0)); } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { - disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); - disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit)); + disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); + disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); } else if (payout == DisputePayout.SELLER_GETS_ALL) { - disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(0)); - disputeResult.setSellerPayoutAmount(tradeAmount - .add(sellerSecurityDeposit) - .add(buyerSecurityDeposit)); + disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.valueOf(0)); + disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); } else if (payout == DisputePayout.CUSTOM) { - if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) { - throw new RuntimeException("Winner payout is more than the trade wallet's balance"); - } + if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) 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(); - if (loserAmount < 0) { - throw new RuntimeException("Loser payout cannot be negative"); - } - disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount)); - disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount)); + if (loserAmount < 0) 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.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, Res.get("disputeSummaryWindow.reason." + reason.name()), amount, - HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount(), true), - HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount(), true), + HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true), + HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true), disputeResult.summaryNotesProperty().get() ); diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index c4486aed..1642c606 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -71,6 +71,12 @@ public class TradeInfo implements Payload { private final long amount; private final long buyerSecurityDeposit; 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 volume; private final String arbitratorNodeAddress; @@ -105,6 +111,12 @@ public class TradeInfo implements Payload { this.amount = builder.getAmount(); this.buyerSecurityDeposit = builder.getBuyerSecurityDeposit(); 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.volume = builder.getVolume(); this.arbitratorNodeAddress = builder.getArbitratorNodeAddress(); @@ -161,6 +173,13 @@ public class TradeInfo implements Payload { .withAmount(trade.getAmount().longValueExact()) .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().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)) .withVolume(toRoundedVolume.apply(trade)) .withArbitratorNodeAddress(toArbitratorNodeAddress.apply(trade)) @@ -204,6 +223,12 @@ public class TradeInfo implements Payload { .setAmount(amount) .setBuyerSecurityDeposit(buyerSecurityDeposit) .setSellerSecurityDeposit(sellerSecurityDeposit) + .setBuyerDepositTxFee(buyerDepositTxFee) + .setSellerDepositTxFee(sellerDepositTxFee) + .setBuyerPayoutTxFee(buyerPayoutTxFee) + .setSellerPayoutTxFee(sellerPayoutTxFee) + .setBuyerPayoutAmount(buyerPayoutAmount) + .setSellerPayoutAmount(sellerPayoutAmount) .setPrice(price) .setTradeVolume(volume) .setArbitratorNodeAddress(arbitratorNodeAddress) @@ -241,6 +266,12 @@ public class TradeInfo implements Payload { .withAmount(proto.getAmount()) .withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit()) .withSellerSecurityDeposit(proto.getSellerSecurityDeposit()) + .withBuyerDepositTxFee(proto.getBuyerDepositTxFee()) + .withSellerDepositTxFee(proto.getSellerDepositTxFee()) + .withBuyerPayoutTxFee(proto.getBuyerPayoutTxFee()) + .withSellerPayoutTxFee(proto.getSellerPayoutTxFee()) + .withBuyerPayoutAmount(proto.getBuyerPayoutAmount()) + .withSellerPayoutAmount(proto.getSellerPayoutAmount()) .withPrice(proto.getPrice()) .withVolume(proto.getTradeVolume()) .withPeriodState(proto.getPeriodState()) @@ -278,6 +309,12 @@ public class TradeInfo implements Payload { ", amount='" + amount + '\'' + "\n" + ", buyerSecurityDeposit='" + buyerSecurityDeposit + '\'' + "\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" + ", arbitratorNodeAddress='" + arbitratorNodeAddress + '\'' + "\n" + ", tradePeerNodeAddress='" + tradePeerNodeAddress + '\'' + "\n" + diff --git a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java index 79870618..68d3a57e 100644 --- a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java @@ -41,6 +41,12 @@ public final class TradeInfoV1Builder { private long takerFee; private long buyerSecurityDeposit; 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 takerDepositTxId; private String payoutTxId; @@ -117,6 +123,36 @@ public final class TradeInfoV1Builder { 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) { this.makerDepositTxId = makerDepositTxId; return this; diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 6a1f69c9..95ad78d6 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -69,7 +69,6 @@ import java.math.BigInteger; import java.security.KeyPair; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; @@ -729,23 +728,21 @@ public abstract class DisputeManager> extends Sup 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()); - if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) { - trade.getProcessModel().setUnsignedPayoutTx(createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false)); // can be null if we don't have receiver's multisig hex + if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) { + createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); } // 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(); - 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, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType(), 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 // send dispute closed message @@ -812,12 +809,6 @@ public abstract class DisputeManager> extends Sup } } ); - - // save state - if (unsignedPayoutTx != null) { - trade.setPayoutTx(unsignedPayoutTx); - trade.setPayoutTxHex(unsignedPayoutTx.getTxSet().getMultisigTxHex()); - } trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); requestPersistence(); } catch (Exception e) { @@ -829,7 +820,7 @@ public abstract class DisputeManager> extends Sup // 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 trade.importMultisigHex(); @@ -848,42 +839,34 @@ public abstract class DisputeManager> extends Sup // 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()); - // 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 = 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))); + // check amounts + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); + if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).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 " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); } - // add any loss of precision to winner payout - winnerPayoutAmount = winnerPayoutAmount.add(trade.getWallet().getUnlockedBalance().subtract(winnerPayoutAmount.add(loserPayoutAmount))); - // create dispute payout tx config 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); - if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount); - if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount); + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); // 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 else { switch (disputeResult.getSubtractFeeFrom()) { case BUYER_AND_SELLER: - txConfig.setSubtractFeeFrom(Arrays.asList(0, 1)); + txConfig.setSubtractFeeFrom(0, 1); break; case BUYER_ONLY: - txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.BUYER ? 0 : 1); + txConfig.setSubtractFeeFrom(0); break; case SELLER_ONLY: - txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.SELLER ? 0 : 1); + txConfig.setSubtractFeeFrom(1); break; } } @@ -897,8 +880,14 @@ public abstract class DisputeManager> extends Sup throw new RuntimeException("Loser payout is too small to cover the mining fee"); } - // save updated multisig hex - trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); + // update trade state + 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; } catch (Exception e) { trade.syncAndPollWallet(); diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeResult.java b/core/src/main/java/haveno/core/support/dispute/DisputeResult.java index 7929b2b3..f428bfe1 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeResult.java @@ -86,8 +86,8 @@ public final class DisputeResult implements NetworkPayload { @Setter @Nullable private byte[] arbitratorSignature; - private long buyerPayoutAmount; - private long sellerPayoutAmount; + private long buyerPayoutAmountBeforeCost; + private long sellerPayoutAmountBeforeCost; @Setter @Nullable private byte[] arbitratorPubKey; @@ -109,8 +109,8 @@ public final class DisputeResult implements NetworkPayload { String summaryNotes, @Nullable ChatMessage chatMessage, @Nullable byte[] arbitratorSignature, - long buyerPayoutAmount, - long sellerPayoutAmount, + long buyerPayoutAmountBeforeCost, + long sellerPayoutAmountBeforeCost, @Nullable byte[] arbitratorPubKey, long closeDate) { this.tradeId = tradeId; @@ -124,8 +124,8 @@ public final class DisputeResult implements NetworkPayload { this.summaryNotesProperty.set(summaryNotes); this.chatMessage = chatMessage; this.arbitratorSignature = arbitratorSignature; - this.buyerPayoutAmount = buyerPayoutAmount; - this.sellerPayoutAmount = sellerPayoutAmount; + this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost; + this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost; this.arbitratorPubKey = arbitratorPubKey; this.closeDate = closeDate; } @@ -147,8 +147,8 @@ public final class DisputeResult implements NetworkPayload { proto.getSummaryNotes(), proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getArbitratorSignature().toByteArray(), - proto.getBuyerPayoutAmount(), - proto.getSellerPayoutAmount(), + proto.getBuyerPayoutAmountBeforeCost(), + proto.getSellerPayoutAmountBeforeCost(), proto.getArbitratorPubKey().toByteArray(), proto.getCloseDate()); } @@ -163,8 +163,8 @@ public final class DisputeResult implements NetworkPayload { .setIdVerification(idVerificationProperty.get()) .setScreenCast(screenCastProperty.get()) .setSummaryNotes(summaryNotesProperty.get()) - .setBuyerPayoutAmount(buyerPayoutAmount) - .setSellerPayoutAmount(sellerPayoutAmount) + .setBuyerPayoutAmountBeforeCost(buyerPayoutAmountBeforeCost) + .setSellerPayoutAmountBeforeCost(sellerPayoutAmountBeforeCost) .setCloseDate(closeDate); Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); @@ -213,22 +213,22 @@ public final class DisputeResult implements NetworkPayload { return summaryNotesProperty; } - public void setBuyerPayoutAmount(BigInteger buyerPayoutAmount) { - if (buyerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmount cannot be negative"); - this.buyerPayoutAmount = buyerPayoutAmount.longValueExact(); + public void setBuyerPayoutAmountBeforeCost(BigInteger buyerPayoutAmountBeforeCost) { + if (buyerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmountBeforeCost cannot be negative"); + this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost.longValueExact(); } - public BigInteger getBuyerPayoutAmount() { - return BigInteger.valueOf(buyerPayoutAmount); + public BigInteger getBuyerPayoutAmountBeforeCost() { + return BigInteger.valueOf(buyerPayoutAmountBeforeCost); } - public void setSellerPayoutAmount(BigInteger sellerPayoutAmount) { - if (sellerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmount cannot be negative"); - this.sellerPayoutAmount = sellerPayoutAmount.longValueExact(); + public void setSellerPayoutAmountBeforeCost(BigInteger sellerPayoutAmountBeforeCost) { + if (sellerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmountBeforeCost cannot be negative"); + this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost.longValueExact(); } - public BigInteger getSellerPayoutAmount() { - return BigInteger.valueOf(sellerPayoutAmount); + public BigInteger getSellerPayoutAmountBeforeCost() { + return BigInteger.valueOf(sellerPayoutAmountBeforeCost); } public void setCloseDate(Date closeDate) { @@ -253,8 +253,8 @@ public final class DisputeResult implements NetworkPayload { ",\n summaryNotesProperty=" + summaryNotesProperty + ",\n chatMessage=" + chatMessage + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + - ",\n buyerPayoutAmount=" + buyerPayoutAmount + - ",\n sellerPayoutAmount=" + sellerPayoutAmount + + ",\n buyerPayoutAmountBeforeCost=" + buyerPayoutAmountBeforeCost + + ",\n sellerPayoutAmountBeforeCost=" + sellerPayoutAmountBeforeCost + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n closeDate=" + closeDate + "\n}"; diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 57908663..fc7197da 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -380,42 +380,22 @@ public final class ArbitrationManager extends DisputeManager 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=" + actualWinnerAmount.add(actualLoserAmount) + ", winner payout=" + actualWinnerAmount + ", loser payout=" + actualLoserAmount); + BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // cost = fee + lost dust change + if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), trade.getId(), arbitratorSignedPayoutTx.getChangeAmount()); + 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 - BigInteger expectedWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount(); - BigInteger expectedLoserAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount(); - - // subtract mining fee from expected payouts - if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner pays fee if loser gets 0 - 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); + // verify payout amounts + BigInteger[] buyerSellerPayoutTxCost = getBuyerSellerPayoutTxCost(disputeResult, txCost); + BigInteger expectedBuyerAmount = disputeResult.getBuyerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[0]); + BigInteger expectedSellerAmount = disputeResult.getSellerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[1]); + if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new RuntimeException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount); + if (!expectedSellerAmount.equals(actualSellerAmount)) throw new RuntimeException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount); // check wallet's daemon connection trade.checkDaemonConnection(); @@ -431,7 +411,8 @@ public final class ArbitrationManager extends DisputeManager 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); } - } else { - disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex()); } // submit fully signed payout tx to the network @@ -468,4 +448,26 @@ public final class ArbitrationManager extends DisputeManager Trade trade = tradeOptional.get(); if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) { - trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmount().longValueExact()); - trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmount().longValueExact()); + trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmountBeforeCost().longValueExact()); + trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmountBeforeCost().longValueExact()); trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); @@ -222,8 +222,8 @@ public final class MediationManager extends DisputeManager Optional optionalDispute = findDispute(tradeId); checkArgument(optionalDispute.isPresent(), "dispute must be present"); DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); - BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); - BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); + BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); ProcessModel processModel = trade.getProcessModel(); processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.longValueExact()); processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.longValueExact()); diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 9991c66e..246d9f2a 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -58,6 +58,9 @@ import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; +import monero.wallet.model.MoneroDestination; +import monero.wallet.model.MoneroTxWallet; + import org.bitcoinj.core.Coin; /** @@ -543,4 +546,12 @@ public class HavenoUtils { if (c1 == null) return false; 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; + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index e1538895..c9a3ccbb 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -37,6 +37,8 @@ import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.proto.network.CoreNetworkProtoResolver; 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.refund.RefundResultState; import haveno.core.support.messages.ChatMessage; @@ -323,7 +325,6 @@ public abstract class Trade implements Tradable, Model { @Getter private final Offer offer; private final long takerFee; - private final long totalTxFee; // Added in 1.5.1 @Getter @@ -451,6 +452,7 @@ public abstract class Trade implements Tradable, Model { @Getter @Setter private String payoutTxKey; + private long payoutTxFee; private Long payoutHeight; private IdlePayoutSyncer idlePayoutSyncer; @@ -472,7 +474,6 @@ public abstract class Trade implements Tradable, Model { this.offer = offer; this.amount = tradeAmount.longValueExact(); this.takerFee = takerFee.longValueExact(); - this.totalTxFee = 0l; // TODO: sum tx fees this.price = tradePrice; this.xmrWalletService = xmrWalletService; this.processModel = processModel; @@ -941,20 +942,25 @@ public abstract class Trade implements Tradable, Model { .setRelay(false) .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()); return payoutTx; } /** - * Verify a payout tx. + * Process a payout tx. * * @param payoutTxHex is the payout tx hex to verify * @param sign signs the payout tx if true * @param publish publishes the signed payout tx if true */ - public void verifyPayoutTx(String payoutTxHex, boolean sign, boolean publish) { - log.info("Verifying payout tx"); + public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { + log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); // gather relevant info 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"); // 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"); // 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 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); // 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); // check wallet connection @@ -1025,7 +1033,6 @@ public abstract class Trade implements Tradable, Model { // submit payout tx 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); pollWallet(); } @@ -1296,9 +1303,47 @@ public abstract class Trade implements Tradable, Model { public void setPayoutTx(MoneroTxWallet payoutTx) { this.payoutTx = payoutTx; 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(); + payoutTxFee = payoutTx.getFee().longValueExact(); 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) { @@ -1653,15 +1698,7 @@ public abstract class Trade implements Tradable, Model { @Override public BigInteger getTotalTxFee() { - return BigInteger.valueOf(totalTxFee); - } - - @Nullable - public MoneroTx getPayoutTx() { - if (payoutTx == null) { - payoutTx = payoutTxId == null ? null : (this instanceof ArbitratorTrade) ? xmrWalletService.getTxWithCache(payoutTxId) : xmrWalletService.getWallet().getTx(payoutTxId); - } - return payoutTx; + return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees } public boolean hasErrorMessage() { @@ -2019,7 +2056,6 @@ public abstract class Trade implements Tradable, Model { protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() .setOffer(offer.toProtoMessage()) .setTakerFee(takerFee) - .setTotalTxFee(totalTxFee) .setTakeOfferDate(takeOfferDate) .setProcessModel(processModel.toProtoMessage()) .setAmount(amount) @@ -2081,7 +2117,7 @@ public abstract class Trade implements Tradable, Model { return "Trade{" + "\n offer=" + offer + ",\n takerFee=" + takerFee + - ",\n totalTxFee=" + totalTxFee + + ",\n totalTxFee=" + getTotalTxFee() + ",\n takeOfferDate=" + takeOfferDate + ",\n processModel=" + processModel + ",\n payoutTxId='" + payoutTxId + '\'' + @@ -2098,7 +2134,6 @@ public abstract class Trade implements Tradable, Model { ",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' + ",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' + ",\n chatMessages=" + chatMessages + - ",\n totalTxFee=" + totalTxFee + ",\n takerFee=" + takerFee + ",\n xmrWalletService=" + xmrWalletService + ",\n stateProperty=" + stateProperty + diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index 7a381831..2a755be9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -136,6 +136,11 @@ public final class TradePeer implements PersistablePayload { private long depositTxFee; private long securityDeposit; @Nullable + @Setter + private String unsignedPayoutTxHex; + private long payoutTxFee; + private long payoutAmount; + @Nullable private String updatedMultisigHex; @Getter @Setter @@ -161,6 +166,22 @@ public final class TradePeer implements PersistablePayload { 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 public Message toProtoMessage() { 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(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); 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(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(depositTxFee).ifPresent(e -> builder.setDepositTxFee(depositTxFee)); 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.setCurrentDate(currentDate); @@ -237,13 +261,16 @@ public final class TradePeer implements PersistablePayload { tradePeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex())); tradePeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex())); tradePeer.setExchangedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getExchangedMultisigHex())); + tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); + tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked()); tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradePeer.setDepositTxFee(BigInteger.valueOf(proto.getDepositTxFee())); tradePeer.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit())); - tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); - tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked()); + tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex())); + tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee())); + tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount())); return tradePeer; } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 79cd9890..b54febab 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -136,17 +136,17 @@ public class ProcessPaymentReceivedMessage extends TradeTask { if (!trade.isPayoutPublished()) { if (isSigned) { 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 { try { 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 (StringUtils.equals(trade.getPayoutTxHex(), paymentSentMessage.getPayoutTxHex())) { // unsigned 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 { 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) { trade.syncAndPollWallet(); @@ -157,7 +157,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { } } else { 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); } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index 5183a24b..a4d8bb72 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -48,7 +48,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { if (trade.getPayoutTxHex() != null) { try { 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) { log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}. Creating unsigned payout tx", trade.getId(), e.getMessage()); createUnsignedPayoutTx(); @@ -60,7 +60,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { // republish payout tx from previous message 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(); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 1c4ab0e6..a325e3b2 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -211,8 +211,8 @@ public class DisputeSummaryWindow extends Overlay { if (applyPeersDisputeResult) { // If the other peers dispute has been closed we apply the result to ourselves DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); - disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); - disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); + disputeResult.setBuyerPayoutAmountBeforeCost(peersDisputeResult.getBuyerPayoutAmountBeforeCost()); + disputeResult.setSellerPayoutAmountBeforeCost(peersDisputeResult.getSellerPayoutAmountBeforeCost()); disputeResult.setWinner(peersDisputeResult.getWinner()); disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); @@ -402,8 +402,8 @@ public class DisputeSummaryWindow extends Overlay { buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); } - disputeResult.setBuyerPayoutAmount(buyerAmount); - disputeResult.setSellerPayoutAmount(sellerAmount); + disputeResult.setBuyerPayoutAmountBeforeCost(buyerAmount); + 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.setSubtractFeeFrom(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.SubtractFeeFrom.SELLER_ONLY : DisputeResult.SubtractFeeFrom.BUYER_ONLY); } @@ -587,8 +587,7 @@ public class DisputeSummaryWindow extends Overlay { !trade.isPayoutPublished()) { // create payout tx - MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false); - trade.getProcessModel().setUnsignedPayoutTx((MoneroTxWallet) payoutTx); + MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); // show confirmation showPayoutTxConfirmation(contract, @@ -709,8 +708,8 @@ public class DisputeSummaryWindow extends Overlay { throw new IllegalStateException("Unknown radio button"); } disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1); - buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount())); - sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount())); + buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost())); + sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost())); } private void applyTradeAmountRadioButtonStates() { @@ -719,8 +718,8 @@ public class DisputeSummaryWindow extends Overlay { BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger tradeAmount = contract.getTradeAmount(); - BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); - BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); + BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount)); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount)); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java index e6fb43f5..ce05b831 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -91,8 +91,7 @@ public class ClosedTradesView extends ActivatableViewAndModel 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())); sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); @@ -275,11 +274,9 @@ public class ClosedTradesView extends ActivatableViewAndModel { String sellersRole = contract.isBuyerMakerAndSellerTaker() ? "Seller as taker" : "Seller as maker"; String opener = firstDispute.isDisputeOpenerIsBuyer() ? buyersRole : sellersRole; DisputeResult disputeResult = firstDispute.getDisputeResultProperty().get(); - String winner = disputeResult != null && - disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; - String buyerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmount(), true) : ""; - String sellerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmount(), true) : ""; + String winner = disputeResult != null && disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; + String buyerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true) : ""; + String sellerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true) : ""; int index = disputeIndex.incrementAndGet(); String tradeDateString = dateFormatter.format(firstDispute.getTradeDate()); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 20506eb2..dc7ab4c3 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -831,33 +831,38 @@ message TradeInfo { uint64 date = 4; string role = 5; uint64 taker_fee = 6 [jstype = JS_STRING]; - string taker_fee_tx_id = 7; - string payout_tx_id = 8; - uint64 amount = 9 [jstype = JS_STRING]; - uint64 buyer_security_deposit = 10 [jstype = JS_STRING]; - uint64 seller_security_deposit = 11 [jstype = JS_STRING]; - string price = 12; - string arbitrator_node_address = 13; - string trade_peer_node_address = 14; - string state = 15; - string phase = 16; - string period_state = 17; - string payout_state = 18; - string dispute_state = 19; - bool is_deposits_published = 20; - bool is_deposits_confirmed = 21; - bool is_deposits_unlocked = 22; - bool is_payment_sent = 23; - bool is_payment_received = 24; - bool is_payout_published = 25; - bool is_payout_confirmed = 26; - bool is_payout_unlocked = 27; - bool is_completed = 28; - string contract_as_json = 29; - ContractInfo contract = 30; - string trade_volume = 31; - string maker_deposit_tx_id = 32; - string taker_deposit_tx_id = 33; + uint64 amount = 7 [jstype = JS_STRING]; + uint64 buyer_security_deposit = 8 [jstype = JS_STRING]; + uint64 seller_security_deposit = 9 [jstype = JS_STRING]; + uint64 buyer_deposit_tx_fee = 10 [jstype = JS_STRING]; + uint64 seller_deposit_tx_fee = 11 [jstype = JS_STRING]; + uint64 buyer_payout_tx_fee = 12 [jstype = JS_STRING]; + uint64 seller_payout_tx_fee = 13 [jstype = JS_STRING]; + uint64 buyer_payout_amount = 14 [jstype = JS_STRING]; + uint64 seller_payout_amount = 15 [jstype = JS_STRING]; + string price = 16; + string arbitrator_node_address = 17; + string trade_peer_node_address = 18; + string state = 19; + string phase = 20; + string period_state = 21; + string payout_state = 22; + string dispute_state = 23; + bool is_deposits_published = 24; + bool is_deposits_confirmed = 25; + bool is_deposits_unlocked = 26; + bool is_payment_sent = 27; + bool is_payment_received = 28; + bool is_payout_published = 29; + bool is_payout_confirmed = 30; + bool is_payout_unlocked = 31; + bool is_completed = 32; + 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 { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 6420672d..67320e79 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -758,8 +758,8 @@ message DisputeResult { string summary_notes = 8; ChatMessage chat_message = 9; bytes arbitrator_signature = 10; - int64 buyer_payout_amount = 11; - int64 seller_payout_amount = 12; + int64 buyer_payout_amount_before_cost = 11; + int64 seller_payout_amount_before_cost = 12; SubtractFeeFrom subtract_fee_from = 13; bytes arbitrator_pub_key = 14; int64 close_date = 15; @@ -1488,28 +1488,27 @@ message Trade { string payout_tx_key = 5; int64 amount = 6; int64 taker_fee = 8; - int64 total_tx_fee = 9; - int64 take_offer_date = 10; - int64 price = 11; - State state = 12; - PayoutState payout_state = 13; - DisputeState dispute_state = 14; - TradePeriodState period_state = 15; - Contract contract = 16; - string contract_as_json = 17; - bytes contract_hash = 18; - NodeAddress arbitrator_node_address = 19; - NodeAddress mediator_node_address = 20; - string error_message = 21; - string counter_currency_tx_id = 22; - repeated ChatMessage chat_message = 23; - MediationResultState mediation_result_state = 24; - int64 lock_time = 25; - int64 start_time = 26; - NodeAddress refund_agent_node_address = 27; - RefundResultState refund_result_state = 28; - string counter_currency_extra_data = 29; - string uid = 30; + int64 take_offer_date = 9; + int64 price = 10; + State state = 11; + PayoutState payout_state = 12; + DisputeState dispute_state = 13; + TradePeriodState period_state = 14; + Contract contract = 15; + string contract_as_json = 16; + bytes contract_hash = 17; + NodeAddress arbitrator_node_address = 18; + NodeAddress mediator_node_address = 19; + string error_message = 20; + string counter_currency_tx_id = 21; + repeated ChatMessage chat_message = 22; + MediationResultState mediation_result_state = 23; + int64 lock_time = 24; + int64 start_time = 25; + NodeAddress refund_agent_node_address = 26; + RefundResultState refund_result_state = 27; + string counter_currency_extra_data = 28; + string uid = 29; } message BuyerAsMakerTrade { @@ -1572,21 +1571,23 @@ message TradePeer { PaymentSentMessage payment_sent_message = 23; PaymentReceivedMessage payment_received_message = 24; DisputeClosedMessage dispute_closed_message = 25; - - string reserve_tx_hash = 1001; - string reserve_tx_hex = 1002; - string reserve_tx_key = 1003; - repeated string reserve_tx_key_images = 1004; - string prepared_multisig_hex = 1005; - string made_multisig_hex = 1006; - string exchanged_multisig_hex = 1007; - string deposit_tx_hash = 1008; - string deposit_tx_hex = 1009; - string deposit_tx_key = 1010; - int64 deposit_tx_fee = 1011; - int64 security_deposit = 1012; - string updated_multisig_hex = 1013; - bool deposits_confirmed_message_acked = 1014; + string reserve_tx_hash = 26; + string reserve_tx_hex = 27; + string reserve_tx_key = 28; + repeated string reserve_tx_key_images = 29; + string prepared_multisig_hex = 30; + string made_multisig_hex = 31; + string exchanged_multisig_hex = 32; + string updated_multisig_hex = 33; + bool deposits_confirmed_message_acked = 34; + string deposit_tx_hash = 35; + string deposit_tx_hex = 36; + string deposit_tx_key = 37; + int64 deposit_tx_fee = 38; + int64 security_deposit = 39; + string unsigned_payout_tx_hex = 40; + int64 payout_tx_fee = 41; + int64 payout_amount = 42; } ///////////////////////////////////////////////////////////////////////////////////////////