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

View File

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

View File

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

View File

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

View File

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

View File

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