show offers as pending, fix offer funding from manual subaddress
This commit is contained in:
parent
9ee67a046c
commit
59c0496d34
@ -21,6 +21,9 @@ import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.proto.ProtoUtil;
|
||||
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.Getter;
|
||||
import lombok.Setter;
|
||||
@ -78,15 +81,12 @@ public final class OpenOffer implements Tradable {
|
||||
@Setter
|
||||
@Getter
|
||||
private String reserveTxKey;
|
||||
|
||||
|
||||
// Added in v1.5.3.
|
||||
// If market price reaches that trigger price the offer gets deactivated
|
||||
@Getter
|
||||
private final long triggerPrice;
|
||||
@Getter
|
||||
@Setter
|
||||
transient private long mempoolStatus = -1;
|
||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||
|
||||
public OpenOffer(Offer offer) {
|
||||
this(offer, 0, false);
|
||||
@ -185,6 +185,7 @@ public final class OpenOffer implements Tradable {
|
||||
|
||||
public void setState(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
|
||||
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() {
|
||||
return state == State.DEACTIVATED;
|
||||
}
|
||||
|
@ -526,7 +526,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
public void activateOpenOffer(OpenOffer openOffer,
|
||||
ResultHandler resultHandler,
|
||||
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();
|
||||
offerBookService.activateOffer(offer,
|
||||
() -> {
|
||||
@ -545,14 +547,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
Offer offer = openOffer.getOffer();
|
||||
offerBookService.deactivateOffer(offer.getOfferPayload(),
|
||||
() -> {
|
||||
openOffer.setState(OpenOffer.State.DEACTIVATED);
|
||||
requestPersistence();
|
||||
log.debug("deactivateOpenOffer, offerId={}", offer.getId());
|
||||
resultHandler.handleResult();
|
||||
},
|
||||
errorMessageHandler);
|
||||
if (openOffer.isScheduled()) {
|
||||
resultHandler.handleResult(); // ignore if scheduled
|
||||
} else {
|
||||
offerBookService.deactivateOffer(offer.getOfferPayload(),
|
||||
() -> {
|
||||
openOffer.setState(OpenOffer.State.DEACTIVATED);
|
||||
requestPersistence();
|
||||
log.debug("deactivateOpenOffer, offerId={}", offer.getId());
|
||||
resultHandler.handleResult();
|
||||
},
|
||||
errorMessageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOpenOffer(OpenOffer openOffer,
|
||||
@ -799,6 +805,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
// get offer reserve amount
|
||||
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
|
||||
// handle split output offer
|
||||
if (openOffer.isSplitOutput()) {
|
||||
|
||||
@ -816,11 +823,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
||||
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?
|
||||
|
||||
@ -975,7 +982,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) {
|
||||
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()
|
||||
.setAccountIndex(0)
|
||||
.setAddress(fundingSubaddress)
|
||||
|
@ -56,7 +56,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
BigInteger exactOutputAmount = model.getOpenOffer().isSplitOutput() ? model.getOpenOffer().getOffer().getReserveAmount() : null;
|
||||
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
|
||||
Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null;
|
||||
Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null;
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
@ -349,7 +350,7 @@ public class XmrWalletService {
|
||||
}
|
||||
}
|
||||
|
||||
/**s
|
||||
/**
|
||||
* Create the multisig deposit tx and freeze its inputs.
|
||||
*
|
||||
* @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);
|
||||
return tradeTx;
|
||||
} catch (Exception e) {
|
||||
if (exactOutputAmount != null) return spendOutputManually(false, tradeFee, sendAmount, securityDeposit, multisigAddress, exactOutputAmount);
|
||||
|
||||
// retry creating deposit tx using funds outside subaddress
|
||||
if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
MoneroWallet wallet = getWallet();
|
||||
synchronized (wallet) {
|
||||
@ -398,7 +426,7 @@ public class XmrWalletService {
|
||||
MoneroTxWallet tradeTx = null;
|
||||
double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit
|
||||
double searchDiff = 1.0; // difference for next binary search
|
||||
int maxSearches = 5 ;
|
||||
int maxSearches = 5;
|
||||
for (int i = 0; i < maxSearches; i++) {
|
||||
try {
|
||||
BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger();
|
||||
|
@ -229,6 +229,7 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac
|
||||
shared.numItemsLabel=Number of entries: {0}
|
||||
shared.filter=Filter
|
||||
shared.enabled=Enabled
|
||||
shared.pending=Pending
|
||||
shared.me=Me
|
||||
shared.maker=Maker
|
||||
shared.taker=Taker
|
||||
|
@ -68,10 +68,17 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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;
|
||||
|
||||
@ -109,6 +116,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
|
||||
private ChangeListener<Number> widthListener;
|
||||
|
||||
private Map<String, Subscription> offerStateSubscriptions = new HashMap<String, Subscription>();
|
||||
|
||||
@Inject
|
||||
public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) {
|
||||
super(model);
|
||||
@ -285,16 +294,24 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
root.widthProperty().removeListener(widthListener);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
tableView.refresh();
|
||||
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.setSelected(false);
|
||||
} else {
|
||||
selectToggleButton.setDisable(false);
|
||||
long numDeactivated = sortedList.stream()
|
||||
long numDeactivated = availableItems.stream()
|
||||
.filter(openOfferListItem -> openOfferListItem.getOpenOffer().isDeactivated())
|
||||
.count();
|
||||
if (numDeactivated == sortedList.size()) {
|
||||
if (numDeactivated == availableItems.size()) {
|
||||
selectToggleButton.setSelected(false);
|
||||
} else if (numDeactivated == 0) {
|
||||
selectToggleButton.setSelected(true);
|
||||
@ -683,15 +700,24 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
AutoTooltipSlideToggleButton checkBox;
|
||||
|
||||
private void updateState(@NotNull OpenOffer openOffer) {
|
||||
checkBox.setSelected(!openOffer.isDeactivated());
|
||||
if (checkBox != null) checkBox.setSelected(!openOffer.isDeactivated());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
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) {
|
||||
checkBox = new AutoTooltipSlideToggleButton();
|
||||
checkBox.setPadding(new Insets(-7, 0, -7, 0));
|
||||
|
Loading…
Reference in New Issue
Block a user