fix 'not enough money' error on reserve exact offer amount #1089

This commit is contained in:
woodser 2024-07-03 11:00:08 -04:00
parent 86e4f7b3f2
commit 5d39eecd4f
3 changed files with 49 additions and 84 deletions

View File

@ -42,7 +42,6 @@ import javafx.beans.property.SimpleObjectProperty;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -51,7 +50,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
@EqualsAndHashCode @EqualsAndHashCode
@Slf4j
public final class OpenOffer implements Tradable { public final class OpenOffer implements Tradable {
public enum State { public enum State {

View File

@ -96,6 +96,7 @@ import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -113,6 +114,7 @@ import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTransferQuery;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxQuery;
@ -852,10 +854,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
public boolean hasAvailableOutput(BigInteger amount) {
return findSplitOutputFundingTx(getOpenOffers(), null, amount, null) != null;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Place offer helpers // Place offer helpers
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -929,7 +927,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isReserveExactAmount()) { if (openOffer.isReserveExactAmount()) {
// find tx with exact input amount // find tx with exact input amount
MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer); MoneroTxWallet splitOutputTx = getSplitOutputFundingTx(openOffers, openOffer);
if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) { if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) {
setSplitOutputTx(openOffer, splitOutputTx); setSplitOutputTx(openOffer, splitOutputTx);
} }
@ -965,89 +963,62 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}).start(); }).start();
} }
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) { private MoneroTxWallet getSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) {
XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
return findSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex());
} }
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { private MoneroTxWallet getSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> fundingTxs = new ArrayList<>();
MoneroTxWallet earliestUnscheduledTx = null;
// return split output tx if already assigned // return split output tx if already assigned
if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { if (openOffer != null && openOffer.getSplitOutputTxHash() != null) {
return xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); return xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
} }
// return earliest tx with exact amount to offer's subaddress if available // get split output tx to offer's preferred subaddress
if (preferredSubaddressIndex != null) { if (preferredSubaddressIndex != null) {
List<MoneroTxWallet> fundingTxs = getSplitOutputFundingTxs(reserveAmount, preferredSubaddressIndex);
MoneroTxWallet earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
}
// get txs with exact output amount // get split output tx to any subaddress
fundingTxs = xmrWalletService.getTxs(new MoneroTxQuery() List<MoneroTxWallet> fundingTxs = getSplitOutputFundingTxs(reserveAmount, null);
.setIsConfirmed(true) return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs);
.setOutputQuery(new MoneroOutputQuery() }
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
for (MoneroTxWallet tx : splitOutputTxs) {
if (tx.getOutputs() != null) { // outputs not available until first confirmation
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (output.isSpent() || output.isFrozen()) removeTxs.add(tx);
}
}
if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx);
}
splitOutputTxs.removeAll(removeTxs);
return splitOutputTxs;
}
private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) {
boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery()
.setAccountIndex(0) .setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex) .setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount) .setAmount(amount)).size() > 0);
.setIsSpent(false) if (hasExactOutput) return true;
.setIsFrozen(false))); boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
// return earliest tx if available
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
}
// return if awaiting scheduled tx
if (openOffer.getScheduledTxHashes() != null) return null;
// get all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getTxs(false);
if (preferredSubaddressIndex != null) {
// return earliest tx with exact incoming transfer to fund offer's subaddress if available (since outputs are not available until confirmed)
fundingTxs.clear();
for (MoneroTxWallet tx : allTxs) {
boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery()
.setIsIncoming(true)
.setAccountIndex(0) .setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex) .setSubaddressIndex(preferredSubaddressIndex)
.setAmount(reserveAmount)).size() > 0; .setAmount(amount)).size() > 0);
if (hasExactTransfer) fundingTxs.add(tx); return hasExactTransfer;
}
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
} }
// return earliest tx with exact confirmed output to any subaddress if available private MoneroTxWallet getEarliestUnscheduledTx(List<OpenOffer> openOffers, OpenOffer excludeOpenOffer, List<MoneroTxWallet> txs) {
fundingTxs.clear();
for (MoneroTxWallet tx : allTxs) {
boolean hasExactOutput = tx.getOutputsWallet(new MoneroOutputQuery()
.setAccountIndex(0)
.setAmount(reserveAmount)
.setIsSpent(false)
.setIsFrozen(false)).size() > 0;
if (hasExactOutput) fundingTxs.add(tx);
}
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
// return earliest tx with exact incoming transfer to any subaddress if available (since outputs are not available until confirmed)
fundingTxs.clear();
for (MoneroTxWallet tx : allTxs) {
boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery()
.setIsIncoming(true)
.setAccountIndex(0)
.setAmount(reserveAmount)).size() > 0;
if (hasExactTransfer) fundingTxs.add(tx);
}
return getEarliestUnscheduledTx(openOffers, fundingTxs);
}
private MoneroTxWallet getEarliestUnscheduledTx(List<OpenOffer> openOffers, List<MoneroTxWallet> txs) {
MoneroTxWallet earliestUnscheduledTx = null; MoneroTxWallet earliestUnscheduledTx = null;
for (MoneroTxWallet tx : txs) { for (MoneroTxWallet tx : txs) {
if (isTxScheduled(openOffers, tx.getHash())) continue; if (isTxScheduledByOtherOffer(openOffers, excludeOpenOffer, tx.getHash())) continue;
if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx; if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx;
} }
return earliestUnscheduledTx; return earliestUnscheduledTx;
@ -1121,7 +1092,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
List<String> scheduledTxHashes = new ArrayList<String>(); List<String> scheduledTxHashes = new ArrayList<String>();
BigInteger scheduledAmount = BigInteger.ZERO; BigInteger scheduledAmount = BigInteger.ZERO;
for (MoneroTxWallet lockedTx : lockedTxs) { for (MoneroTxWallet lockedTx : lockedTxs) {
if (isTxScheduled(openOffers, lockedTx.getHash())) continue; if (isTxScheduledByOtherOffer(openOffers, openOffer, lockedTx.getHash())) continue;
if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue; if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue;
scheduledTxHashes.add(lockedTx.getHash()); scheduledTxHashes.add(lockedTx.getHash());
for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) { for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) {
@ -1154,11 +1125,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return scheduledAmount; return scheduledAmount;
} }
private boolean isTxScheduled(List<OpenOffer> openOffers, String txHash) { private boolean isTxScheduledByOtherOffer(List<OpenOffer> openOffers, OpenOffer openOffer, String txHash) {
for (OpenOffer openOffer : openOffers) { for (OpenOffer otherOffer : openOffers) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (otherOffer == openOffer) continue;
if (openOffer.getScheduledTxHashes() == null) continue; if (otherOffer.getState() != OpenOffer.State.SCHEDULED) continue;
for (String scheduledTxHash : openOffer.getScheduledTxHashes()) { if (otherOffer.getScheduledTxHashes() == null) continue;
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true; if (txHash.equals(scheduledTxHash)) return true;
} }
} }

View File

@ -466,11 +466,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
} }
} }
public boolean hasAvailableSplitOutput() {
BigInteger reserveAmount = totalToPay.get();
return openOfferManager.hasAvailableOutput(reserveAmount);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Utils // Utils
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////