mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-23 04:29:22 +00:00
show offers as pending, fix offer funding from manual subaddress
This commit is contained in:
parent
9ee67a046c
commit
59c0496d34
6 changed files with 97 additions and 25 deletions
|
@ -21,6 +21,9 @@ import haveno.common.Timer;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.proto.ProtoUtil;
|
import haveno.common.proto.ProtoUtil;
|
||||||
import haveno.core.trade.Tradable;
|
import haveno.core.trade.Tradable;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
@ -78,15 +81,12 @@ public final class OpenOffer implements Tradable {
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
private String reserveTxKey;
|
private String reserveTxKey;
|
||||||
|
|
||||||
|
|
||||||
// Added in v1.5.3.
|
|
||||||
// If market price reaches that trigger price the offer gets deactivated
|
|
||||||
@Getter
|
@Getter
|
||||||
private final long triggerPrice;
|
private final long triggerPrice;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
transient private long mempoolStatus = -1;
|
transient private long mempoolStatus = -1;
|
||||||
|
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||||
|
|
||||||
public OpenOffer(Offer offer) {
|
public OpenOffer(Offer offer) {
|
||||||
this(offer, 0, false);
|
this(offer, 0, false);
|
||||||
|
@ -185,6 +185,7 @@ public final class OpenOffer implements Tradable {
|
||||||
|
|
||||||
public void setState(State state) {
|
public void setState(State state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
stateProperty.set(state);
|
||||||
|
|
||||||
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
|
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
|
||||||
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
|
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
|
||||||
|
@ -194,6 +195,14 @@ public final class OpenOffer implements Tradable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||||
|
return stateProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScheduled() {
|
||||||
|
return state == State.SCHEDULED;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isDeactivated() {
|
public boolean isDeactivated() {
|
||||||
return state == State.DEACTIVATED;
|
return state == State.DEACTIVATED;
|
||||||
}
|
}
|
||||||
|
|
|
@ -526,7 +526,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
public void activateOpenOffer(OpenOffer openOffer,
|
public void activateOpenOffer(OpenOffer openOffer,
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
if (openOffer.isScheduled()) {
|
||||||
|
resultHandler.handleResult(); // ignore if scheduled
|
||||||
|
} else if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
||||||
Offer offer = openOffer.getOffer();
|
Offer offer = openOffer.getOffer();
|
||||||
offerBookService.activateOffer(offer,
|
offerBookService.activateOffer(offer,
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -545,14 +547,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
Offer offer = openOffer.getOffer();
|
Offer offer = openOffer.getOffer();
|
||||||
offerBookService.deactivateOffer(offer.getOfferPayload(),
|
if (openOffer.isScheduled()) {
|
||||||
() -> {
|
resultHandler.handleResult(); // ignore if scheduled
|
||||||
openOffer.setState(OpenOffer.State.DEACTIVATED);
|
} else {
|
||||||
requestPersistence();
|
offerBookService.deactivateOffer(offer.getOfferPayload(),
|
||||||
log.debug("deactivateOpenOffer, offerId={}", offer.getId());
|
() -> {
|
||||||
resultHandler.handleResult();
|
openOffer.setState(OpenOffer.State.DEACTIVATED);
|
||||||
},
|
requestPersistence();
|
||||||
errorMessageHandler);
|
log.debug("deactivateOpenOffer, offerId={}", offer.getId());
|
||||||
|
resultHandler.handleResult();
|
||||||
|
},
|
||||||
|
errorMessageHandler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeOpenOffer(OpenOffer openOffer,
|
public void removeOpenOffer(OpenOffer openOffer,
|
||||||
|
@ -799,6 +805,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
|
|
||||||
// get offer reserve amount
|
// get offer reserve amount
|
||||||
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
||||||
|
|
||||||
// handle split output offer
|
// handle split output offer
|
||||||
if (openOffer.isSplitOutput()) {
|
if (openOffer.isSplitOutput()) {
|
||||||
|
|
||||||
|
@ -816,11 +823,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
||||||
return;
|
return;
|
||||||
} else if (splitOutputTx == null) {
|
} else if (splitOutputTx == null) {
|
||||||
|
|
||||||
// handle sufficient available balance to split output
|
// handle sufficient available balance to split output
|
||||||
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
|
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
|
||||||
if (sufficientAvailableBalance) {
|
if (sufficientAvailableBalance) {
|
||||||
|
|
||||||
// create and relay tx to split output
|
// create and relay tx to split output
|
||||||
splitOutputTx = createAndRelaySplitOutputTx(openOffer); // TODO: confirm with user?
|
splitOutputTx = createAndRelaySplitOutputTx(openOffer); // TODO: confirm with user?
|
||||||
|
|
||||||
|
@ -975,7 +982,8 @@ 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();
|
||||||
String fundingSubaddress = xmrWalletService.getAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getAddressString();
|
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()
|
return xmrWalletService.getWallet().createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.setAddress(fundingSubaddress)
|
.setAddress(fundingSubaddress)
|
||||||
|
|
|
@ -56,7 +56,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
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;
|
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 = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null;
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex);
|
||||||
|
|
||||||
// check for error in case creating reserve tx exceeded timeout
|
// check for error in case creating reserve tx exceeded timeout
|
||||||
|
|
|
@ -342,6 +342,7 @@ public class XmrWalletService {
|
||||||
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) {
|
} catch (Exception e) {
|
||||||
|
if (exactOutputAmount != null) return spendOutputManually(true, tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount);
|
||||||
|
|
||||||
// retry creating reserve tx using funds outside subaddress
|
// retry creating reserve tx using funds outside subaddress
|
||||||
if (subaddressIndex != null) return createReserveTx(tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, null);
|
if (subaddressIndex != null) return createReserveTx(tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, null);
|
||||||
|
@ -349,7 +350,7 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**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
|
||||||
|
@ -380,15 +381,42 @@ public class XmrWalletService {
|
||||||
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 tradeTx;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (exactOutputAmount != null) return spendOutputManually(false, tradeFee, sendAmount, securityDeposit, multisigAddress, exactOutputAmount);
|
||||||
|
|
||||||
// retry creating deposit tx using funds outside subaddress
|
// retry creating deposit tx using funds outside subaddress
|
||||||
if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null);
|
if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null);
|
||||||
else throw e;
|
else throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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, BigInteger exactOutputAmount, Integer subaddressIndex) {
|
||||||
MoneroWallet wallet = getWallet();
|
MoneroWallet wallet = getWallet();
|
||||||
synchronized (wallet) {
|
synchronized (wallet) {
|
||||||
|
@ -398,7 +426,7 @@ public class XmrWalletService {
|
||||||
MoneroTxWallet tradeTx = null;
|
MoneroTxWallet tradeTx = null;
|
||||||
double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit
|
double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit
|
||||||
double searchDiff = 1.0; // difference for next binary search
|
double searchDiff = 1.0; // difference for next binary search
|
||||||
int maxSearches = 5 ;
|
int maxSearches = 5;
|
||||||
for (int i = 0; i < maxSearches; i++) {
|
for (int i = 0; i < maxSearches; i++) {
|
||||||
try {
|
try {
|
||||||
BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger();
|
BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger();
|
||||||
|
|
|
@ -229,6 +229,7 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac
|
||||||
shared.numItemsLabel=Number of entries: {0}
|
shared.numItemsLabel=Number of entries: {0}
|
||||||
shared.filter=Filter
|
shared.filter=Filter
|
||||||
shared.enabled=Enabled
|
shared.enabled=Enabled
|
||||||
|
shared.pending=Pending
|
||||||
shared.me=Me
|
shared.me=Me
|
||||||
shared.maker=Maker
|
shared.maker=Maker
|
||||||
shared.taker=Taker
|
shared.taker=Taker
|
||||||
|
|
|
@ -68,10 +68,17 @@ import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
|
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
import org.fxmisc.easybind.Subscription;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static haveno.desktop.util.FormBuilder.getRegularIconButton;
|
import static haveno.desktop.util.FormBuilder.getRegularIconButton;
|
||||||
|
|
||||||
|
@ -109,6 +116,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||||
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
|
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
|
||||||
private ChangeListener<Number> widthListener;
|
private ChangeListener<Number> widthListener;
|
||||||
|
|
||||||
|
private Map<String, Subscription> offerStateSubscriptions = new HashMap<String, Subscription>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) {
|
public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) {
|
||||||
super(model);
|
super(model);
|
||||||
|
@ -285,16 +294,24 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||||
root.widthProperty().removeListener(widthListener);
|
root.widthProperty().removeListener(widthListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refresh() {
|
||||||
|
tableView.refresh();
|
||||||
|
updateSelectToggleButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSelectToggleButtonState() {
|
private void updateSelectToggleButtonState() {
|
||||||
if (sortedList.size() == 0) {
|
List<OpenOfferListItem> availableItems = sortedList.stream()
|
||||||
|
.filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (availableItems.size() == 0) {
|
||||||
selectToggleButton.setDisable(true);
|
selectToggleButton.setDisable(true);
|
||||||
selectToggleButton.setSelected(false);
|
selectToggleButton.setSelected(false);
|
||||||
} else {
|
} else {
|
||||||
selectToggleButton.setDisable(false);
|
selectToggleButton.setDisable(false);
|
||||||
long numDeactivated = sortedList.stream()
|
long numDeactivated = availableItems.stream()
|
||||||
.filter(openOfferListItem -> openOfferListItem.getOpenOffer().isDeactivated())
|
.filter(openOfferListItem -> openOfferListItem.getOpenOffer().isDeactivated())
|
||||||
.count();
|
.count();
|
||||||
if (numDeactivated == sortedList.size()) {
|
if (numDeactivated == availableItems.size()) {
|
||||||
selectToggleButton.setSelected(false);
|
selectToggleButton.setSelected(false);
|
||||||
} else if (numDeactivated == 0) {
|
} else if (numDeactivated == 0) {
|
||||||
selectToggleButton.setSelected(true);
|
selectToggleButton.setSelected(true);
|
||||||
|
@ -683,15 +700,24 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||||
AutoTooltipSlideToggleButton checkBox;
|
AutoTooltipSlideToggleButton checkBox;
|
||||||
|
|
||||||
private void updateState(@NotNull OpenOffer openOffer) {
|
private void updateState(@NotNull OpenOffer openOffer) {
|
||||||
checkBox.setSelected(!openOffer.isDeactivated());
|
if (checkBox != null) checkBox.setSelected(!openOffer.isDeactivated());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
if (item != null && !empty) {
|
if (item != null && !empty) {
|
||||||
OpenOffer openOffer = item.getOpenOffer();
|
OpenOffer openOffer = item.getOpenOffer();
|
||||||
|
if (!offerStateSubscriptions.containsKey(openOffer.getId())) {
|
||||||
|
offerStateSubscriptions.put(openOffer.getId(), EasyBind.subscribe(openOffer.stateProperty(), state -> {
|
||||||
|
refresh();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (openOffer.getState() == OpenOffer.State.SCHEDULED) {
|
||||||
|
setGraphic(new AutoTooltipLabel(Res.get("shared.pending")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (checkBox == null) {
|
if (checkBox == null) {
|
||||||
checkBox = new AutoTooltipSlideToggleButton();
|
checkBox = new AutoTooltipSlideToggleButton();
|
||||||
checkBox.setPadding(new Insets(-7, 0, -7, 0));
|
checkBox.setPadding(new Insets(-7, 0, -7, 0));
|
||||||
|
|
Loading…
Reference in a new issue