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