support scheduling offers with locked funds

This commit is contained in:
woodser 2022-05-15 13:58:27 -04:00
parent 2da77de41b
commit fa15612586
25 changed files with 386 additions and 201 deletions

View file

@ -44,7 +44,7 @@ import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OpenOffer.State.AVAILABLE;
@ -168,7 +168,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
sleep(5000);
continue;
} else {
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG)
.setPhase(PAYMENT_SENT)
.setFiatSent(true);

View file

@ -46,7 +46,7 @@ import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferPayload.Direction.SELL;
import static protobuf.OpenOffer.State.AVAILABLE;
@ -220,7 +220,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
sleep(3000);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED)
.setPayoutPublished(true)

View file

@ -119,13 +119,12 @@ class CoreOffersService {
}
Offer getMyOffer(String id) {
Offer offer = offerBookService.getOffers().stream()
return openOfferManager.getObservableList().stream()
.map(OpenOffer::getOffer)
.filter(o -> o.getId().equals(id))
.filter(o -> o.isMyOffer(keyRing))
.findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
setOpenOfferState(offer);
return offer;
}
List<Offer> getOffers(String direction, String currencyCode) {
@ -143,9 +142,10 @@ class CoreOffersService {
}
List<Offer> getMyOffers(String direction, String currencyCode) {
// get my offers posted to books
List<Offer> offers = offerBookService.getOffers().stream()
// get my open offers
List<Offer> offers = openOfferManager.getObservableList().stream()
.map(OpenOffer::getOffer)
.filter(o -> o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.sorted(priceComparator(direction))
@ -162,9 +162,6 @@ class CoreOffersService {
}
openOfferManager.removeOpenOffers(unreservedOpenOffers, null);
// set offer states
for (Offer offer : offers) setOpenOfferState(offer);
return offers;
}
@ -174,6 +171,7 @@ class CoreOffersService {
// collect reserved key images and check for duplicate funds
List<String> allKeyImages = new ArrayList<String>();
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!allKeyImages.add(keyImage)) {
log.warn("Key image {} belongs to another offer, removing offer {}", keyImage, offer.getId()); // TODO (woodser): this is list, not set, so not checking for duplicates
@ -192,6 +190,7 @@ class CoreOffersService {
// check for offers with spent key images
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
if (unreservedOffers.contains(offer)) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (spentKeyImages.contains(keyImage)) {
@ -256,7 +255,6 @@ class CoreOffersService {
boolean useSavingsWallet = true;
//noinspection ConstantConditions
placeOffer(offer,
buyerSecurityDeposit,
triggerPriceAsString,
useSavingsWallet,
transaction -> resultHandler.accept(offer),
@ -308,14 +306,12 @@ class CoreOffersService {
}
private void placeOffer(Offer offer,
double buyerSecurityDeposit,
String triggerPriceAsString,
boolean useSavingsWallet,
Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
openOfferManager.placeOffer(offer,
buyerSecurityDeposit,
useSavingsWallet,
triggerPriceAsLong,
resultHandler::accept,
@ -331,11 +327,6 @@ class CoreOffersService {
return offerOfWantedDirection && offerInWantedCurrency;
}
private void setOpenOfferState(Offer offer) {
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
if (openOffer.isPresent()) offer.setState(openOffer.get().getState() == OpenOffer.State.AVAILABLE ? Offer.State.AVAILABLE : Offer.State.NOT_AVAILABLE);
}
private Comparator<Offer> priceComparator(String direction) {
// A buyer probably wants to see sell orders in price ascending order.
// A seller probably wants to see buy orders in price descending order.

View file

@ -20,9 +20,10 @@ package bisq.core.api.model;
import bisq.core.offer.Offer;
import bisq.common.Payload;
import bisq.common.proto.ProtoUtil;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@ -47,6 +48,7 @@ public class OfferInfo implements Payload {
private final long minVolume;
private final long txFee;
private final long makerFee;
@Nullable
private final String offerFeePaymentTxId;
private final long buyerSecurityDeposit;
private final long sellerSecurityDeposit;
@ -129,7 +131,7 @@ public class OfferInfo implements Payload {
@Override
public bisq.proto.grpc.OfferInfo toProtoMessage() {
return bisq.proto.grpc.OfferInfo.newBuilder()
bisq.proto.grpc.OfferInfo.Builder builder = bisq.proto.grpc.OfferInfo.newBuilder()
.setId(id)
.setDirection(direction)
.setPrice(price)
@ -141,7 +143,6 @@ public class OfferInfo implements Payload {
.setMinVolume(minVolume)
.setMakerFee(makerFee)
.setTxFee(txFee)
.setOfferFeePaymentTxId(offerFeePaymentTxId)
.setBuyerSecurityDeposit(buyerSecurityDeposit)
.setSellerSecurityDeposit(sellerSecurityDeposit)
.setTriggerPrice(triggerPrice)
@ -151,8 +152,9 @@ public class OfferInfo implements Payload {
.setBaseCurrencyCode(baseCurrencyCode)
.setCounterCurrencyCode(counterCurrencyCode)
.setDate(date)
.setState(state)
.build();
.setState(state);
Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId);
return builder.build();
}
@SuppressWarnings("unused")
@ -169,7 +171,7 @@ public class OfferInfo implements Payload {
.withMinVolume(proto.getMinVolume())
.withMakerFee(proto.getMakerFee())
.withTxFee(proto.getTxFee())
.withOfferFeePaymentTxId(proto.getOfferFeePaymentTxId())
.withOfferFeePaymentTxId(ProtoUtil.stringOrNullFromProto(proto.getOfferFeePaymentTxId()))
.withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit())
.withSellerSecurityDeposit(proto.getSellerSecurityDeposit())
.withTriggerPrice(proto.getTriggerPrice())

View file

@ -413,13 +413,8 @@ public class XmrWalletService {
System.out.println("Monero wallet balance: " + wallet.getBalance(0));
System.out.println("Monero wallet unlocked balance: " + wallet.getUnlockedBalance(0));
// notify on balance changes
wallet.addListener(new MoneroWalletListener() {
@Override
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
notifyBalanceListeners();
}
});
// register internal listener to notify external listeners
wallet.addListener(new XmrWalletListener());
}
}
@ -758,6 +753,15 @@ public class XmrWalletService {
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream());
return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive());
}
public void addWalletListener(MoneroWalletListenerI listener) {
walletListeners.add(listener);
}
public void removeWalletListener(MoneroWalletListenerI listener) {
if (!walletListeners.contains(listener)) throw new RuntimeException("Listener is not registered with wallet");
walletListeners.remove(listener);
}
// TODO (woodser): update balance and other listening
public void addBalanceListener(XmrBalanceListener listener) {
@ -786,26 +790,22 @@ public class XmrWalletService {
for (MoneroTxWallet tx : txs) sb.append('\n' + tx.toString());
log.info("\n" + tracePrefix + ":" + sb.toString());
}
// -------------------------------- HELPERS -------------------------------
/**
* Wraps a MoneroWalletListener to notify the Haveno application.
*
* TODO (woodser): this is no longer necessary since not syncing to thread?
* Processes internally before notifying external listeners.
*
* TODO: no longer neccessary to execute on user thread?
*/
public class HavenoWalletListener extends MoneroWalletListener {
private MoneroWalletListener listener;
public HavenoWalletListener(MoneroWalletListener listener) {
this.listener = listener;
}
private class XmrWalletListener extends MoneroWalletListener {
@Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
UserThread.execute(new Runnable() {
@Override
public void run() {
listener.onSyncProgress(height, startHeight, endHeight, percentDone, message);
for (MoneroWalletListenerI listener : walletListeners) listener.onSyncProgress(height, startHeight, endHeight, percentDone, message);
}
});
}
@ -815,7 +815,7 @@ public class XmrWalletService {
UserThread.execute(new Runnable() {
@Override
public void run() {
listener.onNewBlock(height);
for (MoneroWalletListenerI listener : walletListeners) listener.onNewBlock(height);
}
});
}
@ -825,7 +825,8 @@ public class XmrWalletService {
UserThread.execute(new Runnable() {
@Override
public void run() {
listener.onBalancesChanged(newBalance, newUnlockedBalance);
for (MoneroWalletListenerI listener : walletListeners) listener.onBalancesChanged(newBalance, newUnlockedBalance);
notifyBalanceListeners();
}
});
}
@ -835,7 +836,7 @@ public class XmrWalletService {
UserThread.execute(new Runnable() {
@Override
public void run() {
listener.onOutputReceived(output);
for (MoneroWalletListenerI listener : walletListeners) listener.onOutputReceived(output);
}
});
}
@ -845,7 +846,7 @@ public class XmrWalletService {
UserThread.execute(new Runnable() {
@Override
public void run() {
listener.onOutputSpent(output);
for (MoneroWalletListenerI listener : walletListeners) listener.onOutputSpent(output);
}
});
}

View file

@ -22,13 +22,13 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.OfferPayload.Direction;
import bisq.core.offer.availability.OfferAvailabilityModel;
import bisq.core.offer.availability.OfferAvailabilityProtocol;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.KeyRing;
@ -82,6 +82,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
public enum State {
UNKNOWN,
SCHEDULED,
OFFER_FEE_RESERVED,
AVAILABLE,
NOT_AVAILABLE,
@ -257,6 +258,11 @@ public class Offer implements NetworkPayload, PersistablePayload {
///////////////////////////////////////////////////////////////////////////////////////////
public void setState(Offer.State state) {
try {
throw new RuntimeException("Setting offer state: " + state);
} catch (Exception e) {
e.printStackTrace();
}
stateProperty().set(state);
}
@ -277,6 +283,15 @@ public class Offer implements NetworkPayload, PersistablePayload {
// Getter
///////////////////////////////////////////////////////////////////////////////////////////
// get the amount needed for the maker to reserve the offer
public Coin getReserveAmount() {
Coin reserveAmount = getAmount();
reserveAmount = reserveAmount.add(getDirection() == Direction.BUY ?
getBuyerSecurityDeposit() :
getSellerSecurityDeposit());
return reserveAmount;
}
// converted payload properties
public Coin getTxFee() {
return Coin.valueOf(offerPayload.getTxFee());

View file

@ -297,9 +297,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
.setProtocolVersion(protocolVersion)
.setArbitratorSigner(arbitratorSigner.toProtoMessage());
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
"OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network."));
Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId);
Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode);
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
@ -313,7 +311,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
}
public static OfferPayload fromProto(protobuf.OfferPayload proto) {
checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload");
List<String> acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ?
null : new ArrayList<>(proto.getAcceptedBankIdsList());
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
@ -336,7 +333,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getCounterCurrencyCode(),
proto.getPaymentMethodId(),
proto.getMakerPaymentAccountId(),
proto.getOfferFeePaymentTxId(),
ProtoUtil.stringOrNullFromProto(proto.getOfferFeePaymentTxId()),
ProtoUtil.stringOrNullFromProto(proto.getCountryCode()),
acceptedCountryCodes,
ProtoUtil.stringOrNullFromProto(proto.getBankId()),

View file

@ -25,6 +25,7 @@ import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.proto.ProtoUtil;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import lombok.EqualsAndHashCode;
@ -42,6 +43,7 @@ public final class OpenOffer implements Tradable {
transient private Timer timeoutTimer;
public enum State {
SCHEDULED,
AVAILABLE,
RESERVED,
CLOSED,
@ -59,10 +61,24 @@ public final class OpenOffer implements Tradable {
private NodeAddress backupArbitrator;
@Setter
@Getter
private boolean autoSplit;
@Setter
@Getter
@Nullable
private String scheduledAmount;
@Setter
@Getter
@Nullable
private List<String> scheduledTxHashes;
@Nullable
@Setter
@Getter
private String reserveTxHash;
@Nullable
@Setter
@Getter
private String reserveTxHex;
@Nullable
@Setter
@Getter
private String reserveTxKey;
@ -77,26 +93,18 @@ public final class OpenOffer implements Tradable {
transient private long mempoolStatus = -1;
public OpenOffer(Offer offer) {
this(offer, 0);
this(offer, 0, false);
}
public OpenOffer(Offer offer, long triggerPrice) {
this.offer = offer;
this.triggerPrice = triggerPrice;
state = State.AVAILABLE;
this(offer, triggerPrice, false);
}
public OpenOffer(Offer offer,
long triggerPrice,
String reserveTxHash,
String reserveTxHex,
String reserveTxKey) {
public OpenOffer(Offer offer, long triggerPrice, boolean autoSplit) {
this.offer = offer;
this.triggerPrice = triggerPrice;
state = State.AVAILABLE;
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
this.autoSplit = autoSplit;
state = State.SCHEDULED;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -107,13 +115,18 @@ public final class OpenOffer implements Tradable {
State state,
@Nullable NodeAddress backupArbitrator,
long triggerPrice,
String reserveTxHash,
String reserveTxHex,
String reserveTxKey) {
boolean autoSplit,
@Nullable String scheduledAmount,
@Nullable List<String> scheduledTxHashes,
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
@Nullable String reserveTxKey) {
this.offer = offer;
this.state = state;
this.backupArbitrator = backupArbitrator;
this.triggerPrice = triggerPrice;
this.autoSplit = autoSplit;
this.scheduledTxHashes = scheduledTxHashes;
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
@ -128,11 +141,14 @@ public final class OpenOffer implements Tradable {
.setOffer(offer.toProtoMessage())
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setReserveTxHash(reserveTxHash)
.setReserveTxHex(reserveTxHex)
.setReserveTxKey(reserveTxKey);
.setAutoSplit(autoSplit);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(backupArbitrator).ifPresent(nodeAddress -> builder.setBackupArbitrator(nodeAddress.toProtoMessage()));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@ -142,6 +158,9 @@ public final class OpenOffer implements Tradable {
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null,
proto.getTriggerPrice(),
proto.getAutoSplit(),
proto.getScheduledAmount(),
proto.getScheduledTxHashesList(),
proto.getReserveTxHash(),
proto.getReserveTxHex(),
proto.getReserveTxKey());
@ -172,7 +191,7 @@ public final class OpenOffer implements Tradable {
this.state = state;
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
if (this.state == State.RESERVED) {
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
startTimeout();
} else {
stopTimeout();

View file

@ -38,6 +38,7 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.trade.TradableList;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.closed.ClosedTradableManager;
import bisq.core.trade.handlers.TransactionResultHandler;
import bisq.core.trade.statistics.TradeStatisticsManager;
@ -85,6 +86,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -92,6 +94,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet;
import monero.wallet.model.MoneroWalletListener;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
@ -107,7 +113,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private static final long REFRESH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(6);
private final CoreContext coreContext;
private final CreateOfferService createOfferService;
private final KeyRing keyRing;
private final User user;
private final P2PService p2PService;
@ -130,6 +135,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private final SignedOfferList signedOffers = new SignedOfferList();
private final PersistenceManager<SignedOfferList> signedOfferPersistenceManager;
private final Map<String, PlaceOfferProtocol> placeOfferProtocols = new HashMap<String, PlaceOfferProtocol>();
private BigInteger lastUnlockedBalance;
private boolean stopped;
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer;
@Getter
@ -142,7 +148,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
@Inject
public OpenOfferManager(CoreContext coreContext,
CreateOfferService createOfferService,
KeyRing keyRing,
User user,
P2PService p2PService,
@ -162,7 +167,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
PersistenceManager<TradableList<OpenOffer>> persistenceManager,
PersistenceManager<SignedOfferList> signedOfferPersistenceManager) {
this.coreContext = coreContext;
this.createOfferService = createOfferService;
this.keyRing = keyRing;
this.user = user;
this.p2PService = p2PService;
@ -223,6 +227,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffers.stream()
.forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
.ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
// process unposted offers
lastUnlockedBalance = xmrWalletService.getWallet().getUnlockedBalance(0);
processUnpostedOffers((transaction) -> {}, (errMessage) -> {
log.warn("Error processing unposted offers on new unlocked balance: " + errMessage);
});
// register to process unposted offers when unlocked balance increases
xmrWalletService.addWalletListener(new MoneroWalletListener() {
@Override
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
if (lastUnlockedBalance.compareTo(newUnlockedBalance) < 0) {
processUnpostedOffers((transaction) -> {}, (errMessage) -> {
log.warn("Error processing unposted offers on new unlocked balance: " + errMessage);
});
}
lastUnlockedBalance = newUnlockedBalance;
}
});
}
private void cleanUpAddressEntries() {
@ -384,55 +407,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
public void placeOffer(Offer offer,
double buyerSecurityDeposit,
boolean useSavingsWallet,
long triggerPrice,
TransactionResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
offer.getAmount(),
buyerSecurityDeposit,
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit));
PlaceOfferModel model = new PlaceOfferModel(offer,
reservedFundsForOffer,
useSavingsWallet,
p2PService,
btcWalletService,
xmrWalletService,
tradeWalletService,
offerBookService,
arbitratorManager,
mediatorManager,
tradeStatisticsManager,
user,
keyRing,
filterManager);
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
model,
transaction -> {
// save reserve tx with open offer
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, model.getReserveTx().getHash(), model.getReserveTx().getFullHex(), model.getReserveTx().getKey());
openOffers.add(openOffer);
requestPersistence();
resultHandler.handleResult(transaction);
if (!stopped) {
startPeriodicRepublishOffersTimer();
startPeriodicRefreshOffersTimer();
} else {
log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
}
},
errorMessageHandler
);
synchronized (placeOfferProtocols) {
placeOfferProtocols.put(offer.getId(), placeOfferProtocol);
}
placeOfferProtocol.placeOffer(); // TODO (woodser): if error placing offer (e.g. bad signature), remove protocol and unfreeze trade funds
boolean autoSplit = false; // TODO: support in api
// TODO (woodser): validate offer
// create open offer
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, autoSplit);
// process open offer to schedule or post
processUnpostedOffer(openOffer, (transaction) -> {
openOffers.add(openOffer);
requestPersistence();
resultHandler.handleResult(transaction);
}, (errMessage) -> {
errorMessageHandler.handleErrorMessage(errMessage);
});
}
// Remove from offerbook
@ -442,11 +437,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
removeOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
} else {
log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook.");
errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " +
"We still try to remove it from the offerbook.");
offerBookService.removeOffer(offer.getOfferPayload(),
() -> offer.setState(Offer.State.REMOVED),
null);
errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook.");
offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null);
}
}
@ -569,7 +561,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private void onRemoved(@NotNull OpenOffer openOffer, ResultHandler resultHandler, Offer offer) {
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
}
offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
openOffers.remove(openOffer);
@ -622,6 +616,166 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Place offer helpers
///////////////////////////////////////////////////////////////////////////////////////////
private void processUnpostedOffers(TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) {
List<String> errorMessages = new ArrayList<String>();
for (OpenOffer scheduledOffer : openOffers.getObservableList()) {
if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue;
CountDownLatch latch = new CountDownLatch(openOffers.list.size());
processUnpostedOffer(scheduledOffer, (transaction) -> {
latch.countDown();
}, errorMessage -> {
latch.countDown();
errorMessages.add(errorMessage);
});
TradeUtils.waitForLatch(latch);
}
requestPersistence();
if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString());
else resultHandler.handleResult(null);
}
private void processUnpostedOffer(OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
try {
// get offer reserve amount
Coin offerReserveAmountCoin = openOffer.getOffer().getReserveAmount();
BigInteger offerReserveAmount = ParsingUtils.centinerosToAtomicUnits(offerReserveAmountCoin.value);
// 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
else {
signAndPostOffer(openOffer, offerReserveAmountCoin, true, resultHandler, errorMessageHandler);
}
return;
}
// handle unscheduled offer
if (openOffer.getScheduledTxHashes() == null) {
// check for sufficient balance - scheduled offers amount
if (xmrWalletService.getWallet().getBalance(0).subtract(getScheduledAmount()).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 = new BigInteger("0");
for (MoneroTxWallet lockedTx : lockedTxs) {
if (isTxScheduled(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 Error("Not enough funds to schedule offer");
// schedule txs
openOffer.setScheduledTxHashes(scheduledTxHashes);
openOffer.setScheduledAmount(scheduledAmount.toString());
openOffer.getOffer().setState(Offer.State.SCHEDULED);
}
// handle result
resultHandler.handleResult(null);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage(e.getMessage());
}
}
private BigInteger getScheduledAmount() {
BigInteger scheduledAmount = new BigInteger("0");
for (OpenOffer openOffer : openOffers.getObservableList()) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue;
if (openOffer.getScheduledTxHashes() == null) continue;
List<MoneroTxWallet> fundingTxs = xmrWalletService.getWallet().getTxs(openOffer.getScheduledTxHashes());
for (MoneroTxWallet fundingTx : fundingTxs) {
for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) {
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
}
}
}
return scheduledAmount;
}
private boolean isTxScheduled(String txHash) {
for (OpenOffer openOffer : openOffers.getObservableList()) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue;
if (openOffer.getScheduledTxHashes() == null) continue;
for (String scheduledTxHash : openOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true;
}
}
return false;
}
private void signAndPostOffer(OpenOffer openOffer,
Coin offerReserveAmount, // TODO: switch to BigInteger
boolean useSavingsWallet, // TODO: remove this
TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// create model
PlaceOfferModel model = new PlaceOfferModel(openOffer.getOffer(),
offerReserveAmount,
useSavingsWallet,
p2PService,
btcWalletService,
xmrWalletService,
tradeWalletService,
offerBookService,
arbitratorManager,
mediatorManager,
tradeStatisticsManager,
user,
keyRing,
filterManager);
// create protocol
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(model,
transaction -> {
// set reserve tx on open offer
openOffer.setReserveTxHash(model.getReserveTx().getHash());
openOffer.setReserveTxHex(model.getReserveTx().getHash());
openOffer.setReserveTxKey(model.getReserveTx().getKey());
// set offer state
openOffer.setState(OpenOffer.State.AVAILABLE);
resultHandler.handleResult(transaction);
if (!stopped) {
startPeriodicRepublishOffersTimer();
startPeriodicRefreshOffersTimer();
} else {
log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
}
},
errorMessageHandler);
// run protocol
synchronized (placeOfferProtocols) {
placeOfferProtocols.put(openOffer.getOffer().getId(), placeOfferProtocol);
}
placeOfferProtocol.placeOffer(); // TODO (woodser): if error placing offer (e.g. bad signature), remove protocol and unfreeze trade funds
}
///////////////////////////////////////////////////////////////////////////////////////////
// Arbitrator Signs Offer
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -19,7 +19,7 @@ package bisq.core.offer.placeoffer;
import bisq.core.offer.messages.SignOfferResponse;
import bisq.core.offer.placeoffer.tasks.AddToOfferBook;
import bisq.core.offer.placeoffer.tasks.MakerReservesTradeFunds;
import bisq.core.offer.placeoffer.tasks.MakerReservesOfferFunds;
import bisq.core.offer.placeoffer.tasks.MakerSendsSignOfferRequest;
import bisq.core.offer.placeoffer.tasks.MakerProcessesSignOfferResponse;
import bisq.core.offer.placeoffer.tasks.ValidateOffer;
@ -56,7 +56,6 @@ public class PlaceOfferProtocol {
// Called from UI
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): this returns before offer is placed
public void placeOffer() {
log.debug("placeOffer() " + model.getOffer().getId());
TaskRunner<PlaceOfferModel> taskRunner = new TaskRunner<>(model,
@ -71,7 +70,7 @@ public class PlaceOfferProtocol {
);
taskRunner.addTasks(
ValidateOffer.class,
MakerReservesTradeFunds.class,
MakerReservesOfferFunds.class,
MakerSendsSignOfferRequest.class
);

View file

@ -29,9 +29,9 @@ import java.util.List;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
public class MakerReservesOfferFunds extends Task<PlaceOfferModel> {
public MakerReservesTradeFunds(TaskRunner taskHandler, PlaceOfferModel model) {
public MakerReservesOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) {
super(taskHandler, model);
}
@ -43,7 +43,7 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
try {
runInterceptHook();
// freeze trade funds and get reserve tx
// freeze offer funds and get reserve tx
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());

View file

@ -901,7 +901,7 @@ public abstract class Trade implements Tradable, Model {
}
// create block listener
depositTxListener = processModel.getXmrWalletService().new HavenoWalletListener(new MoneroWalletListener() { // TODO (woodser): separate into own class file
depositTxListener = new MoneroWalletListener() {
Long unlockHeight = null;
@ -939,14 +939,14 @@ public abstract class Trade implements Tradable, Model {
if (unlockHeight != null && height == unlockHeight) {
log.info("Multisig deposits unlocked for trade {}", getId());
setConfirmedState(); // TODO (woodser): bisq "confirmed" = xmr unlocked after 10 confirmations
havenoWallet.removeListener(depositTxListener); // remove listener when notified
xmrWalletService.removeWalletListener(depositTxListener); // remove listener when notified
depositTxListener = null; // prevent re-applying trade state in subsequent requests
}
}
});
};
// register wallet listener
havenoWallet.addListener(depositTxListener);
xmrWalletService.addWalletListener(depositTxListener);
}
@Nullable

View file

@ -27,6 +27,7 @@ import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.messages.InitTradeRequest;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
/**
* Collection of utilities for trading.
@ -173,4 +174,12 @@ public class TradeUtils {
//
// return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress);
}
public static void waitForLatch(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -2,6 +2,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
@ -59,7 +60,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -87,7 +88,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -116,7 +117,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -144,7 +145,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}

View file

@ -20,6 +20,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.BuyerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
@ -100,7 +101,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -129,7 +130,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -158,7 +159,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -188,7 +189,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
@ -222,7 +223,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -254,7 +255,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);

View file

@ -22,6 +22,7 @@ import bisq.core.offer.Offer;
import bisq.core.trade.BuyerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DepositResponse;
@ -33,7 +34,6 @@ import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.TakerProtocol.TakerEvent;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
@ -116,7 +116,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -145,7 +145,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -174,7 +174,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -204,7 +204,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
@ -239,7 +239,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -271,7 +271,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);

View file

@ -19,6 +19,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
@ -26,7 +27,6 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.UpdateMultisigWithTradingPeer;
import bisq.core.trade.protocol.tasks.buyer.BuyerPreparesPaymentSentMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessesPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsPaymentSentMessage;
@ -182,7 +182,7 @@ public abstract class BuyerProtocol extends DisputeProtocol {
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}

View file

@ -21,6 +21,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.DepositTxMessage;
@ -100,7 +101,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -129,7 +130,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -158,7 +159,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -188,7 +189,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
@ -222,7 +223,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -254,7 +255,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);

View file

@ -22,6 +22,7 @@ import bisq.core.offer.Offer;
import bisq.core.trade.SellerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse;
@ -108,7 +109,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -137,7 +138,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -166,7 +167,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -196,7 +197,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
@ -231,7 +232,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -263,7 +264,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
TradeUtils.waitForLatch(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);

View file

@ -20,6 +20,7 @@ package bisq.core.trade.protocol;
import bisq.core.offer.Offer;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
@ -236,7 +237,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
);
startTimeout(TRADE_TIMEOUT);
taskRunner.run();
wait(latch);
TradeUtils.waitForLatch(latch);
}
}
@ -367,14 +368,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
///////////////////////////////////////////////////////////////////////////////////////////
// Timeout
///////////////////////////////////////////////////////////////////////////////////////////
protected void wait(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
protected void startTimeout(long timeoutSec) {
stopTimeout();

View file

@ -56,7 +56,6 @@ public class OpenOfferManagerTest {
when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class));
final OpenOfferManager manager = new OpenOfferManager(coreContext,
null,
null,
null,
p2PService,
@ -103,7 +102,6 @@ public class OpenOfferManagerTest {
when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class));
final OpenOfferManager manager = new OpenOfferManager(coreContext,
null,
null,
null,
p2PService,
@ -144,7 +142,6 @@ public class OpenOfferManagerTest {
final OpenOfferManager manager = new OpenOfferManager(coreContext,
null,
null,
null,
p2PService,

View file

@ -39,7 +39,7 @@ public class TradableListTest {
// test adding an OpenOffer and convert toProto
Offer offer = new Offer(offerPayload);
OpenOffer openOffer = new OpenOffer(offer, 0, "", "", "");
OpenOffer openOffer = new OpenOffer(offer, 0);
openOfferTradableList.add(openOffer);
message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage();
assertEquals(message.getMessageCase(), TRADABLE_LIST);

View file

@ -24,7 +24,7 @@ import bisq.desktop.components.TitledGroupBg;
import bisq.core.offer.availability.tasks.ProcessOfferAvailabilityResponse;
import bisq.core.offer.availability.tasks.SendOfferAvailabilityRequest;
import bisq.core.offer.placeoffer.tasks.AddToOfferBook;
import bisq.core.offer.placeoffer.tasks.MakerReservesTradeFunds;
import bisq.core.offer.placeoffer.tasks.MakerReservesOfferFunds;
import bisq.core.offer.placeoffer.tasks.ValidateOffer;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
@ -109,7 +109,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
addGroup("PlaceOfferProtocol",
FXCollections.observableArrayList(Arrays.asList(
ValidateOffer.class,
MakerReservesTradeFunds.class,
MakerReservesOfferFunds.class,
AddToOfferBook.class)
));

View file

@ -315,7 +315,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) {
openOfferManager.placeOffer(offer,
buyerSecurityDeposit.get(),
useSavingsWallet,
triggerPrice,
resultHandler,

View file

@ -1450,11 +1450,12 @@ message Offer {
enum State {
PB_ERROR = 0;
UNKNOWN = 1;
OFFER_FEE_PAID = 2;
AVAILABLE = 3;
NOT_AVAILABLE = 4;
REMOVED = 5;
MAKER_OFFLINE = 6;
SCHEDULED = 2;
OFFER_FEE_RESERVED = 3;
AVAILABLE = 4;
NOT_AVAILABLE = 5;
REMOVED = 6;
MAKER_OFFLINE = 7;
}
OfferPayload offer_payload = 1;
@ -1474,20 +1475,24 @@ message SignedOffer {
message OpenOffer {
enum State {
PB_ERROR = 0;
AVAILABLE = 1;
RESERVED = 2;
CLOSED = 3;
CANCELED = 4;
DEACTIVATED = 5;
SCHEDULED = 1;
AVAILABLE = 2;
RESERVED = 3;
CLOSED = 4;
CANCELED = 5;
DEACTIVATED = 6;
}
Offer offer = 1;
State state = 2;
NodeAddress backup_arbitrator = 3;
int64 trigger_price = 4;
string reserve_tx_hash = 5;
string reserve_tx_hex = 6;
string reserve_tx_key = 7;
bool auto_split = 5;
repeated string scheduled_tx_hashes = 6;
string scheduled_amount = 7; // BigInteger
string reserve_tx_hash = 8;
string reserve_tx_hex = 9;
string reserve_tx_key = 10;
}
message Tradable {