subtract mining fees from destinations in trade protocol

fixes to scheduling and the deposit view
display address usage context
fix npe when price is null
This commit is contained in:
woodser 2023-07-25 08:21:59 -04:00
parent 13d87a32a5
commit 242bc0e3bb
25 changed files with 273 additions and 280 deletions

View File

@ -237,6 +237,9 @@ public class CoreDisputesService {
throw new RuntimeException("Winner payout is more than the trade wallet's balance"); throw new RuntimeException("Winner payout is more than the trade wallet's balance");
} }
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) {
throw new RuntimeException("Loser payout cannot be negative");
}
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));
} }

View File

@ -248,8 +248,9 @@ public class CoreOffersService {
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!seenKeyImages.add(keyImage)) { if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) { for (Offer offer2 : offers) {
if (offer == offer2) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, removing offer {}", keyImage, offer2.getId()); log.warn("Key image {} belongs to multiple offers, seen in offer {}", keyImage, offer2.getId());
duplicateFundedOffers.add(offer2); duplicateFundedOffers.add(offer2);
} }
} }

View File

@ -214,7 +214,7 @@ public class WalletAppSetup {
if (rejectedTxErrorMessageHandler != null) { if (rejectedTxErrorMessageHandler != null) {
rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.openOffer.makerFeeTxRejected", openOffer.getId(), txId)); rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.openOffer.makerFeeTxRejected", openOffer.getId(), txId));
} }
openOfferManager.removeOpenOffer(openOffer, () -> { openOfferManager.cancelOpenOffer(openOffer, () -> {
log.warn("We removed an open offer because the maker fee was rejected by the Bitcoin " + log.warn("We removed an open offer because the maker fee was rejected by the Bitcoin " +
"network. OfferId={}, txId={}", openOffer.getShortId(), txId); "network. OfferId={}, txId={}", openOffer.getShortId(), txId);
}, log::warn); }, log::warn);

View File

@ -203,6 +203,10 @@ public final class OpenOffer implements Tradable {
return state == State.SCHEDULED; return state == State.SCHEDULED;
} }
public boolean isAvailable() {
return state == State.AVAILABLE;
}
public boolean isDeactivated() { public boolean isDeactivated() {
return state == State.DEACTIVATED; return state == State.DEACTIVATED;
} }

View File

@ -350,7 +350,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
int size = openOffers.size(); int size = openOffers.size();
// Copy list as we remove in the loop // Copy list as we remove in the loop
List<OpenOffer> openOffersList = new ArrayList<>(openOffers); List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
openOffersList.forEach(openOffer -> removeOpenOffer(openOffer, () -> { openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> {
}, errorMessage -> { }, errorMessage -> {
log.warn("Error removing open offer: " + errorMessage); log.warn("Error removing open offer: " + errorMessage);
})); }));
@ -505,7 +505,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
}, (errorMessage) -> { }, (errorMessage) -> {
log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage); log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage);
onRemoved(openOffer); onCancelled(openOffer);
offer.setErrorMessage(errorMessage); offer.setErrorMessage(errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage);
}); });
@ -515,7 +515,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId()); Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
if (openOfferOptional.isPresent()) { if (openOfferOptional.isPresent()) {
removeOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
} else { } else {
log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook."); log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook.");
errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook."); errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook.");
@ -561,15 +561,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
public void removeOpenOffer(OpenOffer openOffer, public void cancelOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
if (!offersToBeEdited.containsKey(openOffer.getId())) { if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isDeactivated()) { if (openOffer.isDeactivated()) {
onRemoved(openOffer); onCancelled(openOffer);
} else { } else {
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> onRemoved(openOffer), () -> onCancelled(openOffer),
errorMessageHandler); errorMessageHandler);
} }
} else { } else {
@ -647,7 +647,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
// remove open offer which thaws its key images // remove open offer which thaws its key images
private void onRemoved(@NotNull OpenOffer openOffer) { private void onCancelled(@NotNull OpenOffer openOffer) {
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
if (offer.getOfferPayload().getReserveTxKeyImages() != null) { if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
@ -667,7 +667,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
getOpenOfferById(offer.getId()).ifPresent(openOffer -> { getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
removeOpenOffer(openOffer); removeOpenOffer(openOffer);
openOffer.setState(OpenOffer.State.CLOSED); openOffer.setState(OpenOffer.State.CLOSED);
xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); xmrWalletService.resetOfferFundingForOpenOffer(offer.getId());
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> log.info("Successfully removed offer {}", offer.getId()), () -> log.info("Successfully removed offer {}", offer.getId()),
log::error); log::error);
@ -780,7 +780,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
latch.countDown(); latch.countDown();
}, errorMessage -> { }, errorMessage -> {
log.warn("Error processing unposted offer {}: {}", scheduledOffer.getId(), errorMessage); log.warn("Error processing unposted offer {}: {}", scheduledOffer.getId(), errorMessage);
onRemoved(scheduledOffer); onCancelled(scheduledOffer);
errorMessages.add(errorMessage); errorMessages.add(errorMessage);
latch.countDown(); latch.countDown();
}); });
@ -808,8 +808,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle split output offer // handle split output offer
if (openOffer.isSplitOutput()) { if (openOffer.isSplitOutput()) {
// get tx to fund split output // find tx with exact input amount
MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer); MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer);
if (openOffer.getScheduledTxHashes() == null && splitOutputTx != null) { if (openOffer.getScheduledTxHashes() == null && splitOutputTx != null) {
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
@ -818,27 +818,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setState(OpenOffer.State.SCHEDULED); openOffer.setState(OpenOffer.State.SCHEDULED);
} }
// handle split output available // if not found, create tx to split exact output
if (splitOutputTx != null && !splitOutputTx.isLocked()) { if (splitOutputTx == null) {
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); splitOrSchedule(openOffers, openOffer, offerReserveAmount);
} else if (!splitOutputTx.isLocked()) {
// otherwise sign and post offer if split output available
signAndPostOffer(openOffer, true, resultHandler, (errMsg) -> {
// on error, create new tx to split output if offer subaddress does not have exact output
int offerSubaddress = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getSubaddressIndex();
if (!splitOutputTx.getOutgoingTransfer().getSubaddressIndices().equals(Arrays.asList(offerSubaddress))) {
log.warn("Splitting new output because spending existing output(s) failed for offer {}", openOffer.getId());
splitOrSchedule(openOffers, openOffer, offerReserveAmount);
resultHandler.handleResult(null);
} else {
errorMessageHandler.handleErrorMessage(errMsg);
}
});
return; return;
} else if (splitOutputTx == null) {
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
if (sufficientAvailableBalance) {
// create and relay tx to split output
splitOutputTx = createAndRelaySplitOutputTx(openOffer); // TODO: confirm with user?
// schedule txs
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
openOffer.setScheduledAmount(offerReserveAmount.toString());
openOffer.setState(OpenOffer.State.SCHEDULED);
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleOfferWithEarliestTxs(openOffers, openOffer);
}
} }
} else { } else {
@ -862,44 +860,65 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}).start(); }).start();
} }
private void splitOrSchedule(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) {
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
if (sufficientAvailableBalance) {
// create and relay tx to split output
MoneroTxWallet splitOutputTx = createAndRelaySplitOutputTx(openOffer);
// schedule txs
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
openOffer.setScheduledAmount(offerReserveAmount.toString());
openOffer.setState(OpenOffer.State.SCHEDULED);
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleOfferWithEarliestTxs(openOffers, openOffer);
}
}
public boolean hasAvailableOutput(BigInteger amount) { public boolean hasAvailableOutput(BigInteger amount) {
return findSplitOutputFundingTx(getOpenOffers(), amount, null) != null; return findSplitOutputFundingTx(getOpenOffers(), null, amount, null) != null;
} }
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) { private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) {
// return split output tx if already assigned
if (openOffer.getSplitOutputTxHash() != null) {
return xmrWalletService.getWallet().getTx(openOffer.getSplitOutputTxHash());
}
// find tx with exact output
XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
return findSplitOutputFundingTx(openOffers, openOffer.getOffer().getReserveAmount(), addressEntry.getSubaddressIndex()); return findSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getReserveAmount(), addressEntry.getSubaddressIndex());
} }
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, BigInteger reserveAmount, Integer subaddressIndex) { private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> fundingTxs = new ArrayList<>(); List<MoneroTxWallet> fundingTxs = new ArrayList<>();
MoneroTxWallet earliestUnscheduledTx = null; MoneroTxWallet earliestUnscheduledTx = null;
if (subaddressIndex != null) {
// return earliest tx with exact confirmed output to fund offer's subaddress if available // return earliest tx with exact confirmed output to given subaddress if available
if (preferredSubaddressIndex != null) {
// get txs with exact output amount
fundingTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery() fundingTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
.setIsConfirmed(true) .setIsConfirmed(true)
.setOutputQuery(new MoneroOutputQuery() .setOutputQuery(new MoneroOutputQuery()
.setAccountIndex(0) .setAccountIndex(0)
.setSubaddressIndex(subaddressIndex) .setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount) .setAmount(reserveAmount)
.setIsSpent(false) .setIsSpent(false)
.setIsFrozen(false))); .setIsFrozen(false)));
// return earliest tx if available
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx; if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
} }
// return split output tx if already assigned
if (openOffer != null && openOffer.getSplitOutputTxHash() != null) {
return xmrWalletService.getWallet().getTx(openOffer.getSplitOutputTxHash());
}
// cache all transactions including from pool // cache all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true)); List<MoneroTxWallet> allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true));
if (subaddressIndex != null) { if (preferredSubaddressIndex != null) {
// return earliest tx with exact incoming transfer to fund offer's subaddress if available (since outputs are not available until confirmed) // return earliest tx with exact incoming transfer to fund offer's subaddress if available (since outputs are not available until confirmed)
fundingTxs.clear(); fundingTxs.clear();
@ -907,7 +926,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery() boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery()
.setIsIncoming(true) .setIsIncoming(true)
.setAccountIndex(0) .setAccountIndex(0)
.setSubaddressIndex(subaddressIndex) .setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount)).size() > 0; .setAmount(reserveAmount)).size() > 0;
if (hasExactTransfer) fundingTxs.add(tx); if (hasExactTransfer) fundingTxs.add(tx);
} }
@ -982,13 +1001,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) { private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) {
BigInteger reserveAmount = openOffer.getOffer().getReserveAmount(); BigInteger reserveAmount = openOffer.getOffer().getReserveAmount();
xmrWalletService.swapTradeEntryToAvailableEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output // TODO: unecessary with destination funding xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
String fundingSubaddress = xmrWalletService.getNewAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getAddressString(); String fundingSubaddress = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getAddressString();
return xmrWalletService.getWallet().createTx(new MoneroTxConfig() log.info("Creating split output tx to fund offer {}", openOffer.getId());
MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.setAddress(fundingSubaddress) .setAddress(fundingSubaddress)
.setAmount(reserveAmount) .setAmount(reserveAmount)
.setRelay(true)); .setRelay(true));
log.info("Done creating split output tx to fund offer {}", openOffer.getId());
return splitOutputTx;
} }
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) { private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {

View File

@ -54,10 +54,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger exactOutputAmount = model.getOpenOffer().isSplitOutput() ? model.getOpenOffer().getOffer().getReserveAmount() : null;
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null; Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, model.getOpenOffer().isSplitOutput(), preferredSubaddressIndex);
// check for error in case creating reserve tx exceeded timeout // check for error in case creating reserve tx exceeded timeout
// TODO: better way? // TODO: better way?

View File

@ -61,7 +61,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -853,38 +852,24 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Winner payout cannot be negative"); if (winnerPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Winner payout cannot be negative");
if (loserPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Loser payout cannot be negative"); if (loserPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Loser payout cannot be negative");
if (winnerPayoutAmount.add(loserPayoutAmount).compareTo(trade.getWallet().getUnlockedBalance()) > 0) { if (winnerPayoutAmount.add(loserPayoutAmount).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance"); throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + winnerPayoutAmount + " + " + loserPayoutAmount + " = " + (winnerPayoutAmount.add(loserPayoutAmount)));
} }
// add any loss of precision to winner payout // 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 transaction to get fee estimate // create dispute payout tx
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount);
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount);
MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig); txConfig.setSubtractFeeFrom(loserPayoutAmount.equals(BigInteger.ZERO) ? 0 : txConfig.getDestinations().size() - 1); // winner only pays fee if loser gets 0
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null; MoneroTxWallet payoutTx = null;
int numAttempts = 0; try {
while (payoutTx == null && numAttempts < 50) { payoutTx = trade.getWallet().createTx(txConfig);
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful } catch (Exception e) {
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); e.printStackTrace();
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++;
try {
payoutTx = trade.getWallet().createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
}
} }
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
log.info("Dispute payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex // save updated multisig hex
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());

View File

@ -388,9 +388,6 @@ 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();
// add any loss of precision to winner amount
expectedWinnerAmount = expectedWinnerAmount.add(trade.getWallet().getUnlockedBalance().subtract(expectedWinnerAmount.add(expectedLoserAmount)));
// winner pays cost if loser gets nothing, otherwise loser pays cost // winner pays cost if loser gets nothing, otherwise loser pays cost
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost);
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); else expectedLoserAmount = expectedLoserAmount.subtract(txCost);

View File

@ -201,7 +201,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
} }
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
// set state after payout as we call swapTradeEntryToAvailableEntry // set state after payout as we call swapAddressEntryToAvailable
if (tradeManager.getOpenTrade(tradeId).isPresent()) { if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
} else { } else {

View File

@ -630,7 +630,7 @@ public abstract class Trade implements Tradable, Model {
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this);
// reset address entries // reset address entries
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId()); processModel.getXmrWalletService().resetAddressEntriesForTrade(getId());
} }
// cleanup when payout unlocks // cleanup when payout unlocks
@ -922,32 +922,13 @@ public abstract class Trade implements Tradable, Model {
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create transaction to get fee estimate // create payout tx
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig() MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx .addDestination(buyerPayoutAddress, buyerPayoutAmount)
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) .addDestination(sellerPayoutAddress, sellerPayoutAmount)
.setRelay(false) .setSubtractFeeFrom(0, 1) // split tx fee
); .setRelay(false));
// attempt to create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful
try {
numAttempts++;
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount
.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))
.setRelay(false));
} catch (MoneroError e) {
// exception expected
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
log.info("Payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex // save updated multisig hex
getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());

View File

@ -356,6 +356,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
reservedKeyImages.addAll(trade.getSelf().getReserveTxKeyImages()); reservedKeyImages.addAll(trade.getSelf().getReserveTxKeyImages());
} }
for (OpenOffer openOffer : openOfferManager.getObservableList()) { for (OpenOffer openOffer : openOfferManager.getObservableList()) {
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue;
reservedKeyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); reservedKeyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
} }
@ -473,7 +474,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
.filter(addressEntry -> addressEntry.getOfferId() != null) .filter(addressEntry -> addressEntry.getOfferId() != null)
.forEach(addressEntry -> { .forEach(addressEntry -> {
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext()); xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext());
}); });
onTradesInitiailizedAndAppFullyInitialized(); onTradesInitiailizedAndAppFullyInitialized();
@ -946,7 +947,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
removeTrade(trade); removeTrade(trade);
// TODO The address entry should have been removed already. Check and if its the case remove that. // TODO The address entry should have been removed already. Check and if its the case remove that.
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId()); xmrWalletService.resetAddressEntriesForTrade(trade.getId());
requestPersistence(); requestPersistence();
} }
@ -961,7 +962,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
trade.setDisputeState(disputeState); trade.setDisputeState(disputeState);
onTradeCompleted(trade); onTradeCompleted(trade);
xmrWalletService.swapTradeEntryToAvailableEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); xmrWalletService.resetAddressEntriesForTrade(trade.getId());
requestPersistence(); requestPersistence();
} }
} }

View File

@ -22,6 +22,7 @@ import common.utils.JsonUtils;
import haveno.common.app.Version; import haveno.common.app.Version;
import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.common.util.Tuple2;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
@ -33,6 +34,7 @@ import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
@ -83,8 +85,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
String depositAddress = processModel.getMultisigAddress(); String depositAddress = processModel.getMultisigAddress();
// verify deposit tx // verify deposit tx
Tuple2<MoneroTx, BigInteger> txResult;
try { try {
trade.getXmrWalletService().verifyTradeTx( txResult = trade.getXmrWalletService().verifyTradeTx(
offer.getId(), offer.getId(),
tradeFee, tradeFee,
sendAmount, sendAmount,
@ -100,6 +103,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
} }
// set deposit info // set deposit info
trader.setSecurityDeposit(txResult.second);
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());

View File

@ -31,7 +31,6 @@ 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;
@ -77,15 +76,12 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs // create deposit tx and freeze inputs
Integer subaddressIndex = null; Integer subaddressIndex = null;
BigInteger exactOutputAmount = null; boolean isSplitOutputOffer = false;
if (trade instanceof MakerTrade) { if (trade instanceof MakerTrade) {
boolean isSplitOutputOffer = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isSplitOutput(); isSplitOutputOffer = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isSplitOutput();
if (isSplitOutputOffer) { if (isSplitOutputOffer) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
exactOutputAmount = trade.getOffer().getReserveAmount();
subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
}
} }
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, exactOutputAmount, subaddressIndex); MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, isSplitOutputOffer, subaddressIndex);
// collect reserved key images // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();

View File

@ -38,7 +38,7 @@ public class RemoveOffer extends TradeTask {
if (trade instanceof MakerTrade) { if (trade instanceof MakerTrade) {
processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(trade.getOffer())); processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(trade.getOffer()));
} else { } else {
trade.getXmrWalletService().resetAddressEntriesForOpenOffer(trade.getId()); trade.getXmrWalletService().resetOfferFundingForOpenOffer(trade.getId());
} }
complete(); complete();

View File

@ -44,7 +44,7 @@ public class SellerPublishDepositTx extends TradeTask {
// //
// trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX); // trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX);
// //
// processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(processModel.getOffer().getId(), // processModel.getBtcWalletService().swapAddressEntryToAvailable(processModel.getOffer().getId(),
// AddressEntry.Context.RESERVED_FOR_TRADE); // AddressEntry.Context.RESERVED_FOR_TRADE);
// //
// processModel.getTradeManager().requestPersistence(); // processModel.getTradeManager().requestPersistence();

View File

@ -44,7 +44,7 @@ public class TakerReserveTradeFunds extends TradeTask {
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0); BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit(); BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress, null, null); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress, false, null);
// collect reserved key images // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();

View File

@ -131,7 +131,7 @@ public class VolumeUtil {
} }
public static String formatVolume(Volume volume) { public static String formatVolume(Volume volume) {
return formatVolume(volume, getMonetaryFormat(volume.getCurrencyCode()), false); return volume == null ? "" : formatVolume(volume, getMonetaryFormat(volume.getCurrencyCode()), false);
} }
private static String formatVolume(Volume volume, MonetaryFormat volumeFormat, boolean appendCurrencyCode) { private static String formatVolume(Volume volume, MonetaryFormat volumeFormat, boolean appendCurrencyCode) {

View File

@ -289,9 +289,9 @@ public class BtcWalletService extends WalletService {
return addressEntryList.getAddressEntriesAsListImmutable(); return addressEntryList.getAddressEntriesAsListImmutable();
} }
public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) { public void swapAddressEntryToAvailable(String offerId, AddressEntry.Context context) {
if (context == AddressEntry.Context.MULTI_SIG) { if (context == AddressEntry.Context.MULTI_SIG) {
log.error("swapTradeEntryToAvailableEntry called with MULTI_SIG context. " + log.error("swapAddressEntryToAvailable called with MULTI_SIG context. " +
"This in not permitted as we must not reuse those address entries and there " + "This in not permitted as we must not reuse those address entries and there " +
"are no redeemable funds on that addresses. Only the keys are used for creating " + "are no redeemable funds on that addresses. Only the keys are used for creating " +
"the Multisig address. offerId={}, context={}", offerId, context); "the Multisig address. offerId={}, context={}", offerId, context);
@ -327,8 +327,8 @@ public class BtcWalletService extends WalletService {
public void resetAddressEntriesForOpenOffer(String offerId) { public void resetAddressEntriesForOpenOffer(String offerId) {
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING); swapAddressEntryToAvailable(offerId, AddressEntry.Context.OFFER_FUNDING);
swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE); swapAddressEntryToAvailable(offerId, AddressEntry.Context.RESERVED_FOR_TRADE);
} }
public void resetAddressEntriesForPendingTrade(String offerId) { public void resetAddressEntriesForPendingTrade(String offerId) {
@ -342,7 +342,7 @@ public class BtcWalletService extends WalletService {
// send out the funds to the external wallet. As this cleanup is a rare situation and most users do not use // send out the funds to the external wallet. As this cleanup is a rare situation and most users do not use
// the feature to send out the funds we prefer that strategy (if we keep the address entry it might cause // the feature to send out the funds we prefer that strategy (if we keep the address entry it might cause
// complications in some edge cases after a SPV resync). // complications in some edge cases after a SPV resync).
swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.TRADE_PAYOUT); swapAddressEntryToAvailable(offerId, AddressEntry.Context.TRADE_PAYOUT);
} }
public void swapAnyTradeEntryContextToAvailableEntry(String offerId) { public void swapAnyTradeEntryContextToAvailableEntry(String offerId) {

View File

@ -57,17 +57,18 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -93,8 +94,6 @@ public class XmrWalletService {
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_WALLET_NAME = "haveno_XMR";
public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee
private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent
private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee
private static final int NUM_MAX_BACKUP_WALLETS = 10; private static final int NUM_MAX_BACKUP_WALLETS = 10;
private static final int MONERO_LOG_LEVEL = 0; private static final int MONERO_LOG_LEVEL = 0;
private static final boolean PRINT_STACK_TRACE = false; private static final boolean PRINT_STACK_TRACE = false;
@ -322,50 +321,55 @@ public class XmrWalletService {
} }
} }
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
// fetch unspent, unfrozen, unlocked outputs
List<MoneroOutputWallet> exactOutputs = wallet.getOutputs(new MoneroOutputQuery()
.setAmount(amount)
.setIsSpent(false)
.setIsFrozen(false)
.setTxQuery(new MoneroTxQuery().setIsLocked(false)));
// collect subaddresses indices as sorted set
TreeSet<Integer> subaddressIndices = new TreeSet<Integer>();
for (MoneroOutputWallet output : exactOutputs) subaddressIndices.add(output.getSubaddressIndex());
return new ArrayList<Integer>(subaddressIndices);
}
/** /**
* Create the reserve tx and freeze its inputs. The full amount is returned * Create the reserve tx and freeze its inputs. The full amount is returned
* to the sender's payout address less the trade fee. * to the sender's payout address less the security deposit and mining fee.
* *
* @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
* @param returnAddress return address for reserved funds * @param returnAddress return address for reserved funds
* @param exactOutputAmount exact output amount to spend (optional) * @param reserveExactAmount specifies to reserve the exact input amount
* @param subaddressIndex preferred source subaddress to spend from (optional) * @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @return a transaction to reserve a trade * @return a transaction to reserve a trade
*/ */
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, BigInteger exactOutputAmount, Integer subaddressIndex) { public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
log.info("Creating reserve tx with return address={}", returnAddress); log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
try { MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true, reserveExactAmount, preferredSubaddressIndex);
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true, exactOutputAmount, subaddressIndex); log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); return reserveTx;
return reserveTx;
} catch (Exception e) {
if (exactOutputAmount != null) return spendOutputManually(true, tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount);
// retry creating reserve tx using funds outside subaddress
if (subaddressIndex != null) return createReserveTx(tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, null);
else throw e;
}
} }
/** /**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
* @param exactOutputAmount exact output amount to spend (optional) * @param reserveExactAmount specifies to reserve the exact input amount
* @param subaddressIndex preferred source subaddress to spend from (optional) * @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @return MoneroTxWallet the multisig deposit tx * @return MoneroTxWallet the multisig deposit tx
*/ */
public MoneroTxWallet createDepositTx(Trade trade, BigInteger exactOutputAmount, Integer subaddressIndex) { public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
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) : offer.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); BigInteger securityDeposit = trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
// thaw reserved outputs then create deposit tx
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
synchronized (wallet) { synchronized (wallet) {
@ -374,98 +378,76 @@ public class XmrWalletService {
thawOutputs(trade.getSelf().getReserveTxKeyImages()); thawOutputs(trade.getSelf().getReserveTxKeyImages());
} }
log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getId(), multisigAddress); // create deposit tx
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
try { log.info("Creating deposit tx with multisig address={}", multisigAddress);
MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false, exactOutputAmount, subaddressIndex); MoneroTxWallet depositTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time); log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time);
return tradeTx; return depositTx;
} catch (Exception e) {
if (exactOutputAmount != null) return spendOutputManually(false, tradeFee, sendAmount, securityDeposit, multisigAddress, exactOutputAmount);
// retry creating deposit tx using funds outside subaddress
if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null);
else throw e;
}
} }
} }
// retry with exact outputs in other subaddresses private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
// TODO: this is a hack because wallet2 sometimes prefers to spend multiple inputs intead of exact output; replace with fund by destination address when available
private MoneroTxWallet spendOutputManually(boolean isReserveTx, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, BigInteger exactOutputAmount) {
log.warn("Manually selecting subaddress to spend output from");
List<MoneroOutputWallet> exactOutputs = wallet.getOutputs(new MoneroOutputQuery()
.setAmount(exactOutputAmount)
.setIsSpent(false)
.setIsFrozen(false));
Set<Integer> subaddressIndices = new HashSet<Integer>();
for (MoneroOutputWallet output : exactOutputs) {
if (!output.getTx().isLocked()) subaddressIndices.add(output.getSubaddressIndex());
}
Exception err = null;
for (Integer idx : subaddressIndices) {
try {
long startTime = System.currentTimeMillis();
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, isReserveTx, exactOutputAmount, idx);
log.info("Done creating output tx in {} ms", System.currentTimeMillis() - startTime);
return reserveTx;
} catch (Exception e2) {
err = e2;
}
}
if (err != null) throw new RuntimeException(err);
throw new RuntimeException("No output available with amount " + exactOutputAmount);
}
private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, BigInteger exactOutputAmount, Integer subaddressIndex) {
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
synchronized (wallet) { synchronized (wallet) {
// binary search to maximize security deposit and minimize potential dust // create a list of subaddresses to attempt spending from in preferred order
// TODO: binary search is hacky and slow over TOR connections, replace with destination paying tx fee List<Integer> subaddressIndices = new ArrayList<Integer>();
MoneroTxWallet tradeTx = null; if (reserveExactAmount) {
double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit BigInteger exactInputAmount = tradeFee.add(sendAmount).add(securityDeposit);
double searchDiff = 1.0; // difference for next binary search List<Integer> subaddressIndicesWithExactInput = getSubaddressesWithExactInput(exactInputAmount);
int maxSearches = 5; if (preferredSubaddressIndex != null) subaddressIndicesWithExactInput.remove(preferredSubaddressIndex);
for (int i = 0; i < maxSearches; i++) { Collections.sort(subaddressIndicesWithExactInput);
try { Collections.reverse(subaddressIndicesWithExactInput);
BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger(); subaddressIndices.addAll(subaddressIndicesWithExactInput);
BigInteger amount = sendAmount.add(isReserveTx ? tradeFee : appliedSecurityDeposit); }
MoneroTxWallet testTx = wallet.createTx(new MoneroTxConfig() if (preferredSubaddressIndex != null) {
.setAccountIndex(0) if (wallet.getBalance(0, preferredSubaddressIndex).compareTo(BigInteger.valueOf(0)) > 0) {
.setSubaddressIndices(subaddressIndex == null ? null : Arrays.asList(subaddressIndex)) // TODO monero-java: MoneroTxConfig.setSubadddressIndex(int) causes NPE with null subaddress, could setSubaddressIndices(null) as convenience subaddressIndices.add(0, preferredSubaddressIndex); // try preferred subaddress first if funded
.addDestination(HavenoUtils.getTradeFeeAddress(), isReserveTx ? appliedSecurityDeposit : tradeFee) // reserve tx charges security deposit if published } else if (reserveExactAmount) {
.addDestination(address, amount)); subaddressIndices.add(preferredSubaddressIndex); // otherwise only try preferred subaddress if using exact output
// assert exact input if expected
if (exactOutputAmount == null) {
tradeTx = testTx;
} else {
BigInteger inputSum = BigInteger.valueOf(0);
for (MoneroOutputWallet txInput : testTx.getInputsWallet()) {
MoneroOutputWallet input = wallet.getOutputs(new MoneroOutputQuery().setKeyImage(txInput.getKeyImage())).get(0);
inputSum = inputSum.add(input.getAmount());
}
if (inputSum.compareTo(exactOutputAmount) > 0) throw new RuntimeException("Spending too much since input sum is greater than output amount"); // continues binary search with less security deposit
else if (inputSum.equals(exactOutputAmount) && testTx.getInputs().size() == 1) tradeTx = testTx;
}
appliedTolerance -= searchDiff; // apply less tolerance to increase security deposit
if (appliedTolerance < 0.0) break; // can send full security deposit
} catch (Exception e) {
appliedTolerance += searchDiff; // apply more tolerance to decrease security deposit
if (appliedTolerance > 1.0) {
if (tradeTx == null) throw e;
break;
}
} }
searchDiff /= 2; }
// first try preferred subaddressess
for (int i = 0; i < subaddressIndices.size(); i++) {
try {
return createTradeTxFromSubaddress(tradeFee, sendAmount, securityDeposit, address, isReserveTx, reserveExactAmount, subaddressIndices.get(i));
} catch (Exception e) {
if (i == subaddressIndices.size() - 1 && reserveExactAmount) throw e; // throw if no subaddress with exact output
}
}
// try any subaddress
return createTradeTxFromSubaddress(tradeFee, sendAmount, securityDeposit, address, isReserveTx, reserveExactAmount, null);
}
}
private MoneroTxWallet createTradeTxFromSubaddress(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, boolean reserveExactAmount, Integer subaddressIndex) {
// create tx
MoneroTxWallet tradeTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setSubaddressIndices(subaddressIndex)
.addDestination(HavenoUtils.getTradeFeeAddress(), isReserveTx ? securityDeposit : tradeFee) // reserve tx charges security deposit if published
.addDestination(address, sendAmount.add(isReserveTx ? tradeFee : securityDeposit))
.setSubtractFeeFrom(isReserveTx ? 0 : 1)); // pay fee from same destination as security deposit
// check if tx uses exact input, since wallet2 can prefer to spend 2 outputs
if (reserveExactAmount) {
BigInteger exactInputAmount = tradeFee.add(sendAmount).add(securityDeposit);
BigInteger inputSum = BigInteger.valueOf(0);
for (MoneroOutputWallet txInput : tradeTx.getInputsWallet()) {
MoneroOutputWallet input = wallet.getOutputs(new MoneroOutputQuery().setKeyImage(txInput.getKeyImage())).get(0);
inputSum = inputSum.add(input.getAmount());
}
if (inputSum.compareTo(exactInputAmount) > 0) throw new RuntimeException("Cannot create transaction with exact input amount");
} }
// freeze inputs // freeze inputs
for (MoneroOutput input : tradeTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); for (MoneroOutput input : tradeTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
saveMainWallet(); saveMainWallet();
return tradeTx; return tradeTx;
}
} }
/** /**
@ -533,19 +515,20 @@ public class XmrWalletService {
BigInteger actualSendAmount = transferCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit); BigInteger actualSendAmount = transferCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
// verify trade fee // verify trade fee
if (!tradeFee.equals(actualTradeFee)) { if (actualTradeFee.compareTo(tradeFee) < 0) {
throw new RuntimeException("Trade fee is incorrect amount, expected=" + tradeFee + ", actual=" + actualTradeFee + ", transfer address check=" + JsonUtils.serialize(transferCheck) + ", trade fee address check=" + JsonUtils.serialize(tradeFeeCheck)); throw new RuntimeException("Insufficient trade fee, expected=" + tradeFee + ", actual=" + actualTradeFee + ", transfer address check=" + JsonUtils.serialize(transferCheck) + ", trade fee address check=" + JsonUtils.serialize(tradeFeeCheck));
} }
// verify sufficient security deposit // verify send amount
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger(); if (!actualSendAmount.equals(sendAmount)) {
if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit); throw new RuntimeException("Unexpected send amount, expected " + sendAmount + " but was " + actualSendAmount);
}
// verify deposit amount + miner fee within dust tolerance // verify security deposit
//BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); // TODO: improve when destination pays fee BigInteger expectedSecurityDeposit = securityDeposit.subtract(tx.getFee()); // fee is paid from security deposit
BigInteger minDeposit = sendAmount.add(minSecurityDeposit); if (!actualSecurityDeposit.equals(expectedSecurityDeposit)) {
BigInteger actualDeposit = actualSendAmount.add(actualSecurityDeposit); throw new RuntimeException("Unexpected security deposit amount, expected " + expectedSecurityDeposit + " but was " + actualSecurityDeposit);
if (actualDeposit.compareTo(minDeposit) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDeposit + " but was " + actualDeposit); }
} catch (Exception e) { } catch (Exception e) {
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage()); log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage());
throw e; throw e;
@ -926,8 +909,8 @@ public class XmrWalletService {
// try to use available and not yet used entries // try to use available and not yet used entries
try { try {
List<MoneroTxWallet> incomingTxs = getTxsWithIncomingOutputs(); // prefetch all incoming txs to avoid query per subaddress List<MoneroTxWallet> incomingTxs = getTxsWithIncomingOutputs(); // prefetch all incoming txs to avoid query per subaddress
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny(); List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries(incomingTxs);
if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error getting new address entry based on incoming transactions"); log.warn("Error getting new address entry based on incoming transactions");
e.printStackTrace(); e.printStackTrace();
@ -983,7 +966,7 @@ public class XmrWalletService {
return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0)); return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0));
} }
public synchronized void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) { public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) {
Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
addressEntryOptional.ifPresent(e -> { addressEntryOptional.ifPresent(e -> {
log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context);
@ -994,22 +977,17 @@ public class XmrWalletService {
public synchronized void resetAddressEntriesForOpenOffer(String offerId) { public synchronized void resetAddressEntriesForOpenOffer(String offerId) {
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
} }
public synchronized void resetAddressEntriesForPendingTrade(String offerId) { public synchronized void resetOfferFundingForOpenOffer(String offerId) {
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases log.info("resetOfferFundingForOpenOffer offerId={}", offerId);
// where a user cannot send the funds swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
// to an external wallet directly in the last step of the trade, but the funds }
// are in the Haveno wallet anyway and
// the dealing with the external wallet is pure UI thing. The user can move the public synchronized void resetAddressEntriesForTrade(String offerId) {
// funds to the wallet and then swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
// send out the funds to the external wallet. As this cleanup is a rare
// situation and most users do not use
// the feature to send out the funds we prefer that strategy (if we keep the
// address entry it might cause
// complications in some edge cases after a SPV resync).
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
} }
private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) { private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) {
@ -1063,32 +1041,36 @@ public class XmrWalletService {
public List<XmrAddressEntry> getUnusedAddressEntries(List<MoneroTxWallet> cachedTxs) { public List<XmrAddressEntry> getUnusedAddressEntries(List<MoneroTxWallet> cachedTxs) {
return getAvailableAddressEntries().stream() return getAvailableAddressEntries().stream()
.filter(e -> isSubaddressUnused(e.getSubaddressIndex(), cachedTxs)) .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex(), cachedTxs))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public boolean isSubaddressUnused(int subaddressIndex) { public boolean subaddressHasIncomingTransfers(int subaddressIndex) {
return isSubaddressUnused(subaddressIndex, null); return subaddressHasIncomingTransfers(subaddressIndex, null);
} }
private boolean isSubaddressUnused(int subaddressIndex, List<MoneroTxWallet> incomingTxs) { private boolean subaddressHasIncomingTransfers(int subaddressIndex, List<MoneroTxWallet> incomingTxs) {
return getNumOutputsForSubaddress(subaddressIndex, incomingTxs) == 0; return getNumOutputsForSubaddress(subaddressIndex, incomingTxs) > 0;
}
public int getNumOutputsForSubaddress(int subaddressIndex) {
return getNumOutputsForSubaddress(subaddressIndex, null);
} }
public int getNumOutputsForSubaddress(int subaddressIndex, List<MoneroTxWallet> incomingTxs) { public int getNumOutputsForSubaddress(int subaddressIndex, List<MoneroTxWallet> incomingTxs) {
if (incomingTxs == null) incomingTxs = getTxsWithIncomingOutputs(subaddressIndex); incomingTxs = getTxsWithIncomingOutputs(subaddressIndex, incomingTxs);
int numUnspentOutputs = 0; int numUnspentOutputs = 0;
for (MoneroTxWallet tx : incomingTxs) { for (MoneroTxWallet tx : incomingTxs) {
//if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used //if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used
numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs
} }
boolean positiveBalance = wallet.getBalance(0, subaddressIndex).compareTo(BigInteger.valueOf(0)) > 0;
if (positiveBalance && numUnspentOutputs == 0) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance
return numUnspentOutputs; return numUnspentOutputs;
} }
public int getNumTxsWithIncomingOutputs(int subaddressIndex, List<MoneroTxWallet> txs) {
List<MoneroTxWallet> txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex, txs);
if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex, txsWithIncomingOutputs)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance
return txsWithIncomingOutputs.size();
}
public List<MoneroTxWallet> getTxsWithIncomingOutputs() { public List<MoneroTxWallet> getTxsWithIncomingOutputs() {
return getTxsWithIncomingOutputs(null); return getTxsWithIncomingOutputs(null);
} }

View File

@ -1016,6 +1016,9 @@ funds.tab.transactions=Transactions
funds.deposit.unused=Unused funds.deposit.unused=Unused
funds.deposit.usedInTx=Used in {0} transaction(s) funds.deposit.usedInTx=Used in {0} transaction(s)
funds.deposit.baseAddress=Base address
funds.deposit.offerFunding=Reserved for offer funding
funds.deposit.tradePayout=Reserved for trade payout
funds.deposit.fundHavenoWallet=Fund Haveno wallet funds.deposit.fundHavenoWallet=Fund Haveno wallet
funds.deposit.noAddresses=No deposit addresses have been generated yet funds.deposit.noAddresses=No deposit addresses have been generated yet
funds.deposit.fundWallet=Fund your wallet funds.deposit.fundWallet=Fund your wallet

View File

@ -588,7 +588,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
.width(1000) .width(1000)
.actionButtonText(Res.get("shared.removeOffer")) .actionButtonText(Res.get("shared.removeOffer"))
.onAction(() -> { .onAction(() -> {
openOfferManager.removeOpenOffer(openOffer, () -> { openOfferManager.cancelOpenOffer(openOffer, () -> {
log.info("Invalid open offer with ID {} was successfully removed.", openOffer.getId()); log.info("Invalid open offer with ID {} was successfully removed.", openOffer.getId());
}, log::error); }, log::error);

View File

@ -95,8 +95,23 @@ class DepositListItem {
} }
private void updateUsage(int subaddressIndex, List<MoneroTxWallet> cachedTxs) { private void updateUsage(int subaddressIndex, List<MoneroTxWallet> cachedTxs) {
numTxsWithOutputs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs).size(); numTxsWithOutputs = xmrWalletService.getNumTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs);
usage = subaddressIndex == 0 ? "Base address" : numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs); switch (addressEntry.getContext()) {
case BASE_ADDRESS:
usage = Res.get("funds.deposit.baseAddress");
break;
case AVAILABLE:
usage = numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs);
break;
case OFFER_FUNDING:
usage = Res.get("funds.deposit.offerFunding");
break;
case TRADE_PAYOUT:
usage = Res.get("funds.deposit.tradePayout");
break;
default:
usage = addressEntry.getContext().toString();
}
} }
public void cleanup() { public void cleanup() {

View File

@ -156,7 +156,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(txsWithIncomingOutputs))); confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(txsWithIncomingOutputs)));
usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxsWithOutputs)); usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage));
tableView.getSortOrder().add(usageColumn); tableView.getSortOrder().add(usageColumn);
tableView.setItems(sortedList); tableView.setItems(sortedList);
@ -202,8 +202,8 @@ public class DepositView extends ActivatableView<VBox, Void> {
generateNewAddressButton = buttonCheckBoxHBox.first; generateNewAddressButton = buttonCheckBoxHBox.first;
generateNewAddressButton.setOnAction(event -> { generateNewAddressButton.setOnAction(event -> {
boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getSubaddressIndex() != 0 && xmrWalletService.getTxsWithIncomingOutputs(e.getSubaddressIndex()).isEmpty()); boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty();
if (hasUnUsedAddress) { if (hasUnusedAddress) {
new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); new Popup().warning(Res.get("funds.deposit.selectUnused")).show();
} else { } else {
XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry(); XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry();
@ -311,7 +311,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
// cache incoming txs // cache incoming txs
txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs(); txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs();
// add available address entries and base address // add address entries
xmrWalletService.getAddressEntries() xmrWalletService.getAddressEntries()
.forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs))); .forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs)));
} }

View File

@ -73,7 +73,7 @@ class OpenOffersDataModel extends ActivatableDataModel {
} }
void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
openOfferManager.removeOpenOffer(openOffer, resultHandler, errorMessageHandler); openOfferManager.cancelOpenOffer(openOffer, resultHandler, errorMessageHandler);
} }

View File

@ -121,7 +121,7 @@ public class BuyerStep4View extends TradeStepView {
private void handleTradeCompleted() { private void handleTradeCompleted() {
closeButton.setDisable(true); closeButton.setDisable(true);
model.dataModel.xmrWalletService.swapTradeEntryToAvailableEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); model.dataModel.xmrWalletService.swapAddressEntryToAvailable(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
openTradeFeedbackWindow(); openTradeFeedbackWindow();
} }