diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 151effb3..49b1a9de 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -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); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index ab250251..54837b27 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -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) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 0aa87a46..7e978566 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -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 getOffers(String direction, String currencyCode) { @@ -143,9 +142,10 @@ class CoreOffersService { } List getMyOffers(String direction, String currencyCode) { - - // get my offers posted to books - List offers = offerBookService.getOffers().stream() + + // get my open offers + List 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 allKeyImages = new ArrayList(); 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 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 = openOfferManager.getOpenOfferById(offer.getId()); - if (openOffer.isPresent()) offer.setState(openOffer.get().getState() == OpenOffer.State.AVAILABLE ? Offer.State.AVAILABLE : Offer.State.NOT_AVAILABLE); - } - private Comparator 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. diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index 05e4ca72..2e9c70a3 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -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()) diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index e6820968..f168f191 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -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); } }); } diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index be1d9074..5e961c52 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -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()); diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index a5953375..f75b70a3 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -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 acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? null : new ArrayList<>(proto.getAcceptedBankIdsList()); List 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()), diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 0056cbd3..d1892296 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -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 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 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(); diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 6d591929..ce32c9d8 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -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 signedOfferPersistenceManager; private final Map placeOfferProtocols = new HashMap(); + 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> persistenceManager, PersistenceManager 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 errorMessages = new ArrayList(); + 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 lockedTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIsLocked(true)); + + // get earliest unscheduled txs with sufficient incoming amount + List scheduledTxHashes = new ArrayList(); + 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 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 /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java index 202a3a29..aafa8bff 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferProtocol.java @@ -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 taskRunner = new TaskRunner<>(model, @@ -71,7 +70,7 @@ public class PlaceOfferProtocol { ); taskRunner.addTasks( ValidateOffer.class, - MakerReservesTradeFunds.class, + MakerReservesOfferFunds.class, MakerSendsSignOfferRequest.class ); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesOfferFunds.java similarity index 93% rename from core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java rename to core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesOfferFunds.java index 2a2ee8af..80542c45 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesOfferFunds.java @@ -29,9 +29,9 @@ import java.util.List; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; -public class MakerReservesTradeFunds extends Task { +public class MakerReservesOfferFunds extends Task { - public MakerReservesTradeFunds(TaskRunner taskHandler, PlaceOfferModel model) { + public MakerReservesOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @@ -43,7 +43,7 @@ public class MakerReservesTradeFunds extends Task { 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()); diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index bfc72957..6f928133 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -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 diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java index c8d5911e..d52f34ec 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -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); + } + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java index 5233d531..c917ef90 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java @@ -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); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index 194e9c2d..38b5eee7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -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); diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index 749f1eb6..2855c3be 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -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); diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java index bd826a9c..c283a5b4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -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); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index 5f8d7614..7d1572ae 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -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); diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index 4c659b8c..c4849f6c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -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); diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index adf2e06f..25db1839 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -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(); diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index 67a93ebe..494e0be7 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -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, diff --git a/core/src/test/java/bisq/core/trade/TradableListTest.java b/core/src/test/java/bisq/core/trade/TradableListTest.java index 9bfbbfa4..2b48d4b5 100644 --- a/core/src/test/java/bisq/core/trade/TradableListTest.java +++ b/core/src/test/java/bisq/core/trade/TradableListTest.java @@ -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); diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index fe8c0afd..8f8fcc72 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -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 { addGroup("PlaceOfferProtocol", FXCollections.observableArrayList(Arrays.asList( ValidateOffer.class, - MakerReservesTradeFunds.class, + MakerReservesOfferFunds.class, AddToOfferBook.class) )); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index a6b10555..0ede059f 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -315,7 +315,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { openOfferManager.placeOffer(offer, - buyerSecurityDeposit.get(), useSavingsWallet, triggerPrice, resultHandler, diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index f34d86ec..af2a9284 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -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 {