show offers as pending, fix offer funding from manual subaddress

This commit is contained in:
woodser 2023-06-14 10:54:34 -04:00
parent 9ee67a046c
commit 59c0496d34
6 changed files with 97 additions and 25 deletions

View File

@ -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;
} }

View File

@ -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,6 +547,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
if (openOffer.isScheduled()) {
resultHandler.handleResult(); // ignore if scheduled
} else {
offerBookService.deactivateOffer(offer.getOfferPayload(), offerBookService.deactivateOffer(offer.getOfferPayload(),
() -> { () -> {
openOffer.setState(OpenOffer.State.DEACTIVATED); openOffer.setState(OpenOffer.State.DEACTIVATED);
@ -554,6 +559,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, },
errorMessageHandler); errorMessageHandler);
} }
}
public void removeOpenOffer(OpenOffer openOffer, public void removeOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler, ResultHandler resultHandler,
@ -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()) {
@ -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)

View File

@ -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) {

View File

@ -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

View File

@ -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));