mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
support reserving exact offer amount by splitting output
This commit is contained in:
parent
0bbb8a4183
commit
722b02f4c9
@ -405,6 +405,7 @@ public class CoreApi {
|
||||
long minAmountAsLong,
|
||||
double buyerSecurityDeposit,
|
||||
String triggerPriceAsString,
|
||||
boolean splitOutput,
|
||||
String paymentAccountId,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
@ -417,6 +418,7 @@ public class CoreApi {
|
||||
minAmountAsLong,
|
||||
buyerSecurityDeposit,
|
||||
triggerPriceAsString,
|
||||
splitOutput,
|
||||
paymentAccountId,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
|
@ -165,6 +165,7 @@ public class CoreOffersService {
|
||||
long minAmountAsLong,
|
||||
double buyerSecurityDeposit,
|
||||
String triggerPriceAsString,
|
||||
boolean splitOutput,
|
||||
String paymentAccountId,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
@ -200,6 +201,7 @@ public class CoreOffersService {
|
||||
placeOffer(offer,
|
||||
triggerPriceAsString,
|
||||
useSavingsWallet,
|
||||
splitOutput,
|
||||
transaction -> resultHandler.accept(offer),
|
||||
errorMessageHandler);
|
||||
}
|
||||
@ -269,12 +271,14 @@ public class CoreOffersService {
|
||||
private void placeOffer(Offer offer,
|
||||
String triggerPriceAsString,
|
||||
boolean useSavingsWallet,
|
||||
boolean splitOutput,
|
||||
Consumer<Transaction> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
|
||||
openOfferManager.placeOffer(offer,
|
||||
useSavingsWallet,
|
||||
triggerPriceAsLong,
|
||||
splitOutput,
|
||||
resultHandler::accept,
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
@ -285,6 +285,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
public BigInteger getReserveAmount() {
|
||||
BigInteger reserveAmount = getDirection() == OfferDirection.BUY ? getBuyerSecurityDeposit() : getSellerSecurityDeposit();
|
||||
if (getDirection() == OfferDirection.SELL) reserveAmount = reserveAmount.add(getAmount());
|
||||
reserveAmount = reserveAmount.add(getMakerFee());
|
||||
return reserveAmount;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ public final class OpenOffer implements Tradable {
|
||||
private State state;
|
||||
@Setter
|
||||
@Getter
|
||||
private boolean autoSplit;
|
||||
private boolean splitOutput;
|
||||
@Setter
|
||||
@Getter
|
||||
@Nullable
|
||||
@ -62,6 +62,10 @@ public final class OpenOffer implements Tradable {
|
||||
@Getter
|
||||
@Nullable
|
||||
private List<String> scheduledTxHashes;
|
||||
@Setter
|
||||
@Getter
|
||||
@Nullable
|
||||
String splitOutputTxHash;
|
||||
@Nullable
|
||||
@Setter
|
||||
@Getter
|
||||
@ -92,10 +96,10 @@ public final class OpenOffer implements Tradable {
|
||||
this(offer, triggerPrice, false);
|
||||
}
|
||||
|
||||
public OpenOffer(Offer offer, long triggerPrice, boolean autoSplit) {
|
||||
public OpenOffer(Offer offer, long triggerPrice, boolean splitOutput) {
|
||||
this.offer = offer;
|
||||
this.triggerPrice = triggerPrice;
|
||||
this.autoSplit = autoSplit;
|
||||
this.splitOutput = splitOutput;
|
||||
state = State.SCHEDULED;
|
||||
}
|
||||
|
||||
@ -106,17 +110,19 @@ public final class OpenOffer implements Tradable {
|
||||
private OpenOffer(Offer offer,
|
||||
State state,
|
||||
long triggerPrice,
|
||||
boolean autoSplit,
|
||||
boolean splitOutput,
|
||||
@Nullable String scheduledAmount,
|
||||
@Nullable List<String> scheduledTxHashes,
|
||||
String splitOutputTxHash,
|
||||
@Nullable String reserveTxHash,
|
||||
@Nullable String reserveTxHex,
|
||||
@Nullable String reserveTxKey) {
|
||||
this.offer = offer;
|
||||
this.state = state;
|
||||
this.triggerPrice = triggerPrice;
|
||||
this.autoSplit = autoSplit;
|
||||
this.splitOutput = splitOutput;
|
||||
this.scheduledTxHashes = scheduledTxHashes;
|
||||
this.splitOutputTxHash = splitOutputTxHash;
|
||||
this.reserveTxHash = reserveTxHash;
|
||||
this.reserveTxHex = reserveTxHex;
|
||||
this.reserveTxKey = reserveTxKey;
|
||||
@ -131,10 +137,11 @@ public final class OpenOffer implements Tradable {
|
||||
.setOffer(offer.toProtoMessage())
|
||||
.setTriggerPrice(triggerPrice)
|
||||
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
|
||||
.setAutoSplit(autoSplit);
|
||||
.setSplitOutput(splitOutput);
|
||||
|
||||
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
|
||||
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
|
||||
Optional.ofNullable(splitOutputTxHash).ifPresent(e -> builder.setSplitOutputTxHash(splitOutputTxHash));
|
||||
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
|
||||
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
|
||||
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
|
||||
@ -146,9 +153,10 @@ public final class OpenOffer implements Tradable {
|
||||
OpenOffer openOffer = new OpenOffer(Offer.fromProto(proto.getOffer()),
|
||||
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
|
||||
proto.getTriggerPrice(),
|
||||
proto.getAutoSplit(),
|
||||
proto.getSplitOutput(),
|
||||
proto.getScheduledAmount(),
|
||||
proto.getScheduledTxHashesList(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getSplitOutputTxHash()),
|
||||
proto.getReserveTxHash(),
|
||||
proto.getReserveTxHex(),
|
||||
proto.getReserveTxKey());
|
||||
|
@ -56,6 +56,7 @@ import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.JsonUtil;
|
||||
import haveno.core.util.Validator;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.BtcWalletService;
|
||||
import haveno.core.xmr.wallet.MoneroKeyImageListener;
|
||||
import haveno.core.xmr.wallet.MoneroKeyImagePoller;
|
||||
@ -79,6 +80,9 @@ import monero.common.MoneroRpcConnection;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.model.MoneroIncomingTransfer;
|
||||
import monero.wallet.model.MoneroOutputQuery;
|
||||
import monero.wallet.model.MoneroTransferQuery;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroTxQuery;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
@ -90,6 +94,7 @@ import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -290,7 +295,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
// process unposted offers
|
||||
processUnpostedOffers((transaction) -> {}, (errorMessage) -> {
|
||||
log.warn("Error processing unposted offers on new unlocked balance: " + errorMessage);
|
||||
log.warn("Error processing unposted offers: " + errorMessage);
|
||||
});
|
||||
|
||||
// register to process unposted offers when unlocked balance increases
|
||||
@ -300,7 +305,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
|
||||
if (lastUnlockedBalance == null || lastUnlockedBalance.compareTo(newUnlockedBalance) < 0) {
|
||||
processUnpostedOffers((transaction) -> {}, (errorMessage) -> {
|
||||
log.warn("Error processing unposted offers on new unlocked balance: " + errorMessage);
|
||||
log.warn("Error processing unposted offers on new unlocked balance: " + errorMessage); // TODO: popup to notify user that offer did not post
|
||||
});
|
||||
}
|
||||
lastUnlockedBalance = newUnlockedBalance;
|
||||
@ -485,16 +490,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
public void placeOffer(Offer offer,
|
||||
boolean useSavingsWallet,
|
||||
long triggerPrice,
|
||||
boolean splitOutput,
|
||||
TransactionResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
|
||||
|
||||
boolean autoSplit = false; // TODO: support in api
|
||||
|
||||
// TODO (woodser): validate offer
|
||||
|
||||
// create open offer
|
||||
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, autoSplit);
|
||||
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, splitOutput);
|
||||
|
||||
// process open offer to schedule or post
|
||||
processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> {
|
||||
@ -786,74 +788,201 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
private void processUnpostedOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
synchronized (xmrWalletService) {
|
||||
try {
|
||||
|
||||
// done processing if wallet not initialized
|
||||
if (xmrWalletService.getWallet() == null) {
|
||||
resultHandler.handleResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// get offer reserve amount
|
||||
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
|
||||
// handle sufficient available balance
|
||||
if (xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0) {
|
||||
|
||||
// split outputs if applicable
|
||||
boolean splitOutput = openOffer.isAutoSplit(); // TODO: determine if output needs split
|
||||
if (splitOutput) {
|
||||
throw new Error("Post offer with split output option not yet supported"); // TODO: support scheduling offer with split outputs
|
||||
// done processing if wallet not initialized
|
||||
if (xmrWalletService.getWallet() == null) {
|
||||
resultHandler.handleResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise sign and post offer
|
||||
else {
|
||||
signAndPostOffer(openOffer, offerReserveAmount, true, resultHandler, errorMessageHandler);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle unscheduled offer
|
||||
if (openOffer.getScheduledTxHashes() == null) {
|
||||
log.info("Scheduling offer " + openOffer.getId());
|
||||
|
||||
// check for sufficient balance - scheduled offers amount
|
||||
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) {
|
||||
throw new RuntimeException("Not enough money in Haveno wallet");
|
||||
}
|
||||
|
||||
// get locked txs
|
||||
List<MoneroTxWallet> lockedTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIsLocked(true));
|
||||
|
||||
// get earliest unscheduled txs with sufficient incoming amount
|
||||
List<String> scheduledTxHashes = new ArrayList<String>();
|
||||
BigInteger scheduledAmount = BigInteger.valueOf(0);
|
||||
for (MoneroTxWallet lockedTx : lockedTxs) {
|
||||
if (isTxScheduled(openOffers, lockedTx.getHash())) continue;
|
||||
if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue;
|
||||
scheduledTxHashes.add(lockedTx.getHash());
|
||||
for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) {
|
||||
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
|
||||
|
||||
// get offer reserve amount
|
||||
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
// handle split output offer
|
||||
if (openOffer.isSplitOutput()) {
|
||||
|
||||
// get tx to fund split output
|
||||
MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer);
|
||||
if (openOffer.getScheduledTxHashes() == null && splitOutputTx != null) {
|
||||
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
|
||||
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
|
||||
openOffer.setScheduledAmount(offerReserveAmount.toString());
|
||||
openOffer.setState(OpenOffer.State.SCHEDULED);
|
||||
}
|
||||
|
||||
// handle split output available
|
||||
if (splitOutputTx != null && !splitOutputTx.isLocked()) {
|
||||
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?
|
||||
|
||||
// schedule txs
|
||||
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
|
||||
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
|
||||
openOffer.setScheduledAmount(offerReserveAmount.toString());
|
||||
openOffer.setState(OpenOffer.State.SCHEDULED);
|
||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||
scheduleOfferWithEarliestTxs(openOffers, openOffer);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// handle sufficient balance
|
||||
boolean hasSufficientBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
|
||||
if (hasSufficientBalance) {
|
||||
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
||||
return;
|
||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||
scheduleOfferWithEarliestTxs(openOffers, openOffer);
|
||||
}
|
||||
if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break;
|
||||
}
|
||||
if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer");
|
||||
|
||||
// schedule txs
|
||||
openOffer.setScheduledTxHashes(scheduledTxHashes);
|
||||
openOffer.setScheduledAmount(scheduledAmount.toString());
|
||||
openOffer.setState(OpenOffer.State.SCHEDULED);
|
||||
|
||||
// handle result
|
||||
resultHandler.handleResult(null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
errorMessageHandler.handleErrorMessage(e.getMessage());
|
||||
}
|
||||
|
||||
// handle result
|
||||
resultHandler.handleResult(null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
errorMessageHandler.handleErrorMessage(e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public boolean hasAvailableOutput(BigInteger amount) {
|
||||
return findSplitOutputFundingTx(getOpenOffers(), amount, null) != null;
|
||||
}
|
||||
|
||||
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) {
|
||||
|
||||
// return split output tx if already assigned
|
||||
if (openOffer.getSplitOutputTxHash() != null) {
|
||||
return xmrWalletService.getWallet().getTx(openOffer.getSplitOutputTxHash());
|
||||
}
|
||||
|
||||
// find tx with exact output
|
||||
XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
return findSplitOutputFundingTx(openOffers, openOffer.getOffer().getReserveAmount(), addressEntry.getSubaddressIndex());
|
||||
}
|
||||
|
||||
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, BigInteger reserveAmount, Integer subaddressIndex) {
|
||||
List<MoneroTxWallet> fundingTxs = new ArrayList<>();
|
||||
MoneroTxWallet earliestUnscheduledTx = null;
|
||||
if (subaddressIndex != null) {
|
||||
|
||||
// return earliest tx with exact confirmed output to fund offer's subaddress if available
|
||||
fundingTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
|
||||
.setIsConfirmed(true)
|
||||
.setOutputQuery(new MoneroOutputQuery()
|
||||
.setAccountIndex(0)
|
||||
.setSubaddressIndex(subaddressIndex)
|
||||
.setAmount(reserveAmount)
|
||||
.setIsSpent(false)
|
||||
.setIsFrozen(false)));
|
||||
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
|
||||
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
|
||||
}
|
||||
|
||||
// cache all transactions including from pool
|
||||
List<MoneroTxWallet> allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
|
||||
if (subaddressIndex != 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)
|
||||
.setSubaddressIndex(subaddressIndex)
|
||||
.setAmount(reserveAmount)).size() > 0;
|
||||
if (hasExactTransfer) fundingTxs.add(tx);
|
||||
}
|
||||
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs);
|
||||
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
|
||||
}
|
||||
|
||||
// return earliest tx with exact confirmed output to any subaddress if available
|
||||
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;
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
if (isTxScheduled(openOffers, tx.getHash())) continue;
|
||||
if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx;
|
||||
}
|
||||
return earliestUnscheduledTx;
|
||||
}
|
||||
|
||||
private void scheduleOfferWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) {
|
||||
|
||||
// check for sufficient balance - scheduled offers amount
|
||||
BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) {
|
||||
throw new RuntimeException("Not enough money in Haveno wallet");
|
||||
}
|
||||
|
||||
// get locked txs
|
||||
List<MoneroTxWallet> lockedTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIsLocked(true));
|
||||
|
||||
// get earliest unscheduled txs with sufficient incoming amount
|
||||
List<String> scheduledTxHashes = new ArrayList<String>();
|
||||
BigInteger scheduledAmount = BigInteger.valueOf(0);
|
||||
for (MoneroTxWallet lockedTx : lockedTxs) {
|
||||
if (isTxScheduled(openOffers, lockedTx.getHash())) continue;
|
||||
if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue;
|
||||
scheduledTxHashes.add(lockedTx.getHash());
|
||||
for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) {
|
||||
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
|
||||
}
|
||||
if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break;
|
||||
}
|
||||
if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer");
|
||||
|
||||
// schedule txs
|
||||
openOffer.setScheduledTxHashes(scheduledTxHashes);
|
||||
openOffer.setScheduledAmount(scheduledAmount.toString());
|
||||
openOffer.setState(OpenOffer.State.SCHEDULED);
|
||||
}
|
||||
|
||||
private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) {
|
||||
BigInteger reserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
String fundingSubaddress = xmrWalletService.getAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getAddressString();
|
||||
return xmrWalletService.getWallet().createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setAddress(fundingSubaddress)
|
||||
.setAmount(reserveAmount)
|
||||
.setRelay(true));
|
||||
}
|
||||
|
||||
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {
|
||||
BigInteger scheduledAmount = BigInteger.valueOf(0);
|
||||
for (OpenOffer openOffer : openOffers) {
|
||||
@ -861,8 +990,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
if (openOffer.getScheduledTxHashes() == null) continue;
|
||||
List<MoneroTxWallet> fundingTxs = xmrWalletService.getWallet().getTxs(openOffer.getScheduledTxHashes());
|
||||
for (MoneroTxWallet fundingTx : fundingTxs) {
|
||||
for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) {
|
||||
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
|
||||
if (fundingTx.getIncomingTransfers() != null) {
|
||||
for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) {
|
||||
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -881,14 +1012,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
|
||||
private void signAndPostOffer(OpenOffer openOffer,
|
||||
BigInteger offerReserveAmount,
|
||||
boolean useSavingsWallet, // TODO: remove this
|
||||
boolean useSavingsWallet, // TODO: remove this?
|
||||
TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.info("Signing and posting offer " + openOffer.getId());
|
||||
|
||||
// create model
|
||||
PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(),
|
||||
offerReserveAmount,
|
||||
PlaceOfferModel model = new PlaceOfferModel(openOffer,
|
||||
openOffer.getOffer().getReserveAmount(),
|
||||
useSavingsWallet,
|
||||
p2PService,
|
||||
btcWalletService,
|
||||
@ -914,6 +1044,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
// set offer state
|
||||
openOffer.setState(OpenOffer.State.AVAILABLE);
|
||||
openOffer.setScheduledTxHashes(null);
|
||||
openOffer.setScheduledAmount(null);
|
||||
requestPersistence();
|
||||
|
||||
resultHandler.handleResult(transaction);
|
||||
@ -1397,7 +1529,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
synchronized (openOffers) {
|
||||
contained = openOffers.contains(openOffer);
|
||||
}
|
||||
if (contained && !openOffer.isDeactivated() && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) {
|
||||
if (contained && !openOffer.isDeactivated() && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && !openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) {
|
||||
// TODO It is not clear yet if it is better for the node and the network to send out all add offer
|
||||
// messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have
|
||||
// some significant impact to user experience and the network
|
||||
|
@ -21,8 +21,8 @@ import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.taskrunner.Model;
|
||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
import haveno.core.filter.FilterManager;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferBookService;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.offer.messages.SignOfferResponse;
|
||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import haveno.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
@ -44,7 +44,7 @@ import java.math.BigInteger;
|
||||
@Getter
|
||||
public class PlaceOfferModel implements Model {
|
||||
// Immutable
|
||||
private final Offer offer;
|
||||
private final OpenOffer openOffer;
|
||||
private final BigInteger reservedFundsForOffer;
|
||||
private final boolean useSavingsWallet;
|
||||
private final P2PService p2PService;
|
||||
@ -72,7 +72,7 @@ public class PlaceOfferModel implements Model {
|
||||
@Setter
|
||||
private SignOfferResponse signOfferResponse;
|
||||
|
||||
public PlaceOfferModel(Offer offer,
|
||||
public PlaceOfferModel(OpenOffer openOffer,
|
||||
BigInteger reservedFundsForOffer,
|
||||
boolean useSavingsWallet,
|
||||
P2PService p2PService,
|
||||
@ -87,7 +87,7 @@ public class PlaceOfferModel implements Model {
|
||||
KeyRing keyRing,
|
||||
FilterManager filterManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService) {
|
||||
this.offer = offer;
|
||||
this.openOffer = openOffer;
|
||||
this.reservedFundsForOffer = reservedFundsForOffer;
|
||||
this.useSavingsWallet = useSavingsWallet;
|
||||
this.p2PService = p2PService;
|
||||
|
@ -84,17 +84,17 @@ public class PlaceOfferProtocol {
|
||||
|
||||
// TODO (woodser): switch to fluent
|
||||
public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) {
|
||||
log.debug("handleSignOfferResponse() " + model.getOffer().getId());
|
||||
log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId());
|
||||
model.setSignOfferResponse(response);
|
||||
|
||||
if (!model.getOffer().getOfferPayload().getArbitratorSigner().equals(sender)) {
|
||||
if (!model.getOpenOffer().getOffer().getOfferPayload().getArbitratorSigner().equals(sender)) {
|
||||
log.warn("Ignoring sign offer response from different sender");
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if timer already stopped
|
||||
if (timeoutTimer == null) {
|
||||
log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOffer().getId());
|
||||
log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOpenOffer().getOffer().getId());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ public class PlaceOfferProtocol {
|
||||
},
|
||||
(errorMessage) -> {
|
||||
if (model.isOfferAddedToOfferBook()) {
|
||||
model.getOfferBookService().removeOffer(model.getOffer().getOfferPayload(),
|
||||
model.getOfferBookService().removeOffer(model.getOpenOffer().getOffer().getOfferPayload(),
|
||||
() -> {
|
||||
model.setOfferAddedToOfferBook(false);
|
||||
log.debug("OfferPayload removed from offer book.");
|
||||
@ -141,7 +141,7 @@ public class PlaceOfferProtocol {
|
||||
if (timeoutTimer != null) {
|
||||
log.error(errorMessage);
|
||||
stopTimeoutTimer();
|
||||
model.getOffer().setErrorMessage(errorMessage);
|
||||
model.getOpenOffer().getOffer().setErrorMessage(errorMessage);
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
}
|
||||
}
|
||||
|
@ -34,20 +34,20 @@ public class AddToOfferBook extends Task<PlaceOfferModel> {
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOffer().getId());
|
||||
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId());
|
||||
model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
|
||||
() -> {
|
||||
model.setOfferAddedToOfferBook(true);
|
||||
complete();
|
||||
},
|
||||
errorMessage -> {
|
||||
model.getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
|
||||
model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
|
||||
"Please check your network connection and try again.");
|
||||
|
||||
failed(errorMessage);
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
model.getOffer().setErrorMessage("An error occurred.\n" +
|
||||
model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" +
|
||||
"Error message:\n"
|
||||
+ t.getMessage());
|
||||
|
||||
|
@ -40,7 +40,7 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
Offer offer = model.getOffer();
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
@ -33,7 +33,7 @@ public class MakerProcessSignOfferResponse extends Task<PlaceOfferModel> {
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
Offer offer = model.getOffer();
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
@ -46,7 +46,7 @@ public class MakerProcessSignOfferResponse extends Task<PlaceOfferModel> {
|
||||
}
|
||||
|
||||
// set arbitrator signature for maker's offer
|
||||
model.getOffer().getOfferPayload().setArbitratorSignature(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature());
|
||||
offer.getOfferPayload().setArbitratorSignature(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature());
|
||||
offer.setState(Offer.State.AVAILABLE);
|
||||
complete();
|
||||
} catch (Exception e) {
|
||||
|
@ -41,7 +41,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
@Override
|
||||
protected void run() {
|
||||
|
||||
Offer offer = model.getOffer();
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
|
||||
try {
|
||||
runInterceptHook();
|
||||
@ -53,12 +53,15 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
BigInteger makerFee = offer.getMakerFee();
|
||||
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
||||
String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).getAddressString();
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress);
|
||||
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;
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex);
|
||||
|
||||
// check for error in case creating reserve tx exceeded timeout
|
||||
// TODO: better way?
|
||||
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).isPresent()) {
|
||||
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) {
|
||||
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
|
||||
}
|
||||
|
||||
|
@ -55,14 +55,14 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
Offer offer = model.getOffer();
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// create request for arbitrator to sign offer
|
||||
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString();
|
||||
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
||||
SignOfferRequest request = new SignOfferRequest(
|
||||
model.getOffer().getId(),
|
||||
offer.getId(),
|
||||
P2PService.getMyNodeAddress(),
|
||||
model.getKeyRing().getPubKeyRing(),
|
||||
model.getUser().getAccountId(),
|
||||
@ -113,8 +113,8 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||
if (!ackMessage.getSourceUid().equals(request.getUid())) return;
|
||||
if (ackMessage.isSuccess()) {
|
||||
model.getP2PService().removeDecryptedDirectMessageListener(this);
|
||||
model.getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
|
||||
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
|
||||
model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
|
||||
model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
|
||||
resultHandler.handleResult();
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
|
||||
@ -127,7 +127,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||
sendSignOfferRequest(request, arbitratorNodeAddress, new SendDirectMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at arbitrator: offerId={}", request.getClass().getSimpleName(), model.getOffer().getId());
|
||||
log.info("{} arrived at arbitrator: offerId={}", request.getClass().getSimpleName(), model.getOpenOffer().getId());
|
||||
}
|
||||
|
||||
// if unavailable, try alternative arbitrator
|
||||
|
@ -37,7 +37,7 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
Offer offer = model.getOffer();
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
|
@ -123,6 +123,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
@Getter
|
||||
private final CoreNotificationService notificationService;
|
||||
private final OfferBookService offerBookService;
|
||||
@Getter
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private final ClosedTradableManager closedTradableManager;
|
||||
private final FailedTradesManager failedTradesManager;
|
||||
@ -1093,8 +1094,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
if (entries == null)
|
||||
return false;
|
||||
|
||||
xmrWalletService.recoverAddressEntry(trade.getId(), entries.first,
|
||||
XmrAddressEntry.Context.MULTI_SIG);
|
||||
xmrWalletService.recoverAddressEntry(trade.getId(), entries.second,
|
||||
XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||
return true;
|
||||
|
@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
|
||||
trade.getSelf().getReserveTxHash(),
|
||||
trade.getSelf().getReserveTxHex(),
|
||||
trade.getSelf().getReserveTxKey(),
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString(),
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
||||
null);
|
||||
|
||||
// send request to arbitrator
|
||||
|
@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@ -71,9 +72,20 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||
return;
|
||||
}
|
||||
|
||||
// create deposit tx and freeze inputs
|
||||
// initialize progress steps
|
||||
trade.addInitProgressStep();
|
||||
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade);
|
||||
|
||||
// create deposit tx and freeze inputs
|
||||
Integer subaddressIndex = null;
|
||||
BigInteger exactOutputAmount = null;
|
||||
if (trade instanceof MakerTrade) {
|
||||
boolean isSplitOutputOffer = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isSplitOutput();
|
||||
if (isSplitOutputOffer) {
|
||||
exactOutputAmount = trade.getOffer().getReserveAmount();
|
||||
subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
|
||||
}
|
||||
}
|
||||
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, exactOutputAmount, subaddressIndex);
|
||||
|
||||
// collect reserved key images
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
|
@ -44,7 +44,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress, null, null);
|
||||
|
||||
// collect reserved key images
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
|
@ -497,6 +497,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public void setSplitOfferOutput(boolean splitOfferOutput) {
|
||||
prefPayload.setSplitOfferOutput(splitOfferOutput);
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook) {
|
||||
prefPayload.setShowOwnOffersInOfferBook(showOwnOffersInOfferBook);
|
||||
requestPersistence();
|
||||
@ -797,6 +802,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
return prefPayload.isUseTorForMonero();
|
||||
}
|
||||
|
||||
public boolean getSplitOfferOutput() {
|
||||
return prefPayload.isSplitOfferOutput();
|
||||
}
|
||||
|
||||
public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) {
|
||||
double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ?
|
||||
prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent();
|
||||
@ -861,6 +870,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
|
||||
void setUseTorForMonero(boolean useTorForMonero);
|
||||
|
||||
void setSplitOfferOutput(boolean splitOfferOutput);
|
||||
|
||||
void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook);
|
||||
|
||||
void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent);
|
||||
|
@ -59,6 +59,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
private Map<String, Boolean> dontShowAgainMap = new HashMap<>();
|
||||
private boolean tacAccepted;
|
||||
private boolean useTorForMonero = true;
|
||||
private boolean splitOfferOutput = false;
|
||||
private boolean showOwnOffersInOfferBook = true;
|
||||
@Nullable
|
||||
private TradeCurrency preferredTradeCurrency;
|
||||
@ -161,6 +162,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
.putAllDontShowAgainMap(dontShowAgainMap)
|
||||
.setTacAccepted(tacAccepted)
|
||||
.setUseTorForMonero(useTorForMonero)
|
||||
.setSplitOfferOutput(splitOfferOutput)
|
||||
.setShowOwnOffersInOfferBook(showOwnOffersInOfferBook)
|
||||
.setWithdrawalTxFeeInVbytes(withdrawalTxFeeInVbytes)
|
||||
.setUseCustomWithdrawalTxFee(useCustomWithdrawalTxFee)
|
||||
@ -243,6 +245,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
Maps.newHashMap(proto.getDontShowAgainMapMap()),
|
||||
proto.getTacAccepted(),
|
||||
proto.getUseTorForMonero(),
|
||||
proto.getSplitOfferOutput(),
|
||||
proto.getShowOwnOffersInOfferBook(),
|
||||
proto.hasPreferredTradeCurrency() ? TradeCurrency.fromProto(proto.getPreferredTradeCurrency()) : null,
|
||||
proto.getWithdrawalTxFeeInVbytes(),
|
||||
|
@ -39,12 +39,10 @@ import java.util.Optional;
|
||||
public final class XmrAddressEntry implements PersistablePayload {
|
||||
public enum Context {
|
||||
ARBITRATOR,
|
||||
BASE_ADDRESS,
|
||||
AVAILABLE,
|
||||
OFFER_FUNDING,
|
||||
RESERVED_FOR_TRADE,
|
||||
MULTI_SIG,
|
||||
TRADE_PAYOUT,
|
||||
BASE_ADDRESS
|
||||
TRADE_PAYOUT
|
||||
}
|
||||
|
||||
// keyPair can be null in case the object is created from deserialization as it is transient.
|
||||
@ -120,11 +118,11 @@ public final class XmrAddressEntry implements PersistablePayload {
|
||||
}
|
||||
|
||||
public boolean isOpenOffer() {
|
||||
return context == Context.OFFER_FUNDING || context == Context.RESERVED_FOR_TRADE;
|
||||
return context == Context.OFFER_FUNDING;
|
||||
}
|
||||
|
||||
public boolean isTrade() {
|
||||
return context == Context.MULTI_SIG || context == Context.TRADE_PAYOUT;
|
||||
return context == Context.TRADE_PAYOUT;
|
||||
}
|
||||
|
||||
public boolean isTradable() {
|
||||
|
@ -44,7 +44,6 @@ import monero.wallet.model.MoneroOutputQuery;
|
||||
import monero.wallet.model.MoneroOutputWallet;
|
||||
import monero.wallet.model.MoneroSubaddress;
|
||||
import monero.wallet.model.MoneroSyncResult;
|
||||
import monero.wallet.model.MoneroTransferQuery;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroTxQuery;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
@ -327,27 +326,38 @@ public class XmrWalletService {
|
||||
* Create the reserve tx and freeze its inputs. The full amount is returned
|
||||
* to the sender's payout address less the trade fee.
|
||||
*
|
||||
* @param returnAddress return address for reserved funds
|
||||
* @param tradeFee trade fee
|
||||
* @param sendAmount amount to give peer
|
||||
* @param securityDeposit security deposit amount
|
||||
* @param returnAddress return address for reserved funds
|
||||
* @param exactOutputAmount exact output amount to spend (optional)
|
||||
* @param subaddressIndex preferred source subaddress to spend from (optional)
|
||||
* @return a transaction to reserve a trade
|
||||
*/
|
||||
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress) {
|
||||
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, BigInteger exactOutputAmount, Integer subaddressIndex) {
|
||||
log.info("Creating reserve tx with return address={}", returnAddress);
|
||||
long time = System.currentTimeMillis();
|
||||
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true);
|
||||
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
|
||||
return reserveTx;
|
||||
try {
|
||||
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true, exactOutputAmount, subaddressIndex);
|
||||
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
|
||||
return reserveTx;
|
||||
} catch (Exception e) {
|
||||
|
||||
// retry creating reserve tx using funds outside subaddress
|
||||
if (subaddressIndex != null) return createReserveTx(tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, null);
|
||||
else throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**s
|
||||
* Create the multisig deposit tx and freeze its inputs.
|
||||
*
|
||||
* @param trade the trade to create a deposit tx from
|
||||
* @param exactOutputAmount exact output amount to spend (optional)
|
||||
* @param subaddressIndex preferred source subaddress to spend from (optional)
|
||||
* @return MoneroTxWallet the multisig deposit tx
|
||||
*/
|
||||
public MoneroTxWallet createDepositTx(Trade trade) {
|
||||
public MoneroTxWallet createDepositTx(Trade trade, BigInteger exactOutputAmount, Integer subaddressIndex) {
|
||||
Offer offer = trade.getProcessModel().getOffer();
|
||||
String multisigAddress = trade.getProcessModel().getMultisigAddress();
|
||||
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee();
|
||||
@ -365,33 +375,60 @@ public class XmrWalletService {
|
||||
|
||||
log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getId(), multisigAddress);
|
||||
long time = System.currentTimeMillis();
|
||||
MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false);
|
||||
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time);
|
||||
return tradeTx;
|
||||
try {
|
||||
MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false, exactOutputAmount, subaddressIndex);
|
||||
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time);
|
||||
return tradeTx;
|
||||
} catch (Exception e) {
|
||||
|
||||
// retry creating deposit tx using funds outside subaddress
|
||||
if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx) {
|
||||
private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, BigInteger exactOutputAmount, Integer subaddressIndex) {
|
||||
MoneroWallet wallet = getWallet();
|
||||
synchronized (wallet) {
|
||||
|
||||
// binary search to maximize security deposit and minimize potential dust
|
||||
// TODO: binary search is hacky and slow over TOR connections, replace with destination paying tx fee
|
||||
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
|
||||
for (int i = 0; i < 10; i++) {
|
||||
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();
|
||||
BigInteger amount = sendAmount.add(isReserveTx ? tradeFee : appliedSecurityDeposit);
|
||||
tradeTx = wallet.createTx(new MoneroTxConfig()
|
||||
MoneroTxWallet testTx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setSubaddressIndices(subaddressIndex == null ? null : Arrays.asList(subaddressIndex)) // TODO monero-java: MoneroTxConfig.setSubadddressIndex(int) causes NPE with null subaddress, could setSubaddressIndices(null) as convenience
|
||||
.addDestination(HavenoUtils.getTradeFeeAddress(), isReserveTx ? appliedSecurityDeposit : tradeFee) // reserve tx charges security deposit if published
|
||||
.addDestination(address, amount));
|
||||
|
||||
// assert exact input if expected
|
||||
if (exactOutputAmount == null) {
|
||||
tradeTx = testTx;
|
||||
} else {
|
||||
BigInteger inputSum = BigInteger.valueOf(0);
|
||||
for (MoneroOutputWallet txInput : testTx.getInputsWallet()) {
|
||||
MoneroOutputWallet input = wallet.getOutputs(new MoneroOutputQuery().setKeyImage(txInput.getKeyImage())).get(0);
|
||||
inputSum = inputSum.add(input.getAmount());
|
||||
}
|
||||
if (inputSum.compareTo(exactOutputAmount) > 0) throw new RuntimeException("Spending too much since input sum is greater than output amount"); // continues binary search with less security deposit
|
||||
else if (inputSum.equals(exactOutputAmount) && testTx.getInputs().size() == 1) tradeTx = testTx;
|
||||
}
|
||||
appliedTolerance -= searchDiff; // apply less tolerance to increase security deposit
|
||||
if (appliedTolerance < 0.0) break; // can send full security deposit
|
||||
} catch (MoneroError e) {
|
||||
} catch (Exception e) {
|
||||
appliedTolerance += searchDiff; // apply more tolerance to decrease security deposit
|
||||
if (appliedTolerance > 1.0) throw e; // not enough money
|
||||
if (appliedTolerance > 1.0) {
|
||||
if (tradeTx == null) throw e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
searchDiff /= 2;
|
||||
}
|
||||
@ -455,22 +492,21 @@ public class XmrWalletService {
|
||||
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
|
||||
|
||||
// verify transfer proof to fee address
|
||||
String feeAddress = HavenoUtils.getTradeFeeAddress();
|
||||
MoneroCheckTx feeCheck = wallet.checkTxKey(txHash, txKey, feeAddress);
|
||||
if (!feeCheck.isGood()) throw new RuntimeException("Invalid proof of trade fee");
|
||||
MoneroCheckTx tradeFeeCheck = wallet.checkTxKey(txHash, txKey, HavenoUtils.getTradeFeeAddress());
|
||||
if (!tradeFeeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address");
|
||||
|
||||
// verify transfer proof to return address
|
||||
MoneroCheckTx returnCheck = wallet.checkTxKey(txHash, txKey, address);
|
||||
if (!returnCheck.isGood()) throw new RuntimeException("Invalid proof of return funds");
|
||||
// verify transfer proof to address
|
||||
MoneroCheckTx transferCheck = wallet.checkTxKey(txHash, txKey, address);
|
||||
if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address");
|
||||
|
||||
// collect actual trade fee, send amount, and security deposit
|
||||
BigInteger actualTradeFee = isReserveTx ? returnCheck.getReceivedAmount().subtract(sendAmount) : feeCheck.getReceivedAmount();
|
||||
actualSecurityDeposit = isReserveTx ? feeCheck.getReceivedAmount() : returnCheck.getReceivedAmount().subtract(sendAmount);
|
||||
BigInteger actualSendAmount = returnCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
|
||||
BigInteger actualTradeFee = isReserveTx ? transferCheck.getReceivedAmount().subtract(sendAmount) : tradeFeeCheck.getReceivedAmount();
|
||||
actualSecurityDeposit = isReserveTx ? tradeFeeCheck.getReceivedAmount() : transferCheck.getReceivedAmount().subtract(sendAmount);
|
||||
BigInteger actualSendAmount = transferCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
|
||||
|
||||
// verify trade fee
|
||||
if (!tradeFee.equals(actualTradeFee)) {
|
||||
throw new RuntimeException("Trade fee is incorrect amount, expected=" + tradeFee + ", actual=" + actualTradeFee + ", return address check=" + JsonUtils.serialize(returnCheck) + ", fee address check=" + JsonUtils.serialize(feeCheck));
|
||||
throw new RuntimeException("Trade fee is incorrect amount, expected=" + tradeFee + ", actual=" + actualTradeFee + ", transfer address check=" + JsonUtils.serialize(transferCheck) + ", trade fee address check=" + JsonUtils.serialize(tradeFeeCheck));
|
||||
}
|
||||
|
||||
// verify sufficient security deposit
|
||||
@ -478,9 +514,10 @@ public class XmrWalletService {
|
||||
if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit);
|
||||
|
||||
// verify deposit amount + miner fee within dust tolerance
|
||||
BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger());
|
||||
BigInteger actualDepositAndFee = actualSendAmount.add(actualSecurityDeposit).add(tx.getFee());
|
||||
if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee);
|
||||
//BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); // TODO: improve when destination pays fee
|
||||
BigInteger minDeposit = sendAmount.add(minSecurityDeposit);
|
||||
BigInteger actualDeposit = actualSendAmount.add(actualSecurityDeposit);
|
||||
if (actualDeposit.compareTo(minDeposit) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDeposit + " but was " + actualDeposit);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage());
|
||||
throw e;
|
||||
@ -928,11 +965,9 @@ public class XmrWalletService {
|
||||
public synchronized void resetAddressEntriesForOpenOffer(String offerId) {
|
||||
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
||||
}
|
||||
|
||||
public synchronized void resetAddressEntriesForPendingTrade(String offerId) {
|
||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.MULTI_SIG);
|
||||
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases
|
||||
// where a user cannot send the funds
|
||||
// to an external wallet directly in the last step of the trade, but the funds
|
||||
@ -951,20 +986,23 @@ public class XmrWalletService {
|
||||
return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny();
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntries() {
|
||||
return getAddressEntryListAsImmutableList().stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAvailableAddressEntries() {
|
||||
return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntriesForOpenOffer() {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.OFFER_FUNDING == addressEntry.getContext() ||
|
||||
XmrAddressEntry.Context.RESERVED_FOR_TRADE == addressEntry.getContext())
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.OFFER_FUNDING == addressEntry.getContext())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntriesForTrade() {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.MULTI_SIG == addressEntry.getContext() || XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext())
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -1015,7 +1053,7 @@ public class XmrWalletService {
|
||||
if (incomingTxs == null) incomingTxs = getTxsWithIncomingOutputs(subaddressIndex);
|
||||
int numUnspentOutputs = 0;
|
||||
for (MoneroTxWallet tx : incomingTxs) {
|
||||
if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue;
|
||||
//if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used
|
||||
numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs
|
||||
}
|
||||
return numUnspentOutputs;
|
||||
@ -1026,11 +1064,11 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public List<MoneroTxWallet> getTxsWithIncomingOutputs(Integer subaddressIndex) {
|
||||
List<MoneroTxWallet> txs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
return getTxsWithIncomingOutputs(subaddressIndex, txs);
|
||||
return getTxsWithIncomingOutputs(subaddressIndex, null);
|
||||
}
|
||||
|
||||
public static List<MoneroTxWallet> getTxsWithIncomingOutputs(Integer subaddressIndex, List<MoneroTxWallet> txs) {
|
||||
public List<MoneroTxWallet> getTxsWithIncomingOutputs(Integer subaddressIndex, List<MoneroTxWallet> txs) {
|
||||
if (txs == null) txs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
List<MoneroTxWallet> incomingTxs = new ArrayList<>();
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
boolean isIncoming = false;
|
||||
@ -1078,7 +1116,7 @@ public class XmrWalletService {
|
||||
public Stream<XmrAddressEntry> getAddressEntriesForAvailableBalanceStream() {
|
||||
Stream<XmrAddressEntry> availableAndPayout = Stream.concat(getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream(), getFundedAvailableAddressEntries().stream());
|
||||
Stream<XmrAddressEntry> available = Stream.concat(availableAndPayout, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream());
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream());
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent()));
|
||||
return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.valueOf(0)) > 0);
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ shared.tradeCurrency=Trade currency
|
||||
shared.offerType=Offer type
|
||||
shared.details=Details
|
||||
shared.address=Address
|
||||
shared.balanceWithCur=Balance in {0}
|
||||
shared.balanceWithCur=Available balance in {0}
|
||||
shared.utxo=Unspent transaction output
|
||||
shared.txId=Transaction ID
|
||||
shared.confirmations=Confirmations
|
||||
@ -196,6 +196,7 @@ shared.total=Total
|
||||
shared.totalsNeeded=Funds needed
|
||||
shared.tradeWalletAddress=Trade wallet address
|
||||
shared.tradeWalletBalance=Trade wallet balance
|
||||
shared.reserveExactAmount=Reserve exact amount for offer. Splits wallet funds if necessary, requiring a mining fee and 10 confirmations (~20 minutes) before the offer is available.
|
||||
shared.makerTxFee=Maker: {0}
|
||||
shared.takerTxFee=Taker: {0}
|
||||
shared.iConfirm=I confirm
|
||||
|
@ -158,6 +158,7 @@ class GrpcOffersService extends OffersImplBase {
|
||||
req.getMinAmount(),
|
||||
req.getBuyerSecurityDepositPct(),
|
||||
req.getTriggerPrice(),
|
||||
req.getSplitOutput(),
|
||||
req.getPaymentAccountId(),
|
||||
offer -> {
|
||||
// This result handling consumer's accept operation will return
|
||||
|
@ -95,7 +95,7 @@ class DepositListItem {
|
||||
}
|
||||
|
||||
private void updateUsage(int subaddressIndex, List<MoneroTxWallet> cachedTxs) {
|
||||
numTxsWithOutputs = XmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs).size();
|
||||
numTxsWithOutputs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), cachedTxs).size();
|
||||
usage = subaddressIndex == 0 ? "Base address" : numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs);
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ class DepositListItem {
|
||||
private MoneroTxWallet getTxWithFewestConfirmations(List<MoneroTxWallet> allIncomingTxs) {
|
||||
|
||||
// get txs with incoming outputs to subaddress index
|
||||
List<MoneroTxWallet> txs = XmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), allIncomingTxs);
|
||||
List<MoneroTxWallet> txs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex(), allIncomingTxs);
|
||||
|
||||
// get tx with fewest confirmations
|
||||
MoneroTxWallet highestTx = null;
|
||||
|
@ -312,9 +312,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
|
||||
txsWithIncomingOutputs = xmrWalletService.getTxsWithIncomingOutputs();
|
||||
|
||||
// add available address entries and base address
|
||||
xmrWalletService.getAvailableAddressEntries()
|
||||
.forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs)));
|
||||
xmrWalletService.getAddressEntries(XmrAddressEntry.Context.BASE_ADDRESS)
|
||||
xmrWalletService.getAddressEntries()
|
||||
.forEach(e -> observableList.add(new DepositListItem(e, xmrWalletService, formatter, txsWithIncomingOutputs)));
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
private final Predicate<ObjectProperty<Volume>> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero();
|
||||
@Getter
|
||||
protected long triggerPrice;
|
||||
@Getter
|
||||
protected boolean splitOutput;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -165,6 +167,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
shortOfferId = Utilities.getShortId(offerId);
|
||||
addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
|
||||
splitOutput = preferences.getSplitOfferOutput();
|
||||
|
||||
useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice());
|
||||
buyerSecurityDepositPct.set(Restrictions.getMinBuyerSecurityDepositAsPercent());
|
||||
|
||||
@ -295,6 +299,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
openOfferManager.placeOffer(offer,
|
||||
useSavingsWallet,
|
||||
triggerPrice,
|
||||
splitOutput,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
}
|
||||
@ -459,6 +464,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAvailableSplitOutput() {
|
||||
BigInteger reserveAmount = totalToPay.get();
|
||||
return openOfferManager.hasAvailableOutput(reserveAmount);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -672,6 +682,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
this.triggerPrice = triggerPrice;
|
||||
}
|
||||
|
||||
public void setSplitOutput(boolean splitOutput) {
|
||||
this.splitOutput = splitOutput;
|
||||
}
|
||||
|
||||
public boolean isUsingRoundedAtmCashAccount() {
|
||||
return PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId());
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
|
||||
import haveno.desktop.main.overlays.windows.QRCodeWindow;
|
||||
import haveno.desktop.main.portfolio.PortfolioView;
|
||||
import haveno.desktop.main.portfolio.openoffer.OpenOffersView;
|
||||
import haveno.desktop.util.FormBuilder;
|
||||
import haveno.desktop.util.GUIUtil;
|
||||
import haveno.desktop.util.Layout;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
@ -70,6 +71,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@ -134,6 +136,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
private TextField currencyTextField;
|
||||
private AddressTextField addressTextField;
|
||||
private BalanceTextField balanceTextField;
|
||||
private CheckBox splitOutputCheckbox;
|
||||
private FundsTextField totalToPayTextField;
|
||||
private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel,
|
||||
waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel,
|
||||
@ -418,6 +421,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
qrCodeImageView.setVisible(true);
|
||||
balanceTextField.setVisible(true);
|
||||
cancelButton2.setVisible(true);
|
||||
splitOutputCheckbox.setVisible(true);
|
||||
}
|
||||
|
||||
private void updateOfferElementsStyle() {
|
||||
@ -1088,6 +1092,21 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
Res.get("shared.tradeWalletBalance"));
|
||||
balanceTextField.setVisible(false);
|
||||
|
||||
splitOutputCheckbox = FormBuilder.addLabelCheckBox(gridPane, ++gridRow,
|
||||
Res.get("shared.reserveExactAmount"));
|
||||
|
||||
GridPane.setHalignment(splitOutputCheckbox, HPos.LEFT);
|
||||
|
||||
splitOutputCheckbox.setVisible(false);
|
||||
splitOutputCheckbox.setSelected(preferences.getSplitOfferOutput());
|
||||
splitOutputCheckbox.setOnAction(event -> {
|
||||
boolean selected = splitOutputCheckbox.isSelected();
|
||||
if (selected != preferences.getSplitOfferOutput()) {
|
||||
preferences.setSplitOfferOutput(selected);
|
||||
model.dataModel.setSplitOutput(selected);
|
||||
}
|
||||
});
|
||||
|
||||
fundingHBox = new HBox();
|
||||
fundingHBox.setVisible(false);
|
||||
fundingHBox.setManaged(false);
|
||||
|
@ -115,6 +115,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||
// If we would change the price representation in the domain we would not be backward compatible
|
||||
public final StringProperty price = new SimpleStringProperty();
|
||||
public final StringProperty triggerPrice = new SimpleStringProperty("");
|
||||
public final BooleanProperty splitOutput = new SimpleBooleanProperty(true);
|
||||
final StringProperty tradeFee = new SimpleStringProperty();
|
||||
final StringProperty tradeFeeInXmrWithFiat = new SimpleStringProperty();
|
||||
final StringProperty tradeFeeCurrencyCode = new SimpleStringProperty();
|
||||
@ -778,6 +779,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||
}
|
||||
}
|
||||
|
||||
public void onSplitOutputCheckboxChanged() {
|
||||
dataModel.setSplitOutput(splitOutput.get());
|
||||
}
|
||||
|
||||
void onFixPriceToggleChange(boolean fixedPriceSelected) {
|
||||
inputIsMarketBasedPrice = !fixedPriceSelected;
|
||||
updateButtonDisableState();
|
||||
|
@ -82,6 +82,7 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
|
||||
triggerPrice.set("");
|
||||
}
|
||||
onTriggerPriceTextFieldChanged();
|
||||
onSplitOutputCheckboxChanged();
|
||||
}
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
|
@ -498,7 +498,8 @@ message PostOfferRequest {
|
||||
uint64 min_amount = 7 [jstype = JS_STRING];
|
||||
double buyer_security_deposit_pct = 8;
|
||||
string trigger_price = 9;
|
||||
string payment_account_id = 10;
|
||||
bool split_output = 10;
|
||||
string payment_account_id = 11;
|
||||
}
|
||||
|
||||
message PostOfferReply {
|
||||
|
@ -1303,12 +1303,10 @@ message XmrAddressEntry {
|
||||
enum Context {
|
||||
PB_ERROR = 0;
|
||||
ARBITRATOR = 1;
|
||||
AVAILABLE = 2;
|
||||
OFFER_FUNDING = 3;
|
||||
RESERVED_FOR_TRADE = 4;
|
||||
MULTI_SIG = 5;
|
||||
TRADE_PAYOUT = 6;
|
||||
BASE_ADDRESS = 7;
|
||||
BASE_ADDRESS = 2;
|
||||
AVAILABLE = 3;
|
||||
OFFER_FUNDING = 4;
|
||||
TRADE_PAYOUT = 5;
|
||||
}
|
||||
|
||||
int32 subaddress_index = 7;
|
||||
@ -1379,12 +1377,13 @@ message OpenOffer {
|
||||
Offer offer = 1;
|
||||
State state = 2;
|
||||
int64 trigger_price = 3;
|
||||
bool auto_split = 4;
|
||||
bool split_output = 4;
|
||||
repeated string scheduled_tx_hashes = 5;
|
||||
string scheduled_amount = 6; // BigInteger
|
||||
string reserve_tx_hash = 7;
|
||||
string reserve_tx_hex = 8;
|
||||
string reserve_tx_key = 9;
|
||||
string split_output_tx_hash = 7;
|
||||
string reserve_tx_hash = 8;
|
||||
string reserve_tx_hex = 9;
|
||||
string reserve_tx_key = 10;
|
||||
}
|
||||
|
||||
message Tradable {
|
||||
@ -1711,6 +1710,7 @@ message PreferencesPayload {
|
||||
int32 clear_data_after_days = 59;
|
||||
string buy_screen_crypto_currency_code = 60;
|
||||
string sell_screen_crypto_currency_code = 61;
|
||||
bool split_offer_output = 62;
|
||||
}
|
||||
|
||||
message AutoConfirmSettings {
|
||||
|
Loading…
Reference in New Issue
Block a user