mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-24 06:49:31 -05:00
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:
parent
13d87a32a5
commit
242bc0e3bb
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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?
|
||||||
|
@ -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());
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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());
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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>();
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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>();
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user