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");
}
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) {
throw new RuntimeException("Loser payout cannot be negative");
}
disputeResult.setBuyerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
disputeResult.setSellerPayoutAmount(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount));
}

View File

@ -248,8 +248,9 @@ public class CoreOffersService {
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) {
if (offer == offer2) continue;
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);
}
}

View File

@ -214,7 +214,7 @@ public class WalletAppSetup {
if (rejectedTxErrorMessageHandler != null) {
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 " +
"network. OfferId={}, txId={}", openOffer.getShortId(), txId);
}, log::warn);

View File

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

View File

@ -350,7 +350,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
int size = openOffers.size();
// Copy list as we remove in the loop
List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
openOffersList.forEach(openOffer -> removeOpenOffer(openOffer, () -> {
openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> {
}, errorMessage -> {
log.warn("Error removing open offer: " + errorMessage);
}));
@ -505,7 +505,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
resultHandler.handleResult(transaction);
}, (errorMessage) -> {
log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage);
onRemoved(openOffer);
onCancelled(openOffer);
offer.setErrorMessage(errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
});
@ -515,7 +515,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
if (openOfferOptional.isPresent()) {
removeOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
} else {
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.");
@ -561,15 +561,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
public void removeOpenOffer(OpenOffer openOffer,
public void cancelOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isDeactivated()) {
onRemoved(openOffer);
onCancelled(openOffer);
} else {
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> onRemoved(openOffer),
() -> onCancelled(openOffer),
errorMessageHandler);
}
} else {
@ -647,7 +647,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
// remove open offer which thaws its key images
private void onRemoved(@NotNull OpenOffer openOffer) {
private void onCancelled(@NotNull OpenOffer openOffer) {
Offer offer = openOffer.getOffer();
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
@ -667,7 +667,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
removeOpenOffer(openOffer);
openOffer.setState(OpenOffer.State.CLOSED);
xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId());
xmrWalletService.resetOfferFundingForOpenOffer(offer.getId());
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> log.info("Successfully removed offer {}", offer.getId()),
log::error);
@ -780,7 +780,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
latch.countDown();
}, errorMessage -> {
log.warn("Error processing unposted offer {}: {}", scheduledOffer.getId(), errorMessage);
onRemoved(scheduledOffer);
onCancelled(scheduledOffer);
errorMessages.add(errorMessage);
latch.countDown();
});
@ -809,7 +809,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle split output offer
if (openOffer.isSplitOutput()) {
// get tx to fund split output
// find tx with exact input amount
MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer);
if (openOffer.getScheduledTxHashes() == null && splitOutputTx != null) {
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
@ -818,27 +818,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setState(OpenOffer.State.SCHEDULED);
}
// handle split output available
if (splitOutputTx != null && !splitOutputTx.isLocked()) {
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
return;
} else if (splitOutputTx == null) {
// if not found, create tx to split exact output
if (splitOutputTx == null) {
splitOrSchedule(openOffers, openOffer, offerReserveAmount);
} else if (!splitOutputTx.isLocked()) {
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
if (sufficientAvailableBalance) {
// otherwise sign and post offer if split output available
signAndPostOffer(openOffer, true, resultHandler, (errMsg) -> {
// 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);
// 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;
}
} else {
@ -862,44 +860,65 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}).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) {
return findSplitOutputFundingTx(getOpenOffers(), amount, null) != null;
return findSplitOutputFundingTx(getOpenOffers(), null, amount, null) != null;
}
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);
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<>();
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()
.setIsConfirmed(true)
.setOutputQuery(new MoneroOutputQuery()
.setAccountIndex(0)
.setSubaddressIndex(subaddressIndex)
.setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount)
.setIsSpent(false)
.setIsFrozen(false)));
// return earliest tx if available
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
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
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)
fundingTxs.clear();
@ -907,7 +926,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery()
.setIsIncoming(true)
.setAccountIndex(0)
.setSubaddressIndex(subaddressIndex)
.setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount)).size() > 0;
if (hasExactTransfer) fundingTxs.add(tx);
}
@ -982,13 +1001,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) {
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
String fundingSubaddress = xmrWalletService.getNewAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getAddressString();
return xmrWalletService.getWallet().createTx(new MoneroTxConfig()
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
String fundingSubaddress = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getAddressString();
log.info("Creating split output tx to fund offer {}", openOffer.getId());
MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAddress(fundingSubaddress)
.setAmount(reserveAmount)
.setRelay(true));
log.info("Done creating split output tx to fund offer {}", openOffer.getId());
return splitOutputTx;
}
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 securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
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);
Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null;
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, model.getOpenOffer().isSplitOutput(), preferredSubaddressIndex);
// check for error in case creating reserve tx exceeded timeout
// TODO: better way?

View File

@ -61,7 +61,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroError;
import monero.wallet.model.MoneroTxConfig;
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 (loserPayoutAmount.compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Loser payout cannot be negative");
if (winnerPayoutAmount.add(loserPayoutAmount).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance");
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + winnerPayoutAmount + " + " + loserPayoutAmount + " = " + (winnerPayoutAmount.add(loserPayoutAmount)));
}
// add any loss of precision to winner payout
winnerPayoutAmount = winnerPayoutAmount.add(trade.getWallet().getUnlockedBalance().subtract(winnerPayoutAmount.add(loserPayoutAmount)));
// create transaction to get fee estimate
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
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 (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig);
// create payout tx by increasing estimated fee until successful
// create dispute payout tx
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
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
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/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
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
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?
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Loser payout is too small to cover the mining 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
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 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
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.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);
// set state after payout as we call swapTradeEntryToAvailableEntry
// set state after payout as we call swapAddressEntryToAvailable
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
} else {

View File

@ -630,7 +630,7 @@ public abstract class Trade implements Tradable, Model {
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this);
// reset address entries
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
processModel.getXmrWalletService().resetAddressEntriesForTrade(getId());
}
// cleanup when payout unlocks
@ -922,32 +922,13 @@ public abstract class Trade implements Tradable, Model {
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create transaction to get fee estimate
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
// create payout tx
MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)))
.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))))
.addDestination(buyerPayoutAddress, buyerPayoutAmount)
.addDestination(sellerPayoutAddress, sellerPayoutAmount)
.setSubtractFeeFrom(0, 1) // split tx fee
.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
getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());

View File

@ -356,6 +356,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
reservedKeyImages.addAll(trade.getSelf().getReserveTxKeyImages());
}
for (OpenOffer openOffer : openOfferManager.getObservableList()) {
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue;
reservedKeyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
}
@ -473,7 +474,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
.filter(addressEntry -> addressEntry.getOfferId() != null)
.forEach(addressEntry -> {
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();
@ -946,7 +947,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
removeTrade(trade);
// 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();
}
@ -961,7 +962,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Trade trade = tradeOptional.get();
trade.setDisputeState(disputeState);
onTradeCompleted(trade);
xmrWalletService.swapTradeEntryToAvailableEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
xmrWalletService.resetAddressEntriesForTrade(trade.getId());
requestPersistence();
}
}

View File

@ -22,6 +22,7 @@ import common.utils.JsonUtils;
import haveno.common.app.Version;
import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner;
import haveno.common.util.Tuple2;
import haveno.core.offer.Offer;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
@ -33,6 +34,7 @@ import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx;
import java.math.BigInteger;
import java.util.Arrays;
@ -83,8 +85,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
String depositAddress = processModel.getMultisigAddress();
// verify deposit tx
Tuple2<MoneroTx, BigInteger> txResult;
try {
trade.getXmrWalletService().verifyTradeTx(
txResult = trade.getXmrWalletService().verifyTradeTx(
offer.getId(),
tradeFee,
sendAmount,
@ -100,6 +103,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
}
// set deposit info
trader.setSecurityDeposit(txResult.second);
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
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.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -77,15 +76,12 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs
Integer subaddressIndex = null;
BigInteger exactOutputAmount = null;
boolean isSplitOutputOffer = false;
if (trade instanceof MakerTrade) {
boolean isSplitOutputOffer = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isSplitOutput();
if (isSplitOutputOffer) {
exactOutputAmount = trade.getOffer().getReserveAmount();
subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
isSplitOutputOffer = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isSplitOutput();
if (isSplitOutputOffer) 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
List<String> reservedKeyImages = new ArrayList<String>();

View File

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

View File

@ -44,7 +44,7 @@ public class SellerPublishDepositTx extends TradeTask {
//
// 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);
//
// 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 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();
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
List<String> reservedKeyImages = new ArrayList<String>();

View File

@ -131,7 +131,7 @@ public class VolumeUtil {
}
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) {

View File

@ -289,9 +289,9 @@ public class BtcWalletService extends WalletService {
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) {
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 " +
"are no redeemable funds on that addresses. Only the keys are used for creating " +
"the Multisig address. offerId={}, context={}", offerId, context);
@ -327,8 +327,8 @@ public class BtcWalletService extends WalletService {
public void resetAddressEntriesForOpenOffer(String offerId) {
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING);
swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE);
swapAddressEntryToAvailable(offerId, AddressEntry.Context.OFFER_FUNDING);
swapAddressEntryToAvailable(offerId, AddressEntry.Context.RESERVED_FOR_TRADE);
}
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
// 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, AddressEntry.Context.TRADE_PAYOUT);
swapAddressEntryToAvailable(offerId, AddressEntry.Context.TRADE_PAYOUT);
}
public void swapAnyTradeEntryContextToAvailableEntry(String offerId) {

View File

@ -57,17 +57,18 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
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_NAME = "haveno_XMR";
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 MONERO_LOG_LEVEL = 0;
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
* 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 sendAmount amount to give peer
* @param securityDeposit security deposit amount
* @param returnAddress return address for reserved funds
* @param exactOutputAmount exact output amount to spend (optional)
* @param subaddressIndex preferred source subaddress to spend from (optional)
* @param reserveExactAmount specifies to reserve the exact input amount
* @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @return a transaction to reserve a trade
*/
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, BigInteger exactOutputAmount, Integer subaddressIndex) {
log.info("Creating reserve tx with return address={}", returnAddress);
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis();
try {
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true, exactOutputAmount, subaddressIndex);
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
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.
*
* @param trade the trade to create a deposit tx from
* @param exactOutputAmount exact output amount to spend (optional)
* @param subaddressIndex preferred source subaddress to spend from (optional)
* @param reserveExactAmount specifies to reserve the exact input amount
* @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @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();
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();
// thaw reserved outputs then create deposit tx
MoneroWallet wallet = getWallet();
synchronized (wallet) {
@ -374,91 +378,70 @@ public class XmrWalletService {
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();
try {
MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false, exactOutputAmount, subaddressIndex);
log.info("Creating deposit tx with multisig address={}", multisigAddress);
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);
return tradeTx;
} 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;
}
return depositTx;
}
}
// retry with exact outputs in other subaddresses
// 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) {
private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
MoneroWallet wallet = getWallet();
synchronized (wallet) {
// binary search to maximize security deposit and minimize potential dust
// TODO: binary search is hacky and slow over TOR connections, replace with destination paying tx fee
MoneroTxWallet tradeTx = null;
double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit
double searchDiff = 1.0; // difference for next binary search
int maxSearches = 5;
for (int i = 0; i < maxSearches; i++) {
try {
BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger();
BigInteger amount = sendAmount.add(isReserveTx ? tradeFee : appliedSecurityDeposit);
MoneroTxWallet testTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setSubaddressIndices(subaddressIndex == null ? null : Arrays.asList(subaddressIndex)) // TODO monero-java: MoneroTxConfig.setSubadddressIndex(int) causes NPE with null subaddress, could setSubaddressIndices(null) as convenience
.addDestination(HavenoUtils.getTradeFeeAddress(), isReserveTx ? appliedSecurityDeposit : tradeFee) // reserve tx charges security deposit if published
.addDestination(address, amount));
// create a list of subaddresses to attempt spending from in preferred order
List<Integer> subaddressIndices = new ArrayList<Integer>();
if (reserveExactAmount) {
BigInteger exactInputAmount = tradeFee.add(sendAmount).add(securityDeposit);
List<Integer> subaddressIndicesWithExactInput = getSubaddressesWithExactInput(exactInputAmount);
if (preferredSubaddressIndex != null) subaddressIndicesWithExactInput.remove(preferredSubaddressIndex);
Collections.sort(subaddressIndicesWithExactInput);
Collections.reverse(subaddressIndicesWithExactInput);
subaddressIndices.addAll(subaddressIndicesWithExactInput);
}
if (preferredSubaddressIndex != null) {
if (wallet.getBalance(0, preferredSubaddressIndex).compareTo(BigInteger.valueOf(0)) > 0) {
subaddressIndices.add(0, preferredSubaddressIndex); // try preferred subaddress first if funded
} else if (reserveExactAmount) {
subaddressIndices.add(preferredSubaddressIndex); // otherwise only try preferred subaddress if using exact output
}
}
// assert exact input if expected
if (exactOutputAmount == null) {
tradeTx = testTx;
} else {
// 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 : testTx.getInputsWallet()) {
for (MoneroOutputWallet txInput : tradeTx.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;
if (inputSum.compareTo(exactInputAmount) > 0) throw new RuntimeException("Cannot create transaction with exact input amount");
}
// freeze inputs
@ -466,7 +449,6 @@ public class XmrWalletService {
saveMainWallet();
return tradeTx;
}
}
/**
* Verify a reserve or deposit transaction.
@ -533,19 +515,20 @@ public class XmrWalletService {
BigInteger actualSendAmount = transferCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
// verify trade fee
if (!tradeFee.equals(actualTradeFee)) {
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));
if (actualTradeFee.compareTo(tradeFee) < 0) {
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
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit);
// verify send amount
if (!actualSendAmount.equals(sendAmount)) {
throw new RuntimeException("Unexpected send amount, expected " + sendAmount + " but was " + actualSendAmount);
}
// verify deposit amount + miner fee within dust tolerance
//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 minDeposit = sendAmount.add(minSecurityDeposit);
BigInteger actualDeposit = actualSendAmount.add(actualSecurityDeposit);
if (actualDeposit.compareTo(minDeposit) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDeposit + " but was " + actualDeposit);
// verify security deposit
BigInteger expectedSecurityDeposit = securityDeposit.subtract(tx.getFee()); // fee is paid from security deposit
if (!actualSecurityDeposit.equals(expectedSecurityDeposit)) {
throw new RuntimeException("Unexpected security deposit amount, expected " + expectedSecurityDeposit + " but was " + actualSecurityDeposit);
}
} catch (Exception e) {
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage());
throw e;
@ -926,8 +909,8 @@ public class XmrWalletService {
// try to use available and not yet used entries
try {
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();
if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries(incomingTxs);
if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId);
} catch (Exception e) {
log.warn("Error getting new address entry based on incoming transactions");
e.printStackTrace();
@ -983,7 +966,7 @@ public class XmrWalletService {
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();
addressEntryOptional.ifPresent(e -> {
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) {
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) {
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases
// where a user cannot send the funds
// 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
// funds to the wallet and then
// 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);
public synchronized void resetOfferFundingForOpenOffer(String offerId) {
log.info("resetOfferFundingForOpenOffer offerId={}", offerId);
swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
}
public synchronized void resetAddressEntriesForTrade(String offerId) {
swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
}
private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) {
@ -1063,32 +1041,36 @@ public class XmrWalletService {
public List<XmrAddressEntry> getUnusedAddressEntries(List<MoneroTxWallet> cachedTxs) {
return getAvailableAddressEntries().stream()
.filter(e -> isSubaddressUnused(e.getSubaddressIndex(), cachedTxs))
.filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex(), cachedTxs))
.collect(Collectors.toList());
}
public boolean isSubaddressUnused(int subaddressIndex) {
return isSubaddressUnused(subaddressIndex, null);
public boolean subaddressHasIncomingTransfers(int subaddressIndex) {
return subaddressHasIncomingTransfers(subaddressIndex, null);
}
private boolean isSubaddressUnused(int subaddressIndex, List<MoneroTxWallet> incomingTxs) {
return getNumOutputsForSubaddress(subaddressIndex, incomingTxs) == 0;
}
public int getNumOutputsForSubaddress(int subaddressIndex) {
return getNumOutputsForSubaddress(subaddressIndex, null);
private boolean subaddressHasIncomingTransfers(int subaddressIndex, List<MoneroTxWallet> incomingTxs) {
return getNumOutputsForSubaddress(subaddressIndex, incomingTxs) > 0;
}
public int getNumOutputsForSubaddress(int subaddressIndex, List<MoneroTxWallet> incomingTxs) {
if (incomingTxs == null) incomingTxs = getTxsWithIncomingOutputs(subaddressIndex);
incomingTxs = getTxsWithIncomingOutputs(subaddressIndex, incomingTxs);
int numUnspentOutputs = 0;
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
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;
}
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() {
return getTxsWithIncomingOutputs(null);
}

View File

@ -1016,6 +1016,9 @@ funds.tab.transactions=Transactions
funds.deposit.unused=Unused
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.noAddresses=No deposit addresses have been generated yet
funds.deposit.fundWallet=Fund your wallet

View File

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

View File

@ -95,8 +95,23 @@ class DepositListItem {
}
private void updateUsage(int subaddressIndex, List<MoneroTxWallet> cachedTxs) {
numTxsWithOutputs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs).size();
usage = subaddressIndex == 0 ? "Base address" : numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs);
numTxsWithOutputs = xmrWalletService.getNumTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs);
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() {

View File

@ -156,7 +156,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
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.setItems(sortedList);
@ -202,8 +202,8 @@ public class DepositView extends ActivatableView<VBox, Void> {
generateNewAddressButton = buttonCheckBoxHBox.first;
generateNewAddressButton.setOnAction(event -> {
boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getSubaddressIndex() != 0 && xmrWalletService.getTxsWithIncomingOutputs(e.getSubaddressIndex()).isEmpty());
if (hasUnUsedAddress) {
boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty();
if (hasUnusedAddress) {
new Popup().warning(Res.get("funds.deposit.selectUnused")).show();
} else {
XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry();
@ -311,7 +311,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
// cache incoming txs
txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs();
// add available address entries and base address
// add address entries
xmrWalletService.getAddressEntries()
.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) {
openOfferManager.removeOpenOffer(openOffer, resultHandler, errorMessageHandler);
openOfferManager.cancelOpenOffer(openOffer, resultHandler, errorMessageHandler);
}

View File

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