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) {
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;
if (customWinnerAmount > 0) {
payout = DisputePayout.CUSTOM;
@ -159,13 +160,14 @@ public class CoreDisputesService {
} else {
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
trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), disputeResult, false));
trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), winnerDisputeResult, false));
// close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
arbitrationManager.requestPersistence();
}, (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");
var loserDispute = loserDisputeOptional.get();
var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate);
loserDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
loserDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
loserDisputeResult.setBuyerPayoutAmount(winnerDisputeResult.getBuyerPayoutAmount());
loserDisputeResult.setSellerPayoutAmount(winnerDisputeResult.getSellerPayoutAmount());
loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom());
closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> {
arbitrationManager.requestPersistence();
}, (errMessage, err) -> {
@ -239,6 +242,7 @@ public class CoreDisputesService {
disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount));
}
disputeResult.setSubtractFeeFrom(DisputeResult.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) {

View File

@ -243,7 +243,7 @@ public class CoreOffersService {
for (Offer offer2 : offers) {
if (offer == offer2) continue;
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);
}
}

View File

@ -134,7 +134,7 @@ class CoreTradesService {
takeOfferModel.initModel(offer, paymentAccount, amount, useSavingsWallet);
takerFee = takeOfferModel.getTakerFee();
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

View File

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

View File

@ -59,6 +59,8 @@ public final class OfferInfoBuilder {
private String versionNumber;
private int protocolVersion;
private String arbitratorSigner;
private String splitOutputTxHash;
private long splitOutputTxFee;
public OfferInfoBuilder withId(String id) {
this.id = id;
@ -209,6 +211,16 @@ public final class OfferInfoBuilder {
this.arbitratorSigner = arbitratorSigner;
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() {
return new OfferInfo(this);

View File

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

View File

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

View File

@ -69,6 +69,7 @@ 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;
@ -869,12 +870,29 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// add any loss of precision to winner payout
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);
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);
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;
try {
payoutTx = trade.getWallet().createTx(txConfig);

View File

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

View File

@ -69,11 +69,13 @@ import monero.common.MoneroError;
import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroKeyImage;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.MoneroWalletRpc;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery;
@ -1595,6 +1597,24 @@ public abstract class Trade implements Tradable, Model {
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() {
return Price.valueOf(offer.getCurrencyCode(), price);
}

View File

@ -815,6 +815,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
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);
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)
// we move the trade to failedTradesManager
// we move the trade to FailedTradesManager
public void onMoveInvalidTradeToFailedTrades(Trade trade) {
removeTrade(trade);
failedTradesManager.add(trade);

View File

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

View File

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

View File

@ -80,7 +80,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
boolean isFromTaker = trader == trade.getTaker();
boolean isFromBuyer = trader == trade.getBuyer();
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();
String depositAddress = processModel.getMultisigAddress();
@ -103,6 +103,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// set deposit info
trader.setSecurityDeposit(txResult.second);
trader.setDepositTxFee(txResult.first.getFee());
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
@ -130,7 +131,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
null);
null,
trade.getBuyer().getSecurityDeposit().longValue(),
trade.getSeller().getSecurityDeposit().longValue());
// send deposit response to maker and taker
sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response);
@ -158,7 +161,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
t.getMessage());
t.getMessage(),
trade.getBuyer().getSecurityDeposit().longValue(),
trade.getSeller().getSecurityDeposit().longValue());
// send deposit response to maker and taker
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.taskrunner.TaskRunner;
import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade;
@ -31,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -91,10 +93,15 @@ public class MaybeSendSignContractRequest extends TradeTask {
processModel.setDepositTxXmr(depositTx); // TODO: redundant with trade.getSelf().setDepositTx(), remove?
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
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().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
byte[] sig = null;
if (trade instanceof MakerTrade) {

View File

@ -18,6 +18,8 @@
package haveno.core.trade.protocol.tasks;
import java.math.BigInteger;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.DepositResponse;
@ -43,10 +45,17 @@ public class ProcessDepositResponse extends TradeTask {
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
trade.setStateIfValidTransitionTo(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS);
trade.addInitProgressStep();
processModel.getTradeManager().requestPersistence();
// update balances
trade.getXmrWalletService().updateBalanceListeners();
complete();
} catch (Throwable t) {
failed(t);

View File

@ -53,6 +53,10 @@ public class ProcessInitTradeRequest extends TradeTask {
checkNotNull(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
TradePeer multisigParticipant;
if (trade instanceof ArbitratorTrade) {

View File

@ -17,28 +17,24 @@
package haveno.core.xmr;
import haveno.core.offer.OfferPayload;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.failed.FailedTradesManager;
import haveno.core.xmr.listeners.XmrBalanceListener;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.P2PService;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet;
import javax.inject.Inject;
import java.math.BigInteger;
@ -77,19 +73,19 @@ public class Balances {
}
public void onAllServicesInitialized() {
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> updatedBalances());
tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updatedBalances());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> updatedBalances());
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> updateBalances());
tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updateBalances());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> updateBalances());
xmrWalletService.addBalanceListener(new XmrBalanceListener() {
@Override
public void onBalanceChanged(BigInteger balance) {
updatedBalances();
updateBalances();
}
});
updatedBalances();
updateBalances();
}
private void updatedBalances() {
private void updateBalances() {
if (!xmrWalletService.isWalletReady()) return;
try {
updateAvailableBalance();
@ -111,7 +107,18 @@ public class Balances {
private void updatePendingBalance() {
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.valueOf(0) : xmrWalletService.getWallet().getBalance(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() {
@ -120,30 +127,21 @@ public class Balances {
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
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);
}
private void updateReservedTradeBalance() {
BigInteger sum = BigInteger.valueOf(0);
List<Trade> openTrades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
for (Trade trade : openTrades) {
try {
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));
List<Trade> trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
for (Trade trade : trades) {
sum = sum.add(trade.getReservedAmount());
}
reservedTradeBalance.set(sum);
}

View File

@ -368,7 +368,7 @@ public class XmrWalletService {
return reserveTx;
}
/**s
/**
* Create the multisig deposit tx and freeze its inputs.
*
* @param trade the trade to create a deposit tx from
@ -388,8 +388,8 @@ public class XmrWalletService {
Offer offer = trade.getProcessModel().getOffer();
String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.valueOf(0) : offer.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.valueOf(0) : trade.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); // TODO: security deposit should be based on trade amount
long time = System.currentTimeMillis();
log.info("Creating deposit tx with multisig address={}", multisigAddress);
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.
* 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 sendAmount amount to give peer
* @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) {
// create task to change main wallet password
@ -1195,6 +1177,25 @@ public class XmrWalletService {
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() {
xmrAddressEntryList.requestPersistence();
}
@ -1259,7 +1260,7 @@ public class XmrWalletService {
@Override
public void run() {
for (MoneroWalletListenerI listener : walletListeners) listener.onBalancesChanged(newBalance, newUnlockedBalance);
notifyBalanceListeners();
updateBalanceListeners();
}
});
}

View File

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

View File

@ -216,6 +216,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setWinner(peersDisputeResult.getWinner());
disputeResult.setReason(peersDisputeResult.getReason());
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
disputeResult.setSubtractFeeFrom(peersDisputeResult.getSubtractFeeFrom());
buyerGetsTradeAmountRadioButton.setDisable(true);
buyerGetsAllRadioButton.setDisable(true);
@ -403,7 +404,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setBuyerPayoutAmount(buyerAmount);
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.SELLER);
}

View File

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

View File

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