fix incorrect deposit amount for range trades

improve display of reserved and pending balances by adjusting
support subtracting fee from buyer and/or seller on dispute resolution
validate trade amount is within offer amount
expose maker's split output tx fee
expose security deposit received from buyer and seller
This commit is contained in:
woodser 2023-10-27 17:11:46 -04:00
parent 0294062312
commit 05e2d925f0
25 changed files with 267 additions and 91 deletions

View file

@ -146,9 +146,10 @@ public class CoreDisputesService {
synchronized (trade) { synchronized (trade) {
try { try {
var closeDate = new Date();
var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
// create dispute result
var closeDate = new Date();
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
DisputePayout payout; DisputePayout payout;
if (customWinnerAmount > 0) { if (customWinnerAmount > 0) {
payout = DisputePayout.CUSTOM; payout = DisputePayout.CUSTOM;
@ -159,13 +160,14 @@ public class CoreDisputesService {
} else { } else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
} }
applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
winnerDisputeResult.setSubtractFeeFrom(customWinnerAmount == 0 ? DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER : winner == DisputeResult.Winner.BUYER ? DisputeResult.SubtractFeeFrom.SELLER_ONLY : DisputeResult.SubtractFeeFrom.BUYER_ONLY);
// create dispute payout tx // create dispute payout tx
trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), disputeResult, false)); trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), winnerDisputeResult, false));
// close winning dispute ticket // close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> { closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
}, (errMessage, err) -> { }, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err); throw new IllegalStateException(errMessage, err);
@ -178,8 +180,9 @@ 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(disputeResult.getBuyerPayoutAmount()); loserDisputeResult.setBuyerPayoutAmount(winnerDisputeResult.getBuyerPayoutAmount());
loserDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); loserDisputeResult.setSellerPayoutAmount(winnerDisputeResult.getSellerPayoutAmount());
loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom());
closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> { closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
}, (errMessage, err) -> { }, (errMessage, err) -> {
@ -239,6 +242,7 @@ public class CoreDisputesService {
disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount)); disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount)); disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount));
} }
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); // TODO: can extend UI to specify who pays mining fee
} }
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler, FaultHandler faultHandler) { public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler, FaultHandler faultHandler) {

View file

@ -243,7 +243,7 @@ public class CoreOffersService {
for (Offer offer2 : offers) { for (Offer offer2 : offers) {
if (offer == offer2) continue; if (offer == offer2) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, seen in offer {}", keyImage, offer2.getId()); log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
duplicateFundedOffers.add(offer2); duplicateFundedOffers.add(offer2);
} }
} }

View file

@ -134,7 +134,7 @@ class CoreTradesService {
takeOfferModel.initModel(offer, paymentAccount, amount, useSavingsWallet); takeOfferModel.initModel(offer, paymentAccount, amount, useSavingsWallet);
takerFee = takeOfferModel.getTakerFee(); takerFee = takeOfferModel.getTakerFee();
fundsNeededForTrade = takeOfferModel.getFundsNeededForTrade(); fundsNeededForTrade = takeOfferModel.getFundsNeededForTrade();
log.info("Initiating take {} offer, {}", offer.isBuyOffer() ? "buy" : "sell", takeOfferModel); log.debug("Initiating take {} offer, {}", offer.isBuyOffer() ? "buy" : "sell", takeOfferModel);
} }
// take offer // take offer

View file

@ -74,6 +74,9 @@ public class OfferInfo implements Payload {
private final int protocolVersion; private final int protocolVersion;
@Nullable @Nullable
private final String arbitratorSigner; private final String arbitratorSigner;
@Nullable
private final String splitOutputTxHash;
private final long splitOutputTxFee;
public OfferInfo(OfferInfoBuilder builder) { public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId(); this.id = builder.getId();
@ -103,6 +106,8 @@ public class OfferInfo implements Payload {
this.versionNumber = builder.getVersionNumber(); this.versionNumber = builder.getVersionNumber();
this.protocolVersion = builder.getProtocolVersion(); this.protocolVersion = builder.getProtocolVersion();
this.arbitratorSigner = builder.getArbitratorSigner(); this.arbitratorSigner = builder.getArbitratorSigner();
this.splitOutputTxHash = builder.getSplitOutputTxHash();
this.splitOutputTxFee = builder.getSplitOutputTxFee();
} }
public static OfferInfo toOfferInfo(Offer offer) { public static OfferInfo toOfferInfo(Offer offer) {
@ -127,6 +132,8 @@ public class OfferInfo implements Payload {
.withTriggerPrice(preciseTriggerPrice) .withTriggerPrice(preciseTriggerPrice)
.withState(openOffer.getState().name()) .withState(openOffer.getState().name())
.withIsActivated(isActivated) .withIsActivated(isActivated)
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
.build(); .build();
} }
@ -199,8 +206,10 @@ public class OfferInfo implements Payload {
.setOwnerNodeAddress(ownerNodeAddress) .setOwnerNodeAddress(ownerNodeAddress)
.setPubKeyRing(pubKeyRing) .setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber) .setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion); .setProtocolVersion(protocolVersion)
.setSplitOutputTxFee(splitOutputTxFee);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
return builder.build(); return builder.build();
} }
@ -234,6 +243,8 @@ public class OfferInfo implements Payload {
.withVersionNumber(proto.getVersionNr()) .withVersionNumber(proto.getVersionNr())
.withProtocolVersion(proto.getProtocolVersion()) .withProtocolVersion(proto.getProtocolVersion())
.withArbitratorSigner(proto.getArbitratorSigner()) .withArbitratorSigner(proto.getArbitratorSigner())
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
.build(); .build();
} }
} }

View file

@ -59,6 +59,8 @@ public final class OfferInfoBuilder {
private String versionNumber; private String versionNumber;
private int protocolVersion; private int protocolVersion;
private String arbitratorSigner; private String arbitratorSigner;
private String splitOutputTxHash;
private long splitOutputTxFee;
public OfferInfoBuilder withId(String id) { public OfferInfoBuilder withId(String id) {
this.id = id; this.id = id;
@ -209,6 +211,16 @@ public final class OfferInfoBuilder {
this.arbitratorSigner = arbitratorSigner; this.arbitratorSigner = arbitratorSigner;
return this; return this;
} }
public OfferInfoBuilder withSplitOutputTxHash(String splitOutputTxHash) {
this.splitOutputTxHash = splitOutputTxHash;
return this;
}
public OfferInfoBuilder withSplitOutputTxFee(long splitOutputTxFee) {
this.splitOutputTxFee = splitOutputTxFee;
return this;
}
public OfferInfo build() { public OfferInfo build() {
return new OfferInfo(this); return new OfferInfo(this);

View file

@ -222,7 +222,7 @@ public class OfferUtil {
getMaxBuyerSecurityDepositAsPercent()); getMaxBuyerSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
"securityDeposit must not be less than " + "securityDeposit must not be less than " +
getMinBuyerSecurityDepositAsPercent()); getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode), checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned")); Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),

View file

@ -70,6 +70,9 @@ public final class OpenOffer implements Tradable {
@Getter @Getter
@Nullable @Nullable
String splitOutputTxHash; String splitOutputTxHash;
@Getter
@Setter
long splitOutputTxFee;
@Nullable @Nullable
@Setter @Setter
@Getter @Getter
@ -114,6 +117,7 @@ public final class OpenOffer implements Tradable {
this.scheduledAmount = openOffer.scheduledAmount; this.scheduledAmount = openOffer.scheduledAmount;
this.scheduledTxHashes = openOffer.scheduledTxHashes == null ? null : new ArrayList<String>(openOffer.scheduledTxHashes); this.scheduledTxHashes = openOffer.scheduledTxHashes == null ? null : new ArrayList<String>(openOffer.scheduledTxHashes);
this.splitOutputTxHash = openOffer.splitOutputTxHash; this.splitOutputTxHash = openOffer.splitOutputTxHash;
this.splitOutputTxFee = openOffer.splitOutputTxFee;
this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHash = openOffer.reserveTxHash;
this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey; this.reserveTxKey = openOffer.reserveTxKey;
@ -130,6 +134,7 @@ public final class OpenOffer implements Tradable {
@Nullable String scheduledAmount, @Nullable String scheduledAmount,
@Nullable List<String> scheduledTxHashes, @Nullable List<String> scheduledTxHashes,
String splitOutputTxHash, String splitOutputTxHash,
long splitOutputTxFee,
@Nullable String reserveTxHash, @Nullable String reserveTxHash,
@Nullable String reserveTxHex, @Nullable String reserveTxHex,
@Nullable String reserveTxKey) { @Nullable String reserveTxKey) {
@ -139,6 +144,7 @@ public final class OpenOffer implements Tradable {
this.reserveExactAmount = reserveExactAmount; this.reserveExactAmount = reserveExactAmount;
this.scheduledTxHashes = scheduledTxHashes; this.scheduledTxHashes = scheduledTxHashes;
this.splitOutputTxHash = splitOutputTxHash; this.splitOutputTxHash = splitOutputTxHash;
this.splitOutputTxFee = splitOutputTxFee;
this.reserveTxHash = reserveTxHash; this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; this.reserveTxKey = reserveTxKey;
@ -153,6 +159,7 @@ public final class OpenOffer implements Tradable {
.setOffer(offer.toProtoMessage()) .setOffer(offer.toProtoMessage())
.setTriggerPrice(triggerPrice) .setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name())) .setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setSplitOutputTxFee(splitOutputTxFee)
.setReserveExactAmount(reserveExactAmount); .setReserveExactAmount(reserveExactAmount);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount)); Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
@ -173,6 +180,7 @@ public final class OpenOffer implements Tradable {
proto.getScheduledAmount(), proto.getScheduledAmount(),
proto.getScheduledTxHashesList(), proto.getScheduledTxHashesList(),
ProtoUtil.stringOrNullFromProto(proto.getSplitOutputTxHash()), ProtoUtil.stringOrNullFromProto(proto.getSplitOutputTxHash()),
proto.getSplitOutputTxFee(),
proto.getReserveTxHash(), proto.getReserveTxHash(),
proto.getReserveTxHex(), proto.getReserveTxHex(),
proto.getReserveTxKey()); proto.getReserveTxKey());
@ -253,6 +261,9 @@ public final class OpenOffer implements Tradable {
",\n offer=" + offer + ",\n offer=" + offer +
",\n state=" + state + ",\n state=" + state +
",\n triggerPrice=" + triggerPrice + ",\n triggerPrice=" + triggerPrice +
",\n reserveExactAmount=" + reserveExactAmount +
",\n scheduledAmount=" + scheduledAmount +
",\n splitOutputTxFee=" + splitOutputTxFee +
"\n}"; "\n}";
} }
} }

View file

@ -1006,8 +1006,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.info("Done creating split output tx to fund offer {}", openOffer.getId()); log.info("Done creating split output tx to fund offer {}", openOffer.getId());
// schedule txs // schedule txs
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact());
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
openOffer.setScheduledAmount(openOffer.getOffer().getReserveAmount().toString()); openOffer.setScheduledAmount(openOffer.getOffer().getReserveAmount().toString());
openOffer.setState(OpenOffer.State.SCHEDULED); openOffer.setState(OpenOffer.State.SCHEDULED);
return splitOutputTx; return splitOutputTx;

View file

@ -69,6 +69,7 @@ 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;
@ -869,12 +870,29 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// add any loss of precision to winner payout // add any loss of precision to winner payout
winnerPayoutAmount = winnerPayoutAmount.add(trade.getWallet().getUnlockedBalance().subtract(winnerPayoutAmount.add(loserPayoutAmount))); winnerPayoutAmount = winnerPayoutAmount.add(trade.getWallet().getUnlockedBalance().subtract(winnerPayoutAmount.add(loserPayoutAmount)));
// create dispute payout tx // create dispute payout tx config
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount); if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount);
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount); if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount);
txConfig.setSubtractFeeFrom(loserPayoutAmount.equals(BigInteger.ZERO) ? 0 : txConfig.getDestinations().size() - 1); // winner only pays fee if loser gets 0
txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); // configure who pays mining fee
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));
break;
case BUYER_ONLY:
txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.BUYER ? 0 : 1);
break;
case SELLER_ONLY:
txConfig.setSubtractFeeFrom(disputeResult.getWinner() == Winner.SELLER ? 0 : 1);
break;
}
}
// create dispute payout tx
MoneroTxWallet payoutTx = null; MoneroTxWallet payoutTx = null;
try { try {
payoutTx = trade.getWallet().createTx(txConfig); payoutTx = trade.getWallet().createTx(txConfig);

View file

@ -61,12 +61,21 @@ public final class DisputeResult implements NetworkPayload {
PEER_WAS_LATE PEER_WAS_LATE
} }
public enum SubtractFeeFrom {
BUYER_ONLY,
SELLER_ONLY,
BUYER_AND_SELLER
}
private final String tradeId; private final String tradeId;
private final int traderId; private final int traderId;
@Setter @Setter
@Nullable @Nullable
private Winner winner; private Winner winner;
private int reasonOrdinal = Reason.OTHER.ordinal(); private int reasonOrdinal = Reason.OTHER.ordinal();
@Setter
@Nullable
private SubtractFeeFrom subtractFeeFrom;
private final BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty(); private final BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty();
private final BooleanProperty idVerificationProperty = new SimpleBooleanProperty(); private final BooleanProperty idVerificationProperty = new SimpleBooleanProperty();
private final BooleanProperty screenCastProperty = new SimpleBooleanProperty(); private final BooleanProperty screenCastProperty = new SimpleBooleanProperty();
@ -93,6 +102,7 @@ public final class DisputeResult implements NetworkPayload {
int traderId, int traderId,
@Nullable Winner winner, @Nullable Winner winner,
int reasonOrdinal, int reasonOrdinal,
@Nullable SubtractFeeFrom subtractFeeFrom,
boolean tamperProofEvidence, boolean tamperProofEvidence,
boolean idVerification, boolean idVerification,
boolean screenCast, boolean screenCast,
@ -107,6 +117,7 @@ public final class DisputeResult implements NetworkPayload {
this.traderId = traderId; this.traderId = traderId;
this.winner = winner; this.winner = winner;
this.reasonOrdinal = reasonOrdinal; this.reasonOrdinal = reasonOrdinal;
this.subtractFeeFrom = subtractFeeFrom;
this.tamperProofEvidenceProperty.set(tamperProofEvidence); this.tamperProofEvidenceProperty.set(tamperProofEvidence);
this.idVerificationProperty.set(idVerification); this.idVerificationProperty.set(idVerification);
this.screenCastProperty.set(screenCast); this.screenCastProperty.set(screenCast);
@ -129,6 +140,7 @@ public final class DisputeResult implements NetworkPayload {
proto.getTraderId(), proto.getTraderId(),
ProtoUtil.enumFromProto(DisputeResult.Winner.class, proto.getWinner().name()), ProtoUtil.enumFromProto(DisputeResult.Winner.class, proto.getWinner().name()),
proto.getReasonOrdinal(), proto.getReasonOrdinal(),
ProtoUtil.enumFromProto(DisputeResult.SubtractFeeFrom.class, proto.getSubtractFeeFrom().name()),
proto.getTamperProofEvidence(), proto.getTamperProofEvidence(),
proto.getIdVerification(), proto.getIdVerification(),
proto.getScreenCast(), proto.getScreenCast(),
@ -158,6 +170,7 @@ public final class DisputeResult implements NetworkPayload {
Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature)));
Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey)));
Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name())));
Optional.ofNullable(subtractFeeFrom).ifPresent(result -> builder.setSubtractFeeFrom(protobuf.DisputeResult.SubtractFeeFrom.valueOf(subtractFeeFrom.name())));
Optional.ofNullable(chatMessage).ifPresent(chatMessage -> Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
@ -201,6 +214,7 @@ public final class DisputeResult implements NetworkPayload {
} }
public void setBuyerPayoutAmount(BigInteger buyerPayoutAmount) { public void setBuyerPayoutAmount(BigInteger buyerPayoutAmount) {
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmount cannot be negative");
this.buyerPayoutAmount = buyerPayoutAmount.longValueExact(); this.buyerPayoutAmount = buyerPayoutAmount.longValueExact();
} }
@ -209,6 +223,7 @@ public final class DisputeResult implements NetworkPayload {
} }
public void setSellerPayoutAmount(BigInteger sellerPayoutAmount) { public void setSellerPayoutAmount(BigInteger sellerPayoutAmount) {
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmount cannot be negative");
this.sellerPayoutAmount = sellerPayoutAmount.longValueExact(); this.sellerPayoutAmount = sellerPayoutAmount.longValueExact();
} }
@ -231,6 +246,7 @@ public final class DisputeResult implements NetworkPayload {
",\n traderId=" + traderId + ",\n traderId=" + traderId +
",\n winner=" + winner + ",\n winner=" + winner +
",\n reasonOrdinal=" + reasonOrdinal + ",\n reasonOrdinal=" + reasonOrdinal +
",\n subtractFeeFrom=" + subtractFeeFrom +
",\n tamperProofEvidenceProperty=" + tamperProofEvidenceProperty + ",\n tamperProofEvidenceProperty=" + tamperProofEvidenceProperty +
",\n idVerificationProperty=" + idVerificationProperty + ",\n idVerificationProperty=" + idVerificationProperty +
",\n screenCastProperty=" + screenCastProperty + ",\n screenCastProperty=" + screenCastProperty +

View file

@ -390,9 +390,25 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
BigInteger expectedWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount(); BigInteger expectedWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount();
BigInteger expectedLoserAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount(); BigInteger expectedLoserAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount();
// winner pays cost if loser gets nothing, otherwise loser pays cost // subtract mining fee from expected payouts
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner pays fee if loser gets 0
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); 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 // verify winner and loser payout amounts
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount); if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);

View file

@ -69,11 +69,13 @@ import monero.common.MoneroError;
import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper; import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroKeyImage;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
import monero.wallet.MoneroWalletRpc; import monero.wallet.MoneroWalletRpc;
import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxQuery;
@ -1595,6 +1597,24 @@ public abstract class Trade implements Tradable, Model {
return offer.getShortId(); return offer.getShortId();
} }
public BigInteger getFrozenAmount() {
BigInteger sum = BigInteger.valueOf(0);
for (String keyImage : getSelf().getReserveTxKeyImages()) {
List<MoneroOutputWallet> outputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); // TODO: will this check tx pool? avoid
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
}
return sum;
}
public BigInteger getReservedAmount() {
if (!isDepositsPublished() || isPayoutPublished()) return BigInteger.valueOf(0);
if (isArbitrator()) {
return getAmount().add(getBuyer().getSecurityDeposit()).add(getSeller().getSecurityDeposit()); // arbitrator reserved balance is sum of amounts sent to multisig
} else {
return isBuyer() ? getBuyer().getSecurityDeposit() : getAmount().add(getSeller().getSecurityDeposit());
}
}
public Price getPrice() { public Price getPrice() {
return Price.valueOf(offer.getCurrencyCode(), price); return Price.valueOf(offer.getCurrencyCode(), price);
} }

View file

@ -815,6 +815,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId()));
// validate inputs
if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount");
if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount");
OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser, paymentAccountId, amount); OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser, paymentAccountId, amount);
offer.checkOfferAvailability(model, offer.checkOfferAvailability(model,
() -> { () -> {
@ -998,7 +1002,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published) // If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published)
// we move the trade to failedTradesManager // we move the trade to FailedTradesManager
public void onMoveInvalidTradeToFailedTrades(Trade trade) { public void onMoveInvalidTradeToFailedTrades(Trade trade) {
removeTrade(trade); removeTrade(trade);
failedTradesManager.add(trade); failedTradesManager.add(trade);

View file

@ -30,15 +30,21 @@ import java.util.Optional;
public final class DepositResponse extends TradeMessage implements DirectMessage { public final class DepositResponse extends TradeMessage implements DirectMessage {
private final long currentDate; private final long currentDate;
private final String errorMessage; private final String errorMessage;
private final long buyerSecurityDeposit;
private final long sellerSecurityDeposit;
public DepositResponse(String tradeId, public DepositResponse(String tradeId,
String uid, String uid,
String messageVersion, String messageVersion,
long currentDate, long currentDate,
String errorMessage) { String errorMessage,
long buyerSecurityDeposit,
long sellerSecurityDeposit) {
super(messageVersion, tradeId, uid); super(messageVersion, tradeId, uid);
this.currentDate = currentDate; this.currentDate = currentDate;
this.errorMessage = errorMessage; this.errorMessage = errorMessage;
this.buyerSecurityDeposit = buyerSecurityDeposit;
this.sellerSecurityDeposit = sellerSecurityDeposit;
} }
@ -52,6 +58,8 @@ public final class DepositResponse extends TradeMessage implements DirectMessage
.setTradeId(tradeId) .setTradeId(tradeId)
.setUid(uid); .setUid(uid);
builder.setCurrentDate(currentDate); builder.setCurrentDate(currentDate);
builder.setBuyerSecurityDeposit(buyerSecurityDeposit);
builder.setSellerSecurityDeposit(sellerSecurityDeposit);
Optional.ofNullable(errorMessage).ifPresent(e -> builder.setErrorMessage(errorMessage)); Optional.ofNullable(errorMessage).ifPresent(e -> builder.setErrorMessage(errorMessage));
return getNetworkEnvelopeBuilder().setDepositResponse(builder).build(); return getNetworkEnvelopeBuilder().setDepositResponse(builder).build();
@ -64,7 +72,9 @@ public final class DepositResponse extends TradeMessage implements DirectMessage
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
proto.getCurrentDate(), proto.getCurrentDate(),
ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()),
proto.getBuyerSecurityDeposit(),
proto.getSellerSecurityDeposit());
} }
@Override @Override
@ -72,6 +82,8 @@ public final class DepositResponse extends TradeMessage implements DirectMessage
return "DepositResponse {" + return "DepositResponse {" +
",\n currentDate=" + currentDate + ",\n currentDate=" + currentDate +
",\n errorMessage=" + errorMessage + ",\n errorMessage=" + errorMessage +
",\n buyerSecurityDeposit=" + buyerSecurityDeposit +
",\n sellerSecurityDeposit=" + sellerSecurityDeposit +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -116,6 +116,7 @@ public final class TradePeer implements PersistablePayload {
private String depositTxHex; private String depositTxHex;
@Nullable @Nullable
private String depositTxKey; private String depositTxKey;
private long depositTxFee;
private long securityDeposit; private long securityDeposit;
@Nullable @Nullable
private String updatedMultisigHex; private String updatedMultisigHex;
@ -126,7 +127,16 @@ public final class TradePeer implements PersistablePayload {
public TradePeer() { public TradePeer() {
} }
public BigInteger getDepositTxFee() {
return BigInteger.valueOf(depositTxFee);
}
public void setDepositTxFee(BigInteger depositTxFee) {
this.depositTxFee = depositTxFee.longValueExact();
}
public BigInteger getSecurityDeposit() { public BigInteger getSecurityDeposit() {
if (depositTxHash == null) return null;
return BigInteger.valueOf(securityDeposit); return BigInteger.valueOf(securityDeposit);
} }
@ -164,6 +174,7 @@ public final class TradePeer implements PersistablePayload {
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(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit)); Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked);
@ -206,6 +217,7 @@ public final class TradePeer implements PersistablePayload {
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.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit())); tradePeer.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit()));
tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked()); tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked());

View file

@ -80,7 +80,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
boolean isFromTaker = trader == trade.getTaker(); boolean isFromTaker = trader == trade.getTaker();
boolean isFromBuyer = trader == trade.getBuyer(); boolean isFromBuyer = trader == trade.getBuyer();
BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee(); BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee();
BigInteger sendAmount = isFromBuyer ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger sendAmount = isFromBuyer ? BigInteger.valueOf(0) : trade.getAmount();
BigInteger securityDeposit = isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); BigInteger securityDeposit = isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
String depositAddress = processModel.getMultisigAddress(); String depositAddress = processModel.getMultisigAddress();
@ -103,6 +103,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// set deposit info // set deposit info
trader.setSecurityDeposit(txResult.second); trader.setSecurityDeposit(txResult.second);
trader.setDepositTxFee(txResult.first.getFee());
trader.setDepositTxHex(request.getDepositTxHex()); trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey()); trader.setDepositTxKey(request.getDepositTxKey());
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
@ -130,7 +131,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
new Date().getTime(), new Date().getTime(),
null); null,
trade.getBuyer().getSecurityDeposit().longValue(),
trade.getSeller().getSecurityDeposit().longValue());
// send deposit response to maker and taker // send deposit response to maker and taker
sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response); sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response);
@ -158,7 +161,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
new Date().getTime(), new Date().getTime(),
t.getMessage()); t.getMessage(),
trade.getBuyer().getSecurityDeposit().longValue(),
trade.getSeller().getSecurityDeposit().longValue());
// send deposit response to maker and taker // send deposit response to maker and taker
sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response); sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response);

View file

@ -20,6 +20,7 @@ package haveno.core.trade.protocol.tasks;
import haveno.common.app.Version; import haveno.common.app.Version;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade; import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
@ -31,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -91,10 +93,15 @@ public class MaybeSendSignContractRequest extends TradeTask {
processModel.setDepositTxXmr(depositTx); // TODO: redundant with trade.getSelf().setDepositTx(), remove? processModel.setDepositTxXmr(depositTx); // TODO: redundant with trade.getSelf().setDepositTx(), remove?
trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(reservedKeyImages); trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
// TODO: security deposit should be based on trade amount, not max offer amount
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getOffer().getBuyerSecurityDeposit() : trade.getOffer().getSellerSecurityDeposit();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
// maker signs deposit hash nonce to avoid challenge protocol // maker signs deposit hash nonce to avoid challenge protocol
byte[] sig = null; byte[] sig = null;
if (trade instanceof MakerTrade) { if (trade instanceof MakerTrade) {

View file

@ -18,6 +18,8 @@
package haveno.core.trade.protocol.tasks; package haveno.core.trade.protocol.tasks;
import java.math.BigInteger;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositResponse;
@ -43,10 +45,17 @@ public class ProcessDepositResponse extends TradeTask {
throw new RuntimeException(message.getErrorMessage()); throw new RuntimeException(message.getErrorMessage());
} }
// record security deposits
trade.getBuyer().setSecurityDeposit(BigInteger.valueOf(message.getBuyerSecurityDeposit()));
trade.getSeller().setSecurityDeposit(BigInteger.valueOf(message.getSellerSecurityDeposit()));
// set success state // set success state
trade.setStateIfValidTransitionTo(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); trade.setStateIfValidTransitionTo(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS);
trade.addInitProgressStep(); trade.addInitProgressStep();
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
// update balances
trade.getXmrWalletService().updateBalanceListeners();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -53,6 +53,10 @@ public class ProcessInitTradeRequest extends TradeTask {
checkNotNull(request); checkNotNull(request);
checkTradeId(processModel.getOfferId(), request); checkTradeId(processModel.getOfferId(), request);
// validate inputs
if (trade.getAmount().compareTo(trade.getOffer().getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount");
if (trade.getAmount().compareTo(trade.getOffer().getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount");
// handle request as arbitrator // handle request as arbitrator
TradePeer multisigParticipant; TradePeer multisigParticipant;
if (trade instanceof ArbitratorTrade) { if (trade instanceof ArbitratorTrade) {

View file

@ -17,28 +17,24 @@
package haveno.core.xmr; package haveno.core.xmr;
import haveno.core.offer.OfferPayload;
import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager;
import haveno.core.trade.failed.FailedTradesManager; import haveno.core.trade.failed.FailedTradesManager;
import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.listeners.XmrBalanceListener;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.P2PService;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet;
import javax.inject.Inject; import javax.inject.Inject;
import java.math.BigInteger; import java.math.BigInteger;
@ -77,19 +73,19 @@ public class Balances {
} }
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> updatedBalances()); openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> updateBalances());
tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updatedBalances()); tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updateBalances());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> updatedBalances()); refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> updateBalances());
xmrWalletService.addBalanceListener(new XmrBalanceListener() { xmrWalletService.addBalanceListener(new XmrBalanceListener() {
@Override @Override
public void onBalanceChanged(BigInteger balance) { public void onBalanceChanged(BigInteger balance) {
updatedBalances(); updateBalances();
} }
}); });
updatedBalances(); updateBalances();
} }
private void updatedBalances() { private void updateBalances() {
if (!xmrWalletService.isWalletReady()) return; if (!xmrWalletService.isWalletReady()) return;
try { try {
updateAvailableBalance(); updateAvailableBalance();
@ -111,7 +107,18 @@ public class Balances {
private void updatePendingBalance() { private void updatePendingBalance() {
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.valueOf(0) : xmrWalletService.getWallet().getBalance(0); BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.valueOf(0) : xmrWalletService.getWallet().getBalance(0);
BigInteger unlockedBalance = xmrWalletService.getWallet() == null ? BigInteger.valueOf(0) : xmrWalletService.getWallet().getUnlockedBalance(0); BigInteger unlockedBalance = xmrWalletService.getWallet() == null ? BigInteger.valueOf(0) : xmrWalletService.getWallet().getUnlockedBalance(0);
pendingBalance.set(balance.subtract(unlockedBalance)); BigInteger pendingBalanceSum = balance.subtract(unlockedBalance);
// add frozen trade balances - reserved amounts
List<Trade> trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
for (Trade trade : trades) {
if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue;
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
pendingBalanceSum = pendingBalanceSum.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee());
}
// add frozen offer balances
pendingBalance.set(pendingBalanceSum);
} }
private void updateReservedOfferBalance() { private void updateReservedOfferBalance() {
@ -120,30 +127,21 @@ public class Balances {
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
for (MoneroOutputWallet frozenOutput : frozenOutputs) sum = sum.add(frozenOutput.getAmount()); for (MoneroOutputWallet frozenOutput : frozenOutputs) sum = sum.add(frozenOutput.getAmount());
} }
// subtract frozen trade balances
List<Trade> trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
for (Trade trade : trades) {
sum = sum.subtract(trade.getFrozenAmount());
}
reservedOfferBalance.set(sum); reservedOfferBalance.set(sum);
} }
private void updateReservedTradeBalance() { private void updateReservedTradeBalance() {
BigInteger sum = BigInteger.valueOf(0); BigInteger sum = BigInteger.valueOf(0);
List<Trade> openTrades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); List<Trade> trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
for (Trade trade : openTrades) { for (Trade trade : trades) {
try { sum = sum.add(trade.getReservedAmount());
List<MoneroTxWallet> depositTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
.setHash(trade.getSelf().getDepositTxHash())
.setInTxPool(false)); // don't check pool
if (depositTxs.size() != 1 || !depositTxs.get(0).isConfirmed()) continue; // outputs are frozen until confirmed by arbitrator's broadcast
} catch (MoneroError e) {
continue;
}
if (trade.getContract() == null) continue;
Long reservedAmt;
OfferPayload offerPayload = trade.getContract().getOfferPayload();
if (trade.getArbitratorNodeAddress().equals(P2PService.getMyNodeAddress())) { // TODO (woodser): this only works if node address does not change
reservedAmt = offerPayload.getAmount() + offerPayload.getBuyerSecurityDeposit() + offerPayload.getSellerSecurityDeposit(); // arbitrator reserved balance is sum of amounts sent to multisig
} else {
reservedAmt = trade.getContract().isMyRoleBuyer(tradeManager.getKeyRing().getPubKeyRing()) ? offerPayload.getBuyerSecurityDeposit() : offerPayload.getAmount() + offerPayload.getSellerSecurityDeposit();
}
sum = sum.add(BigInteger.valueOf(reservedAmt));
} }
reservedTradeBalance.set(sum); reservedTradeBalance.set(sum);
} }

View file

@ -368,7 +368,7 @@ public class XmrWalletService {
return reserveTx; return reserveTx;
} }
/**s /**
* Create the multisig deposit tx and freeze its inputs. * Create the multisig deposit tx and freeze its inputs.
* *
* @param trade the trade to create a deposit tx from * @param trade the trade to create a deposit tx from
@ -388,8 +388,8 @@ public class XmrWalletService {
Offer offer = trade.getProcessModel().getOffer(); Offer offer = trade.getProcessModel().getOffer();
String multisigAddress = trade.getProcessModel().getMultisigAddress(); String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee(); BigInteger tradeFee = trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.valueOf(0) : trade.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); // TODO: security deposit should be based on trade amount
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
log.info("Creating deposit tx with multisig address={}", multisigAddress); log.info("Creating deposit tx with multisig address={}", multisigAddress);
MoneroTxWallet depositTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex); MoneroTxWallet depositTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex);
@ -467,6 +467,7 @@ public class XmrWalletService {
* Checks double spends, trade fee, deposit amount and destination, and miner fee. * Checks double spends, trade fee, deposit amount and destination, and miner fee.
* The transaction is submitted to the pool then flushed without relaying. * The transaction is submitted to the pool then flushed without relaying.
* *
* @param offerId id of offer to verify trade tx
* @param tradeFee trade fee * @param tradeFee trade fee
* @param sendAmount amount to give peer * @param sendAmount amount to give peer
* @param securityDeposit security deposit amount * @param securityDeposit security deposit amount
@ -876,25 +877,6 @@ public class XmrWalletService {
} }
} }
private void notifyBalanceListeners() {
for (XmrBalanceListener balanceListener : balanceListeners) {
BigInteger balance;
if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex());
else balance = getAvailableBalance();
UserThread.execute(new Runnable() { // TODO (woodser): don't execute on UserThread
@Override
public void run() {
try {
balanceListener.onBalanceChanged(balance);
} catch (Exception e) {
log.warn("Failed to notify balance listener of change");
e.printStackTrace();
}
}
});
}
}
private void changeWalletPasswords(String oldPassword, String newPassword) { private void changeWalletPasswords(String oldPassword, String newPassword) {
// create task to change main wallet password // create task to change main wallet password
@ -1195,6 +1177,25 @@ public class XmrWalletService {
balanceListeners.remove(listener); balanceListeners.remove(listener);
} }
public void updateBalanceListeners() {
for (XmrBalanceListener balanceListener : balanceListeners) {
BigInteger balance;
if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex());
else balance = getAvailableBalance();
UserThread.execute(new Runnable() { // TODO (woodser): don't execute on UserThread
@Override
public void run() {
try {
balanceListener.onBalanceChanged(balance);
} catch (Exception e) {
log.warn("Failed to notify balance listener of change");
e.printStackTrace();
}
}
});
}
}
public void saveAddressEntryList() { public void saveAddressEntryList() {
xmrAddressEntryList.requestPersistence(); xmrAddressEntryList.requestPersistence();
} }
@ -1259,7 +1260,7 @@ public class XmrWalletService {
@Override @Override
public void run() { public void run() {
for (MoneroWalletListenerI listener : walletListeners) listener.onBalancesChanged(newBalance, newUnlockedBalance); for (MoneroWalletListenerI listener : walletListeners) listener.onBalancesChanged(newBalance, newUnlockedBalance);
notifyBalanceListeners(); updateBalanceListeners();
} }
}); });
} }

View file

@ -192,6 +192,7 @@ public class AccountAgeWitnessServiceTest {
1, 1,
DisputeResult.Winner.BUYER, DisputeResult.Winner.BUYER,
DisputeResult.Reason.OTHER.ordinal(), DisputeResult.Reason.OTHER.ordinal(),
DisputeResult.SubtractFeeFrom.BUYER_ONLY,
true, true,
true, true,
true, true,

View file

@ -216,6 +216,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
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());
disputeResult.setSubtractFeeFrom(peersDisputeResult.getSubtractFeeFrom());
buyerGetsTradeAmountRadioButton.setDisable(true); buyerGetsTradeAmountRadioButton.setDisable(true);
buyerGetsAllRadioButton.setDisable(true); buyerGetsAllRadioButton.setDisable(true);
@ -403,7 +404,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setBuyerPayoutAmount(buyerAmount); disputeResult.setBuyerPayoutAmount(buyerAmount);
disputeResult.setSellerPayoutAmount(sellerAmount); disputeResult.setSellerPayoutAmount(sellerAmount);
disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? // 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.Winner.BUYER : DisputeResult.Winner.BUYER :
DisputeResult.Winner.SELLER); DisputeResult.Winner.SELLER);
} }

View file

@ -541,6 +541,8 @@ message OfferInfo {
string version_nr = 25; string version_nr = 25;
int32 protocol_version = 26; int32 protocol_version = 26;
string arbitrator_signer = 27; string arbitrator_signer = 27;
string split_output_tx_hash = 28;
uint64 split_output_tx_fee = 29 [jstype = JS_STRING];
} }
message AvailabilityResultWithDescription { message AvailabilityResultWithDescription {

View file

@ -281,6 +281,8 @@ message DepositResponse {
string uid = 2; string uid = 2;
int64 current_date = 3; int64 current_date = 3;
string error_message = 4; string error_message = 4;
int64 buyerSecurityDeposit = 5;
int64 sellerSecurityDeposit = 6;
} }
message DepositsConfirmedMessage { message DepositsConfirmedMessage {
@ -740,6 +742,12 @@ message DisputeResult {
PEER_WAS_LATE = 12; PEER_WAS_LATE = 12;
} }
enum SubtractFeeFrom {
BUYER_ONLY = 0;
SELLER_ONLY = 1;
BUYER_AND_SELLER = 2;
}
string trade_id = 1; string trade_id = 1;
int32 trader_id = 2; int32 trader_id = 2;
Winner winner = 3; Winner winner = 3;
@ -752,9 +760,10 @@ message DisputeResult {
bytes arbitrator_signature = 10; bytes arbitrator_signature = 10;
int64 buyer_payout_amount = 11; int64 buyer_payout_amount = 11;
int64 seller_payout_amount = 12; int64 seller_payout_amount = 12;
bytes arbitrator_pub_key = 13; SubtractFeeFrom subtract_fee_from = 13;
int64 close_date = 14; bytes arbitrator_pub_key = 14;
bool is_loser_publisher = 15; int64 close_date = 15;
bool is_loser_publisher = 16;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -1374,12 +1383,13 @@ message OpenOffer {
State state = 2; State state = 2;
int64 trigger_price = 3; int64 trigger_price = 3;
bool reserve_exact_amount = 4; bool reserve_exact_amount = 4;
repeated string scheduled_tx_hashes = 5; string split_output_tx_hash = 5;
string scheduled_amount = 6; // BigInteger int64 split_output_tx_fee = 6;
string split_output_tx_hash = 7; repeated string scheduled_tx_hashes = 7;
string reserve_tx_hash = 8; string scheduled_amount = 8; // BigInteger
string reserve_tx_hex = 9; string reserve_tx_hash = 9;
string reserve_tx_key = 10; string reserve_tx_hex = 10;
string reserve_tx_key = 11;
} }
message Tradable { message Tradable {
@ -1573,9 +1583,10 @@ message TradePeer {
string deposit_tx_hash = 1008; string deposit_tx_hash = 1008;
string deposit_tx_hex = 1009; string deposit_tx_hex = 1009;
string deposit_tx_key = 1010; string deposit_tx_key = 1010;
int64 security_deposit = 1011; int64 deposit_tx_fee = 1011;
string updated_multisig_hex = 1012; int64 security_deposit = 1012;
bool deposits_confirmed_message_acked = 1013; string updated_multisig_hex = 1013;
bool deposits_confirmed_message_acked = 1014;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////