diff --git a/account/src/main/java/bisq/account/AccountService.java b/account/src/main/java/bisq/account/AccountService.java index 697509bbf0..1323e23758 100644 --- a/account/src/main/java/bisq/account/AccountService.java +++ b/account/src/main/java/bisq/account/AccountService.java @@ -26,8 +26,6 @@ import bisq.common.application.Service; import bisq.common.observable.Observable; import bisq.common.observable.collection.ObservableSet; -import bisq.identity.IdentityService; -import bisq.network.NetworkService; import bisq.persistence.Persistence; import bisq.persistence.PersistenceClient; import bisq.persistence.PersistenceService; @@ -48,12 +46,15 @@ public class AccountService implements PersistenceClient, Service @Getter private transient final ObservableSet>> accounts = new ObservableSet<>(); - public AccountService(NetworkService networkService, - PersistenceService persistenceService, - IdentityService identityService) { + public AccountService(PersistenceService persistenceService) { persistence = persistenceService.getOrCreatePersistence(this, persistableStore); } + @Override + public void onPersistedApplied(AccountStore persisted) { + accounts.setAll(persisted.getAccountByName().values()); + } + /////////////////////////////////////////////////////////////////////////////////////////////////// // Service diff --git a/account/src/main/java/bisq/account/accounts/UserDefinedFiatAccountPayload.java b/account/src/main/java/bisq/account/accounts/UserDefinedFiatAccountPayload.java index 6b77aae3ab..c43f63ee84 100644 --- a/account/src/main/java/bisq/account/accounts/UserDefinedFiatAccountPayload.java +++ b/account/src/main/java/bisq/account/accounts/UserDefinedFiatAccountPayload.java @@ -22,15 +22,19 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + @Getter @Slf4j @ToString @EqualsAndHashCode(callSuper = true) public final class UserDefinedFiatAccountPayload extends AccountPayload { + public static final int MAX_DATA_LENGTH = 1000; private final String accountData; public UserDefinedFiatAccountPayload(String id, String paymentMethodName, String accountData) { super(id, paymentMethodName); + checkArgument(accountData.length() <= MAX_DATA_LENGTH); this.accountData = accountData; } diff --git a/application/src/main/java/bisq/application/DefaultApplicationService.java b/application/src/main/java/bisq/application/DefaultApplicationService.java index 46504bbd3f..faf0c27050 100644 --- a/application/src/main/java/bisq/application/DefaultApplicationService.java +++ b/application/src/main/java/bisq/application/DefaultApplicationService.java @@ -115,7 +115,7 @@ public DefaultApplicationService(String[] args) { oracleService = new OracleService(OracleService.Config.from(getConfig("oracle")), config.getVersion(), networkService); - accountService = new AccountService(networkService, persistenceService, identityService); + accountService = new AccountService(persistenceService); contractService = new ContractService(securityService); @@ -142,7 +142,8 @@ public DefaultApplicationService(String[] args) { supportService = new SupportService(networkService, chatService, userService); - tradeService = new TradeService(networkService, identityService, persistenceService, offerService, contractService, supportService); + tradeService = new TradeService(networkService, identityService, persistenceService, offerService, + contractService, supportService, chatService, oracleService); } @Override diff --git a/application/src/main/resources/default.conf b/application/src/main/resources/default.conf index b896305b24..6b91b6f274 100644 --- a/application/src/main/resources/default.conf +++ b/application/src/main/resources/default.conf @@ -34,6 +34,10 @@ application { // todo } + blockchainExplorer = { + // todo + } + daoBridgeHttpService = { url = "http://localhost:8082" } diff --git a/chat/src/main/java/bisq/chat/bisqeasy/channel/priv/BisqEasyPrivateTradeChatChannelService.java b/chat/src/main/java/bisq/chat/bisqeasy/channel/priv/BisqEasyPrivateTradeChatChannelService.java index c8e4954f84..18e4e1dddd 100644 --- a/chat/src/main/java/bisq/chat/bisqeasy/channel/priv/BisqEasyPrivateTradeChatChannelService.java +++ b/chat/src/main/java/bisq/chat/bisqeasy/channel/priv/BisqEasyPrivateTradeChatChannelService.java @@ -305,10 +305,12 @@ private void processMessage(BisqEasyPrivateTradeChatMessage message) { message.getSender(), message.getMediator())); } else { - // It could be that taker sends quickly a message after take offer and we receive them + // It could be that taker sends quickly a message after take offer, and we receive them // out of order. In that case the seconds message (which arrived first) would get dropped. // This is a very unlikely case, so we ignore it. - log.error("We received the first message for a new channel without an offer. " + + // It also happens if we left a trade channel and receive a message again. + // We ignore that and do not re-open the channel. + log.debug("We received the first message for a new channel without an offer. " + "We drop that message. Message={}", message); return Optional.empty(); } diff --git a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyOfferMessage.java b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyOfferMessage.java index c85e41d73e..305962b4f4 100644 --- a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyOfferMessage.java +++ b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyOfferMessage.java @@ -17,10 +17,12 @@ package bisq.chat.bisqeasy.message; +import bisq.offer.bisq_easy.BisqEasyOffer; + import java.util.Optional; public interface BisqEasyOfferMessage { - Optional getBisqEasyOfferId(); + Optional getBisqEasyOffer(); boolean hasBisqEasyOffer(); } diff --git a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPrivateTradeChatMessage.java b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPrivateTradeChatMessage.java index 118a543f9b..54433af545 100644 --- a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPrivateTradeChatMessage.java +++ b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPrivateTradeChatMessage.java @@ -25,7 +25,6 @@ import bisq.network.p2p.services.data.storage.MetaData; import bisq.network.protobuf.ExternalNetworkMessage; import bisq.network.protobuf.NetworkMessage; -import bisq.offer.Offer; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.user.profile.UserProfile; import com.google.protobuf.Any; @@ -45,7 +44,6 @@ public final class BisqEasyPrivateTradeChatMessage extends PrivateChatMessage im private final Optional mediator; private final Optional bisqEasyOffer; - private final Optional bisqEasyOfferId; public BisqEasyPrivateTradeChatMessage(String messageId, String channelId, @@ -89,7 +87,6 @@ private BisqEasyPrivateTradeChatMessage(String messageId, super(messageId, chatChannelDomain, channelId, sender, receiverUserProfileId, text, citation, date, wasEdited, chatMessageType, metaData); this.mediator = mediator; this.bisqEasyOffer = bisqEasyOffer; - bisqEasyOfferId = bisqEasyOffer.map(Offer::getId); } public static BisqEasyPrivateTradeChatMessage createTakeOfferMessage(String channelId, @@ -122,7 +119,6 @@ private BisqEasyPrivateTradeChatMessage(String channelId, new MetaData(TTL, 100000, BisqEasyPrivateTradeChatMessage.class.getSimpleName())); this.mediator = mediator; this.bisqEasyOffer = Optional.of(bisqEasyOffer); - bisqEasyOfferId = this.bisqEasyOffer.map(Offer::getId); } @Override diff --git a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPublicChatMessage.java b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPublicChatMessage.java index 9ab0f4485e..cf7fe7f369 100644 --- a/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPublicChatMessage.java +++ b/chat/src/main/java/bisq/chat/bisqeasy/message/BisqEasyPublicChatMessage.java @@ -24,6 +24,7 @@ import bisq.chat.message.PublicChatMessage; import bisq.common.util.StringUtils; import bisq.network.p2p.services.data.storage.MetaData; +import bisq.offer.bisq_easy.BisqEasyOffer; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -36,11 +37,11 @@ @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public final class BisqEasyPublicChatMessage extends PublicChatMessage implements BisqEasyOfferMessage { - private final Optional bisqEasyOfferId; + private final Optional bisqEasyOffer; public BisqEasyPublicChatMessage(String channelId, String authorUserProfileId, - Optional bisqEasyOfferId, + Optional bisqEasyOffer, Optional text, Optional citation, long date, @@ -49,7 +50,7 @@ public BisqEasyPublicChatMessage(String channelId, ChatChannelDomain.BISQ_EASY, channelId, authorUserProfileId, - bisqEasyOfferId, + bisqEasyOffer, text, citation, date, @@ -62,7 +63,7 @@ private BisqEasyPublicChatMessage(String messageId, ChatChannelDomain chatChannelDomain, String channelId, String authorUserProfileId, - Optional bisqEasyOfferId, + Optional bisqEasyOffer, Optional text, Optional citation, long date, @@ -79,12 +80,12 @@ private BisqEasyPublicChatMessage(String messageId, wasEdited, chatMessageType, metaData); - this.bisqEasyOfferId = bisqEasyOfferId; + this.bisqEasyOffer = bisqEasyOffer; } public bisq.chat.protobuf.ChatMessage toProto() { bisq.chat.protobuf.BisqEasyPublicChatMessage.Builder builder = bisq.chat.protobuf.BisqEasyPublicChatMessage.newBuilder(); - bisqEasyOfferId.ifPresent(builder::setBisqEasyOfferId); + bisqEasyOffer.ifPresent(e -> builder.setBisqEasyOffer(e.toProto())); return getChatMessageBuilder().setPublicBisqEasyOfferChatMessage(builder).build(); } @@ -95,15 +96,15 @@ public static BisqEasyPublicChatMessage fromProto(bisq.chat.protobuf.ChatMessage Optional text = baseProto.hasText() ? Optional.of(baseProto.getText()) : Optional.empty(); - Optional bisqEasyOfferId = baseProto.getPublicBisqEasyOfferChatMessage().hasBisqEasyOfferId() ? - Optional.of(baseProto.getPublicBisqEasyOfferChatMessage().getBisqEasyOfferId()) : + Optional bisqEasyOffer = baseProto.getPublicBisqEasyOfferChatMessage().hasBisqEasyOffer() ? + Optional.of(BisqEasyOffer.fromProto(baseProto.getPublicBisqEasyOfferChatMessage().getBisqEasyOffer())) : Optional.empty(); return new BisqEasyPublicChatMessage( baseProto.getId(), ChatChannelDomain.fromProto(baseProto.getChatChannelDomain()), baseProto.getChannelId(), baseProto.getAuthorUserProfileId(), - bisqEasyOfferId, + bisqEasyOffer, text, citation, baseProto.getDate(), @@ -119,6 +120,6 @@ public MetaData getMetaData() { @Override public boolean hasBisqEasyOffer() { - return bisqEasyOfferId.isPresent(); + return bisqEasyOffer.isPresent(); } } \ No newline at end of file diff --git a/chat/src/main/proto/chat.proto b/chat/src/main/proto/chat.proto index c6b4f8dbe6..a876fb49f1 100644 --- a/chat/src/main/proto/chat.proto +++ b/chat/src/main/proto/chat.proto @@ -128,7 +128,7 @@ message BisqEasyPrivateTradeChatChannelStore { } message BisqEasyPublicChatMessage { - optional string bisqEasyOfferId = 1; + optional offer.Offer bisqEasyOffer = 1; } message BisqEasyPublicChatChannel { common.Market market = 1; diff --git a/common/src/main/java/bisq/common/encoding/Csv.java b/common/src/main/java/bisq/common/encoding/Csv.java new file mode 100644 index 0000000000..47e26cfe13 --- /dev/null +++ b/common/src/main/java/bisq/common/encoding/Csv.java @@ -0,0 +1,48 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.common.encoding; + +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class Csv { + public static String toCsv(List> data) { + StringBuilder sb = new StringBuilder(); + for (List row : data) { + for (int i = 0; i < row.size(); i++) { + sb.append(escapeSpecialCharacters(row.get(i))); + + if (i != row.size() - 1) { + sb.append(","); + } + } + sb.append(System.lineSeparator()); + } + return sb.toString(); + } + + public static String escapeSpecialCharacters(String value) { + if (value.contains(",") || value.contains("\"") || value.contains("'")) { + value = value.replace("\"", "\"\""); + value = "\"" + value + "\""; + } + return value; + } +} \ No newline at end of file diff --git a/contract/src/main/java/bisq/contract/bisq_easy/BisqEasyContract.java b/contract/src/main/java/bisq/contract/bisq_easy/BisqEasyContract.java index c12d1b3e53..ad464b6962 100644 --- a/contract/src/main/java/bisq/contract/bisq_easy/BisqEasyContract.java +++ b/contract/src/main/java/bisq/contract/bisq_easy/BisqEasyContract.java @@ -39,7 +39,7 @@ public class BisqEasyContract extends TwoPartyContract { private final long baseSideAmount; private final long quoteSideAmount; - protected final BitcoinPaymentMethodSpec baseSidePaymentMethodSpecs; + protected final BitcoinPaymentMethodSpec baseSidePaymentMethodSpec; protected final FiatPaymentMethodSpec quoteSidePaymentMethodSpec; private final Optional mediator; @@ -47,7 +47,7 @@ public BisqEasyContract(BisqEasyOffer offer, NetworkId takerNetworkId, long baseSideAmount, long quoteSideAmount, - BitcoinPaymentMethodSpec baseSidePaymentMethodSpecs, + BitcoinPaymentMethodSpec baseSidePaymentMethodSpec, FiatPaymentMethodSpec quoteSidePaymentMethodSpec, Optional mediator) { this(offer, @@ -55,7 +55,7 @@ public BisqEasyContract(BisqEasyOffer offer, new Party(Role.TAKER, takerNetworkId), baseSideAmount, quoteSideAmount, - baseSidePaymentMethodSpecs, + baseSidePaymentMethodSpec, quoteSidePaymentMethodSpec, mediator); } @@ -65,13 +65,13 @@ private BisqEasyContract(BisqEasyOffer offer, Party taker, long baseSideAmount, long quoteSideAmount, - BitcoinPaymentMethodSpec baseSidePaymentMethodSpecs, + BitcoinPaymentMethodSpec baseSidePaymentMethodSpec, FiatPaymentMethodSpec quoteSidePaymentMethodSpec, Optional mediator) { super(offer, protocolType, taker); this.baseSideAmount = baseSideAmount; this.quoteSideAmount = quoteSideAmount; - this.baseSidePaymentMethodSpecs = baseSidePaymentMethodSpecs; + this.baseSidePaymentMethodSpec = baseSidePaymentMethodSpec; this.quoteSidePaymentMethodSpec = quoteSidePaymentMethodSpec; this.mediator = mediator; } @@ -81,7 +81,7 @@ public bisq.contract.protobuf.Contract toProto() { var bisqEasyContract = bisq.contract.protobuf.BisqEasyContract.newBuilder() .setBaseSideAmount(baseSideAmount) .setQuoteSideAmount(quoteSideAmount) - .setBaseSidePaymentMethodSpecs(baseSidePaymentMethodSpecs.toProto()) + .setBaseSidePaymentMethodSpec(baseSidePaymentMethodSpec.toProto()) .setQuoteSidePaymentMethodSpec(quoteSidePaymentMethodSpec.toProto()); mediator.ifPresent(mediator -> bisqEasyContract.setMediator(mediator.toProto())); var twoPartyContract = getTwoPartyContractBuilder().setBisqEasyContract(bisqEasyContract); @@ -96,7 +96,7 @@ public static BisqEasyContract fromProto(bisq.contract.protobuf.Contract proto) Party.fromProto(twoPartyContractProto.getTaker()), bisqEasyContractProto.getBaseSideAmount(), bisqEasyContractProto.getQuoteSideAmount(), - PaymentMethodSpec.protoToBitcoinPaymentMethodSpec(bisqEasyContractProto.getBaseSidePaymentMethodSpecs()), + PaymentMethodSpec.protoToBitcoinPaymentMethodSpec(bisqEasyContractProto.getBaseSidePaymentMethodSpec()), PaymentMethodSpec.protoToFiatPaymentMethodSpec(bisqEasyContractProto.getQuoteSidePaymentMethodSpec()), bisqEasyContractProto.hasMediator() ? Optional.of(UserProfile.fromProto(bisqEasyContractProto.getMediator())) : diff --git a/contract/src/main/proto/contract.proto b/contract/src/main/proto/contract.proto index 6fa2dfc944..ccf4e50887 100644 --- a/contract/src/main/proto/contract.proto +++ b/contract/src/main/proto/contract.proto @@ -78,7 +78,7 @@ message SignedBisqEasyContract { message BisqEasyContract { uint64 baseSideAmount = 1; uint64 quoteSideAmount = 2; - offer.PaymentMethodSpec baseSidePaymentMethodSpecs = 3; + offer.PaymentMethodSpec baseSidePaymentMethodSpec = 3; offer.PaymentMethodSpec quoteSidePaymentMethodSpec = 4; optional user.UserProfile mediator = 12; } diff --git a/desktop/src/main/java/bisq/desktop/common/utils/FileChooserUtil.java b/desktop/src/main/java/bisq/desktop/common/utils/FileChooserUtil.java index 609555426f..e60dbac732 100644 --- a/desktop/src/main/java/bisq/desktop/common/utils/FileChooserUtil.java +++ b/desktop/src/main/java/bisq/desktop/common/utils/FileChooserUtil.java @@ -31,8 +31,9 @@ @Slf4j public class FileChooserUtil { @Nullable - public static File openFile(Scene scene) { + public static File openFile(Scene scene, String initialFileName) { FileChooser fileChooser = new FileChooser(); + fileChooser.setInitialFileName(initialFileName); String initialDirectory = SettingsService.getInstance().getCookie().asString(CookieKey.FILE_CHOOSER_DIR) .orElse(OsUtils.getDownloadOfHomeDir()); File initDir = new File(initialDirectory); diff --git a/desktop/src/main/java/bisq/desktop/components/controls/MaterialTextField.java b/desktop/src/main/java/bisq/desktop/components/controls/MaterialTextField.java index 67ff25305e..b080c77f4a 100644 --- a/desktop/src/main/java/bisq/desktop/components/controls/MaterialTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/controls/MaterialTextField.java @@ -49,6 +49,7 @@ public class MaterialTextField extends Pane { protected final Region selectionLine = new Region(); protected final Label descriptionLabel = new Label(); protected final TextInputControl textInputControl; + @Getter protected final Label helpLabel = new Label(); @Getter private final BisqIconButton iconButton = new BisqIconButton(); @@ -338,7 +339,8 @@ protected void onWidthChanged(double width) { line.setPrefWidth(width); selectionLine.setPrefWidth(textInputControl.isFocused() && textInputControl.isEditable() ? width : 0); descriptionLabel.setPrefWidth(width - 2 * descriptionLabel.getLayoutX()); - textInputControl.setPrefWidth(width - 2 * textInputControl.getLayoutX()); + double iconWidth = iconButton.isVisible() ? 25 : 0; + textInputControl.setPrefWidth(width - 2 * textInputControl.getLayoutX() - iconWidth); helpLabel.setPrefWidth(width - 2 * helpLabel.getLayoutX()); } } diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/components/ChatMessagesListView.java b/desktop/src/main/java/bisq/desktop/primary/main/content/components/ChatMessagesListView.java index d45050adda..c0a2f4466f 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/components/ChatMessagesListView.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/components/ChatMessagesListView.java @@ -46,10 +46,12 @@ import bisq.desktop.components.table.FilteredListItem; import bisq.desktop.primary.overlay.bisq_easy.take_offer.TakeOfferController; import bisq.i18n.Res; -import bisq.offer.bisq_easy.BisqEasyOfferService; +import bisq.network.NetworkId; +import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.presentation.formatters.DateFormatter; import bisq.settings.SettingsService; -import bisq.support.MediationService; +import bisq.trade.Trade; +import bisq.trade.bisq_easy.BisqEasyTradeService; import bisq.user.identity.UserIdentity; import bisq.user.identity.UserIdentityService; import bisq.user.profile.UserProfile; @@ -127,7 +129,6 @@ private static class Controller implements bisq.desktop.common.view.Controller { private final UserIdentityService userIdentityService; private final UserProfileService userProfileService; private final ReputationService reputationService; - private final MediationService mediationService; private final SettingsService settingsService; private final Consumer mentionUserHandler; private final Consumer replyHandler; @@ -136,7 +137,7 @@ private static class Controller implements bisq.desktop.common.view.Controller { @Getter private final View view; private final ChatNotificationService chatNotificationService; - private final BisqEasyOfferService bisqEasyOfferService; + private final BisqEasyTradeService bisqEasyTradeService; private Pin selectedChannelPin, chatMessagesPin, offerOnlySettingsPin; private Subscription selectedChannelSubscription, focusSubscription; @@ -146,13 +147,12 @@ private Controller(DefaultApplicationService applicationService, Consumer replyHandler, ChatChannelDomain chatChannelDomain) { chatService = applicationService.getChatService(); - bisqEasyOfferService = applicationService.getOfferService().getBisqEasyOfferService(); chatNotificationService = chatService.getChatNotificationService(); userIdentityService = applicationService.getUserService().getUserIdentityService(); userProfileService = applicationService.getUserService().getUserProfileService(); reputationService = applicationService.getUserService().getReputationService(); - mediationService = applicationService.getSupportService().getMediationService(); settingsService = applicationService.getSettingsService(); + bisqEasyTradeService = applicationService.getTradeService().getBisqEasyTradeService(); this.mentionUserHandler = mentionUserHandler; this.showChatUserDetailsHandler = showChatUserDetailsHandler; this.replyHandler = replyHandler; @@ -268,13 +268,16 @@ private void onReply(ChatMessage chatMessage) { // UI - handler /////////////////////////////////////////////////////////////////////////////////////////////////// - private void onTakeOffer(BisqEasyPublicChatMessage chatMessage) { + private void onTakeOffer(BisqEasyPublicChatMessage chatMessage, boolean canTakeOffer) { + if (!canTakeOffer) { + new Popup().information(Res.get("chat.message.offer.offerAlreadyTaken.warn")).show(); + return; + } checkArgument(!model.isMyMessage(chatMessage), "tradeChatMessage must not be mine"); - checkArgument(chatMessage.getBisqEasyOfferId().isPresent(), "message must contain offer"); - chatMessage.getBisqEasyOfferId() - .flatMap(bisqEasyOfferService::findOffer) - .ifPresent(bisqEasyOffer -> - Navigation.navigateTo(NavigationTarget.TAKE_OFFER, new TakeOfferController.InitData(bisqEasyOffer))); + checkArgument(chatMessage.getBisqEasyOffer().isPresent(), "message must contain offer"); + + BisqEasyOffer bisqEasyOffer = chatMessage.getBisqEasyOffer().get(); + Navigation.navigateTo(NavigationTarget.TAKE_OFFER, new TakeOfferController.InitData(bisqEasyOffer)); } private void onDeleteMessage(ChatMessage chatMessage) { @@ -300,7 +303,6 @@ private void doDeleteMessage(ChatMessage chatMessage, UserIdentity userIdentity) checkArgument(chatMessage instanceof PublicChatMessage); if (chatMessage instanceof BisqEasyPublicChatMessage bisqEasyPublicChatMessage) { - bisqEasyPublicChatMessage.getBisqEasyOfferId().ifPresent(bisqEasyOfferService::removeOffer); chatService.getBisqEasyPublicChatChannelService().deleteChatMessage(bisqEasyPublicChatMessage, userIdentity) .whenComplete((result, throwable) -> { if (throwable != null) { @@ -404,7 +406,8 @@ private void applyPredicate() { private > Pin bindChatMessages(C channel) { return FxBindings.>bind(model.chatMessages) - .map(chatMessage -> new ChatMessageListItem<>(chatMessage, userProfileService, reputationService)) + .map(chatMessage -> new ChatMessageListItem<>(chatMessage, userProfileService, reputationService, + bisqEasyTradeService, userIdentityService)) .to(channel.getChatMessages()); } @@ -513,7 +516,6 @@ public ListCell> call(ListView item, bo VBox reputationVBox = new VBox(4, reputationLabel, reputationScoreDisplay); reputationVBox.setAlignment(Pos.CENTER_LEFT); - takeOfferButton.setOnAction(e -> controller.onTakeOffer((BisqEasyPublicChatMessage) chatMessage)); + BisqEasyPublicChatMessage bisqEasyPublicChatMessage = (BisqEasyPublicChatMessage) chatMessage; + takeOfferButton.setOnAction(e -> controller.onTakeOffer(bisqEasyPublicChatMessage, item.isCanTakeOffer())); + takeOfferButton.setDefaultButton(item.isCanTakeOffer()); VBox messageVBox = new VBox(quotedMessageVBox, message); HBox.setMargin(userProfileIconVbox, new Insets(-5, 0, -5, 0)); @@ -907,8 +911,13 @@ public static class ChatMessageListItem implements Compar private final String nickName; @EqualsAndHashCode.Exclude private final ReputationScore reputationScore; + private final boolean canTakeOffer; - public ChatMessageListItem(T chatMessage, UserProfileService userProfileService, ReputationService reputationService) { + public ChatMessageListItem(T chatMessage, + UserProfileService userProfileService, + ReputationService reputationService, + BisqEasyTradeService bisqEasyTradeService, + UserIdentityService userIdentityService) { this.chatMessage = chatMessage; if (chatMessage instanceof PrivateChatMessage) { @@ -925,6 +934,21 @@ public ChatMessageListItem(T chatMessage, UserProfileService userProfileService, nickName = senderUserProfile.map(UserProfile::getNickName).orElse(""); reputationScore = senderUserProfile.flatMap(reputationService::findReputationScore).orElse(ReputationScore.NONE); + + if (chatMessage instanceof BisqEasyPublicChatMessage) { + BisqEasyPublicChatMessage bisqEasyPublicChatMessage = (BisqEasyPublicChatMessage) chatMessage; + if (userIdentityService.getSelectedUserIdentity() != null && bisqEasyPublicChatMessage.getBisqEasyOffer().isPresent()) { + UserProfile userProfile = userIdentityService.getSelectedUserIdentity().getUserProfile(); + NetworkId takerNetworkId = userProfile.getNetworkId(); + BisqEasyOffer bisqEasyOffer = bisqEasyPublicChatMessage.getBisqEasyOffer().get(); + String tradeId = Trade.createId(bisqEasyOffer.getId(), takerNetworkId.getId()); + canTakeOffer = bisqEasyTradeService.findTrade(tradeId).isEmpty(); + } else { + canTakeOffer = false; + } + } else { + canTakeOffer = false; + } } @Override diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/guide/rules/BisqEasyGuideRulesView.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/guide/rules/BisqEasyGuideRulesView.java index 8a946d2eab..5b4844def6 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/guide/rules/BisqEasyGuideRulesView.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/guide/rules/BisqEasyGuideRulesView.java @@ -70,8 +70,8 @@ public BisqEasyGuideRulesView(BisqEasyGuideRulesModel model, BisqEasyGuideRulesC protected void onViewAttached() { confirmCheckBox.setSelected(model.getTradeRulesConfirmed().get()); - confirmCheckBox.visibleProperty().bind(model.getTradeRulesConfirmed().not()); - confirmCheckBox.managedProperty().bind(model.getTradeRulesConfirmed().not()); + confirmCheckBox.setVisible(!model.getTradeRulesConfirmed().get()); + confirmCheckBox.setManaged(!model.getTradeRulesConfirmed().get()); tradeRulesConfirmedPin = EasyBind.subscribe(model.getTradeRulesConfirmed(), tradeRulesConfirmed -> { closeButton.setDefaultButton(tradeRulesConfirmed); @@ -93,9 +93,6 @@ protected void onViewAttached() { @Override protected void onViewDetached() { - confirmCheckBox.visibleProperty().unbind(); - confirmCheckBox.managedProperty().unbind(); - tradeRulesConfirmedPin.unsubscribe(); widthPin.unsubscribe(); diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradePhaseBox.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradePhaseBox.java index 5c6d4cdda7..12e41b1b84 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradePhaseBox.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradePhaseBox.java @@ -115,34 +115,26 @@ private void setBisqEasyTrade(BisqEasyTrade bisqEasyTrade) { switch (state) { case INIT: break; - case TAKER_SEND_TAKE_OFFER_REQUEST: - case MAKER_RECEIVED_TAKE_OFFER_REQUEST: + case TAKER_SENT_TAKE_OFFER_REQUEST: + case MAKER_SENT_TAKE_OFFER_RESPONSE: + case TAKER_RECEIVED_TAKE_OFFER_RESPONSE: model.getPhaseIndex().set(0); break; case SELLER_SENT_ACCOUNT_DATA: - model.getPhaseIndex().set(1); - break; case BUYER_RECEIVED_ACCOUNT_DATA: model.getPhaseIndex().set(1); break; case BUYER_SENT_FIAT_SENT_CONFIRMATION: - model.getPhaseIndex().set(2); - break; case SELLER_RECEIVED_FIAT_SENT_CONFIRMATION: model.getPhaseIndex().set(2); break; case SELLER_SENT_BTC_SENT_CONFIRMATION: - model.getPhaseIndex().set(3); - break; case BUYER_RECEIVED_BTC_SENT_CONFIRMATION: model.getPhaseIndex().set(3); break; case BTC_CONFIRMED: model.getPhaseIndex().set(4); break; - case COMPLETED: - //todo - break; } int phaseIndex = model.getPhaseIndex().get(); model.getDisputeButtonVisible().set(phaseIndex == 2 || phaseIndex == 3); diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradeStateController.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradeStateController.java index ed77de1b24..44cd8b5bc5 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradeStateController.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/TradeStateController.java @@ -57,7 +57,6 @@ public class TradeStateController implements Controller { private final SettingsService settingsService; private final BisqEasyTradeService bisqEasyTradeService; private final DefaultApplicationService applicationService; - private final TradeWelcome tradeWelcome; private final TradePhaseBox tradePhaseBox; private Subscription isCollapsedPin; private Pin tradeRulesConfirmedPin, bisqEasyTradeStatePin; @@ -69,7 +68,7 @@ public TradeStateController(DefaultApplicationService applicationService, Consum settingsService = applicationService.getSettingsService(); bisqEasyTradeService = applicationService.getTradeService().getBisqEasyTradeService(); - tradeWelcome = new TradeWelcome(); + TradeWelcome tradeWelcome = new TradeWelcome(); tradePhaseBox = new TradePhaseBox(applicationService); model = new TradeStateModel(); @@ -77,8 +76,6 @@ public TradeStateController(DefaultApplicationService applicationService, Consum } public void setSelectedChannel(BisqEasyPrivateTradeChatChannel channel) { - - UserIdentity myUserIdentity = channel.getMyUserIdentity(); BisqEasyOffer bisqEasyOffer = channel.getBisqEasyOffer(); boolean maker = isMaker(bisqEasyOffer); @@ -106,8 +103,9 @@ public void setSelectedChannel(BisqEasyPrivateTradeChatChannel channel) { switch (state) { case INIT: break; - case TAKER_SEND_TAKE_OFFER_REQUEST: - case MAKER_RECEIVED_TAKE_OFFER_REQUEST: + case TAKER_SENT_TAKE_OFFER_REQUEST: + case MAKER_SENT_TAKE_OFFER_RESPONSE: + case TAKER_RECEIVED_TAKE_OFFER_RESPONSE: if (isSeller) { model.getStateInfoVBox().set(new SellerState1(applicationService, bisqEasyTrade, channel).getView().getRoot()); } else { @@ -133,11 +131,11 @@ public void setSelectedChannel(BisqEasyPrivateTradeChatChannel channel) { model.getStateInfoVBox().set(new BuyerState4(applicationService, bisqEasyTrade, channel).getView().getRoot()); break; case BTC_CONFIRMED: - model.getStateInfoVBox().set(new SellerState5(applicationService, bisqEasyTrade, channel).getView().getRoot()); - model.getStateInfoVBox().set(new BuyerState5(applicationService, bisqEasyTrade, channel).getView().getRoot()); - break; - case COMPLETED: - //todo + if (isSeller) { + model.getStateInfoVBox().set(new SellerState5(applicationService, bisqEasyTrade, channel).getView().getRoot()); + } else { + model.getStateInfoVBox().set(new BuyerState5(applicationService, bisqEasyTrade, channel).getView().getRoot()); + } break; default: log.error(state.name()); @@ -158,18 +156,6 @@ public void setSelectedChannel(BisqEasyPrivateTradeChatChannel channel) { baseAmountString, quoteAmountString, paymentMethodName)); - - /* if (model.getAppliedPhaseIndex() == phaseIndex) { - return; - } - model.setAppliedPhaseIndex(phaseIndex); - boolean tradeRulesConfirmed = getTradeRulesConfirmed(); - boolean showFirstTimeItems = phaseIndex == 0 && !tradeRulesConfirmed; - applyFirstTimeItemsVisible(); - applyPhaseAndInfoBoxVisible(); - if (showFirstTimeItems) { - return; - }*/ } @Override @@ -215,16 +201,4 @@ private void applyVisibility() { model.getTradeWelcomeVisible().set(isExpanded && !tradeRulesConfirmed); model.getPhaseAndInfoBoxVisible().set(isExpanded && tradeRulesConfirmed); } - - private void applyFirstTimeItemsVisible() { - // model.getFirstTimeItemsVisible().set(!model.getIsCollapsed().get() && model.getPhaseIndex().get() == 0 && !getTradeRulesConfirmed()); - } - - private void applyPhaseAndInfoBoxVisible() { - model.getPhaseAndInfoBoxVisible().set(!model.getIsCollapsed().get() && !model.getTradeWelcomeVisible().get()); - } - - private Boolean getTradeRulesConfirmed() { - return settingsService.getTradeRulesConfirmed().get(); - } } diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState2.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState2.java index 1711c2c2c7..bbbcbc0310 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState2.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState2.java @@ -67,6 +67,7 @@ protected View createView() { @Override public void onActivate() { model.getButtonDisabled().bind(model.getBtcAddress().isEmpty()); + super.onActivate(); } diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState4.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState4.java index fc02dc1d5d..bd75b791bb 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState4.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState4.java @@ -19,13 +19,22 @@ import bisq.application.DefaultApplicationService; import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannel; +import bisq.common.monetary.Coin; +import bisq.desktop.common.Browser; import bisq.desktop.common.threading.UIScheduler; +import bisq.desktop.common.threading.UIThread; import bisq.desktop.components.controls.BisqText; import bisq.desktop.components.controls.MaterialTextField; import bisq.desktop.components.overlay.Popup; import bisq.i18n.Res; +import bisq.oracle.explorer.ExplorerService; +import bisq.oracle.explorer.dto.Output; +import bisq.presentation.formatters.AmountFormatter; import bisq.trade.TradeException; import bisq.trade.bisq_easy.BisqEasyTrade; +import de.jensd.fx.fontawesome.AwesomeIcon; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; @@ -35,6 +44,8 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.TimeUnit; + @Slf4j public class BuyerState4 extends BaseState { private final Controller controller; @@ -48,8 +59,13 @@ public View getView() { } private static class Controller extends BaseState.Controller { + private final ExplorerService explorerService; + private UIScheduler scheduler; + private Controller(DefaultApplicationService applicationService, BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(applicationService, bisqEasyTrade, channel); + + explorerService = applicationService.getOracleService().getExplorerService(); } @Override @@ -69,29 +85,23 @@ public void onActivate() { model.setTxId(model.getBisqEasyTrade().getTxId().get()); model.setBtcAddress(model.getBisqEasyTrade().getBtcAddress().get()); model.getBtcBalance().set(""); - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notInMempoolYet")); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation", 0)); - model.getBtcBalance().set(model.getFormattedBaseAmount()); - }) - .after(2000); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation", 1)); - sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", - model.getFormattedBaseAmount(), model.getBtcAddress())); - }) - .after(4000); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation.plural", 2)); - sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", - model.getFormattedBaseAmount(), model.getBtcAddress())); - }) - .after(6000); + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.explorerLookup")); + requestTx(); } @Override public void onDeactivate() { super.onDeactivate(); + if (scheduler != null) { + scheduler.stop(); + scheduler = null; + } + } + + public void openExplorer() { + ExplorerService.Provider provider = explorerService.getSelectedProvider().get(); + String url = provider.getBaseUrl() + provider.getTxPath() + model.getTxId(); + Browser.open(url); } private void onComplete() { @@ -102,13 +112,46 @@ private void onComplete() { } } - /* private void onBtcConfirmed() { + private void requestTx() { + explorerService.requestTx(model.getTxId()) + .whenComplete((tx, throwable) -> { + UIThread.run(() -> { + if (scheduler != null) { + scheduler.stop(); + } + if (throwable == null) { + model.btcBalance.set( + tx.getOutputs().stream() + .filter(output -> output.getAddress().equals(model.getBtcAddress())) + .map(Output::getValue) + .map(Coin::asBtcFromValue) + .map(e -> AmountFormatter.formatAmountWithCode(e, false)) + .findAny() + .orElse("")); + model.getIsConfirmed().set(tx.getStatus().isConfirmed()); + if (tx.getStatus().isConfirmed()) { + onConfirmed(); + } else { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notConfirmed")); + scheduler = UIScheduler.run(this::requestTx).after(20, TimeUnit.SECONDS); + } + } else { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.txId.failed")); + log.warn("Transaction lookup failed", throwable); + } + }); + }); + } + + private void onConfirmed() { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmed")); + sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", model.getFormattedBaseAmount(), model.btcAddress)); try { bisqEasyTradeService.btcConfirmed(model.getBisqEasyTrade()); } catch (TradeException e) { new Popup().error(e).show(); } - }*/ + } } @Getter @@ -118,7 +161,8 @@ private static class Model extends BaseState.Model { @Setter protected String txId; private final StringProperty btcBalance = new SimpleStringProperty(); - private final StringProperty confirmations = new SimpleStringProperty(); + private final StringProperty confirmationState = new SimpleStringProperty(); + private final BooleanProperty isConfirmed = new SimpleBooleanProperty(); protected Model(BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(bisqEasyTrade, channel); @@ -135,12 +179,13 @@ private View(Model model, Controller controller) { BisqText infoHeadline = new BisqText(Res.get("bisqEasy.tradeState.info.buyer.phase4.headline")); infoHeadline.getStyleClass().add("bisq-easy-trade-state-info-headline"); - txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.buyer.phase4.txId"), "", false); + txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.phase4.txId"), "", false); + txId.setIcon(AwesomeIcon.EXTERNAL_LINK); + txId.setIconTooltip(Res.get("bisqEasy.tradeState.info.phase4.txId.tooltip")); btcBalance = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.buyer.phase4.balance"), "", false); - btcBalance.setHelpText(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notInMempoolYet")); + btcBalance.setHelpText(Res.get("bisqEasy.tradeState.info.phase4.balance.help.explorerLookup")); button = new Button(Res.get("bisqEasy.tradeState.info.phase4.buttonText")); - button.setDefaultButton(true); VBox.setMargin(button, new Insets(5, 0, 5, 0)); root.getChildren().addAll( @@ -156,18 +201,25 @@ protected void onViewAttached() { super.onViewAttached(); txId.setText(model.getTxId()); + + button.defaultButtonProperty().bind(model.isConfirmed); btcBalance.textProperty().bind(model.getBtcBalance()); - btcBalance.helpHelpProperty().bind(model.getConfirmations()); + btcBalance.helpHelpProperty().bind(model.getConfirmationState()); + button.setOnAction(e -> controller.onComplete()); + txId.getIconButton().setOnAction(e -> controller.openExplorer()); } @Override protected void onViewDetached() { super.onViewDetached(); + button.defaultButtonProperty().unbind(); btcBalance.textProperty().unbind(); btcBalance.helpHelpProperty().unbind(); + button.setOnAction(null); + txId.getIconButton().setOnAction(null); } } } \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState5.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState5.java index 1b31d8bd32..363345b4d1 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState5.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/BuyerState5.java @@ -18,19 +18,33 @@ package bisq.desktop.primary.main.content.trade_apps.bisqEasy.chat.trade_state.states; import bisq.application.DefaultApplicationService; +import bisq.chat.bisqeasy.channel.BisqEasyChatChannelSelectionService; import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannel; +import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannelService; +import bisq.common.encoding.Csv; +import bisq.common.util.FileUtils; +import bisq.desktop.common.Browser; +import bisq.desktop.common.utils.FileChooserUtil; import bisq.desktop.components.controls.BisqText; import bisq.desktop.components.controls.MaterialTextField; import bisq.desktop.components.overlay.Popup; import bisq.i18n.Res; -import bisq.trade.TradeException; +import bisq.oracle.explorer.ExplorerService; +import bisq.settings.DontShowAgainService; import bisq.trade.bisq_easy.BisqEasyTrade; +import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.geometry.Insets; import javafx.scene.control.Button; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.io.File; +import java.io.IOException; +import java.util.List; + @Slf4j public class BuyerState5 extends BaseState { private final Controller controller; @@ -44,8 +58,17 @@ public View getView() { } private static class Controller extends BaseState.Controller { + private final ExplorerService explorerService; + private final BisqEasyPrivateTradeChatChannelService bisqEasyPrivateTradeChatChannelService; + private final BisqEasyChatChannelSelectionService bisqEasyChatChannelSelectionService; + private Controller(DefaultApplicationService applicationService, BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(applicationService, bisqEasyTrade, channel); + + explorerService = applicationService.getOracleService().getExplorerService(); + bisqEasyPrivateTradeChatChannelService = applicationService.getChatService().getBisqEasyPrivateTradeChatChannelService(); + bisqEasyChatChannelSelectionService = applicationService.getChatService().getBisqEasyChatChannelSelectionService(); + } @Override @@ -61,6 +84,7 @@ protected View createView() { @Override public void onActivate() { super.onActivate(); + model.setTxId(model.getBisqEasyTrade().getTxId().get()); } @Override @@ -68,25 +92,78 @@ public void onDeactivate() { super.onDeactivate(); } - private void onTradeCompleted() { - try { - bisqEasyTradeService.tradeCompleted(model.getBisqEasyTrade()); - } catch (TradeException e) { - new Popup().error(e).show(); + private void onLeaveChannel() { + String dontShowAgainId = "leaveTradeChannel"; + if (DontShowAgainService.showAgain(dontShowAgainId)) { + new Popup().warning(Res.get("bisqEasy.channelSelection.private.leave.warn", + model.getChannel().getPeer().getUserName())) + .dontShowAgainId(dontShowAgainId) + .closeButtonText(Res.get("action.cancel")) + .actionButtonText(Res.get("bisqEasy.channelSelection.private.leave")) + .onAction(this::doLeaveChannel) + .show(); + } else { + doLeaveChannel(); + } + } + + private void doLeaveChannel() { + bisqEasyPrivateTradeChatChannelService.leaveChannel(model.getChannel()); + bisqEasyChatChannelSelectionService.maybeSelectFirstChannel(); + } + + private void onExportTrade() { + List> tradeData = List.of( + List.of( + "Trade ID", + "BTC amount", + model.getQuoteCode() + " amount", + "Transaction ID", + "Receiver address", + "Payment method" + ), + List.of( + model.getBisqEasyTrade().getId(), + model.getFormattedBaseAmount(), + model.getFormattedQuoteAmount(), + model.getTxId(), + model.getBisqEasyTrade().getBtcAddress().get(), + model.getBisqEasyTrade().getContract().getQuoteSidePaymentMethodSpec().getDisplayString() + ) + ); + + String csv = Csv.toCsv(tradeData); + File file = FileChooserUtil.openFile(getView().getRoot().getScene(), "BisqEasyTrade.csv"); + if (file != null) { + try { + FileUtils.writeToFile(csv, file); + } catch (IOException e) { + new Popup().error(e).show(); + } } } + + private void openExplorer() { + ExplorerService.Provider provider = explorerService.getSelectedProvider().get(); + String url = provider.getBaseUrl() + provider.getTxPath() + model.getTxId(); + Browser.open(url); + } } @Getter private static class Model extends BaseState.Model { + @Setter + protected String txId; + protected Model(BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(bisqEasyTrade, channel); } } public static class View extends BaseState.View { - private final Button button; + private final Button leaveButton, exportButton; private final MaterialTextField quoteAmount, baseAmount; + private final MaterialTextField txId; private View(Model model, Controller controller) { super(model, controller); @@ -94,34 +171,46 @@ private View(Model model, Controller controller) { BisqText infoHeadline = new BisqText(Res.get("bisqEasy.tradeState.info.buyer.phase5.headline")); infoHeadline.getStyleClass().add("bisq-easy-trade-state-info-headline"); - button = new Button(Res.get("bisqEasy.tradeState.info.phase5.buttonText")); - button.setDefaultButton(true); - quoteAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.seller.phase5.quoteAmount"), "", false); - baseAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.seller.phase5.baseAmount"), "", false); + exportButton = new Button(Res.get("bisqEasy.tradeState.info.phase5.exportTrade")); + leaveButton = new Button(Res.get("bisqEasy.tradeState.info.phase5.leaveChannel")); + quoteAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.buyer.phase5.quoteAmount"), "", false); + baseAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.buyer.phase5.baseAmount"), "", false); + + txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.phase4.txId"), "", false); + txId.setIcon(AwesomeIcon.EXTERNAL_LINK); + txId.setIconTooltip(Res.get("bisqEasy.tradeState.info.phase4.txId.tooltip")); - VBox.setMargin(button, new Insets(5, 0, 5, 0)); + HBox buttons = new HBox(20, exportButton, leaveButton); + VBox.setMargin(buttons, new Insets(5, 0, 5, 0)); root.getChildren().addAll( infoHeadline, quoteAmount, baseAmount, - button); + txId, + buttons); } @Override protected void onViewAttached() { super.onViewAttached(); + txId.setText(model.getTxId()); + quoteAmount.setText(model.getFormattedQuoteAmount()); baseAmount.setText(model.getFormattedBaseAmount()); - button.setOnAction(e -> controller.onTradeCompleted()); + leaveButton.setOnAction(e -> controller.onLeaveChannel()); + exportButton.setOnAction(e -> controller.onExportTrade()); + txId.getIconButton().setOnAction(e -> controller.openExplorer()); } @Override protected void onViewDetached() { super.onViewDetached(); - button.setOnAction(null); + leaveButton.setOnAction(null); + exportButton.setOnAction(null); + txId.getIconButton().setOnAction(null); } } } \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState1.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState1.java index b08fed6859..4da6869418 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState1.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState1.java @@ -17,25 +17,38 @@ package bisq.desktop.primary.main.content.trade_apps.bisqEasy.chat.trade_state.states; +import bisq.account.accounts.Account; +import bisq.account.accounts.UserDefinedFiatAccount; import bisq.application.DefaultApplicationService; import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannel; +import bisq.common.observable.Pin; import bisq.desktop.components.containers.Spacer; +import bisq.desktop.components.controls.AutoCompleteComboBox; import bisq.desktop.components.controls.BisqText; import bisq.desktop.components.controls.MaterialTextArea; import bisq.desktop.components.overlay.Popup; import bisq.i18n.Res; import bisq.trade.TradeException; import bisq.trade.bisq_easy.BisqEasyTrade; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; +import javafx.util.StringConverter; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; @Slf4j public class SellerState1 extends BaseState { @@ -50,6 +63,8 @@ public View getView() { } private static class Controller extends BaseState.Controller { + private Pin accountsPin, selectedAccountPin; + private Controller(DefaultApplicationService applicationService, BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(applicationService, bisqEasyTrade, channel); } @@ -67,6 +82,26 @@ protected View createView() { @Override public void onActivate() { super.onActivate(); + + model.getSortedAccounts().setComparator(Comparator.comparing(Account::getAccountName)); + + accountsPin = accountService.getAccounts().addListener(() -> { + List accounts = accountService.getAccounts().stream() + .filter(account -> account instanceof UserDefinedFiatAccount) + .map(account -> (UserDefinedFiatAccount) account) + .collect(Collectors.toList()); + model.setAllAccounts(accounts); + model.getAccountSelectionVisible().set(accounts.size() > 1); + maybeSelectFirstAccount(); + }); + selectedAccountPin = accountService.selectedAccountAsObservable().addObserver(account -> { + if (account instanceof UserDefinedFiatAccount) { + UserDefinedFiatAccount userDefinedFiatAccount = (UserDefinedFiatAccount) account; + model.selectedAccountProperty().set(userDefinedFiatAccount); + model.getPaymentAccountData().set(userDefinedFiatAccount.getAccountPayload().getAccountData()); + } + }); + model.getButtonDisabled().bind(model.getPaymentAccountData().isEmpty()); findUsersAccountData().ifPresent(accountData -> model.getPaymentAccountData().set(accountData)); } @@ -74,6 +109,8 @@ public void onActivate() { @Override public void onDeactivate() { super.onDeactivate(); + accountsPin.unbind(); + selectedAccountPin.unbind(); model.getButtonDisabled().unbind(); } @@ -86,21 +123,52 @@ private void onSendPaymentData() { new Popup().error(e).show(); } } + + private void onSelectAccount(UserDefinedFiatAccount account) { + if (account != null) { + accountService.setSelectedAccount(account); + } + } + + private void maybeSelectFirstAccount() { + if (!model.getSortedAccounts().isEmpty() && accountService.getSelectedAccount() == null) { + accountService.setSelectedAccount(model.getSortedAccounts().get(0)); + } + } } @Getter private static class Model extends BaseState.Model { private final StringProperty paymentAccountData = new SimpleStringProperty(); private final BooleanProperty buttonDisabled = new SimpleBooleanProperty(); + private final BooleanProperty accountSelectionVisible = new SimpleBooleanProperty(); + private final ObservableList accounts = FXCollections.observableArrayList(); + private final SortedList sortedAccounts = new SortedList<>(accounts); + private final ObjectProperty selectedAccount = new SimpleObjectProperty<>(); protected Model(BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(bisqEasyTrade, channel); } + + @Nullable + public UserDefinedFiatAccount getSelectedAccount() { + return selectedAccount.get(); + } + + public ObjectProperty selectedAccountProperty() { + return selectedAccount; + } + + public void setAllAccounts(Collection collection) { + accounts.setAll(collection); + } } public static class View extends BaseState.View { private final Button button; private final MaterialTextArea paymentAccountData; + private final AutoCompleteComboBox accountSelection; + private Subscription selectedAccountPin; private View(Model model, Controller controller) { super(model, controller); @@ -108,6 +176,20 @@ private View(Model model, Controller controller) { BisqText infoHeadline = new BisqText(Res.get("bisqEasy.tradeState.info.seller.phase1.headline")); infoHeadline.getStyleClass().add("bisq-easy-trade-state-info-headline"); + accountSelection = new AutoCompleteComboBox<>(model.getSortedAccounts(), Res.get("user.paymentAccounts.selectAccount")); + accountSelection.setPrefWidth(300); + accountSelection.setConverter(new StringConverter<>() { + @Override + public String toString(UserDefinedFiatAccount object) { + return object != null ? object.getAccountName() : ""; + } + + @Override + public UserDefinedFiatAccount fromString(String string) { + return null; + } + }); + paymentAccountData = FormUtils.addTextArea(Res.get("bisqEasy.tradeState.info.seller.phase1.accountData"), "", true); paymentAccountData.setPromptText(Res.get("bisqEasy.tradeState.info.seller.phase1.accountData.prompt")); @@ -119,6 +201,7 @@ private View(Model model, Controller controller) { VBox.setMargin(button, new Insets(5, 0, 5, 0)); root.getChildren().addAll( infoHeadline, + accountSelection, paymentAccountData, button, Spacer.fillVBox(), @@ -131,6 +214,20 @@ protected void onViewAttached() { paymentAccountData.textProperty().bindBidirectional(model.getPaymentAccountData()); button.disableProperty().bind(model.getButtonDisabled()); + accountSelection.visibleProperty().bind(model.getAccountSelectionVisible()); + accountSelection.managedProperty().bind(model.getAccountSelectionVisible()); + + selectedAccountPin = EasyBind.subscribe(model.selectedAccountProperty(), + accountName -> accountSelection.getSelectionModel().select(accountName)); + + accountSelection.setOnChangeConfirmed(e -> { + if (accountSelection.getSelectionModel().getSelectedItem() == null) { + accountSelection.getSelectionModel().select(model.getSelectedAccount()); + return; + } + controller.onSelectAccount(accountSelection.getSelectionModel().getSelectedItem()); + }); + button.setOnAction(e -> controller.onSendPaymentData()); } @@ -140,6 +237,12 @@ protected void onViewDetached() { paymentAccountData.textProperty().unbindBidirectional(model.getPaymentAccountData()); button.disableProperty().unbind(); + accountSelection.visibleProperty().unbind(); + accountSelection.managedProperty().unbind(); + + selectedAccountPin.unsubscribe(); + + accountSelection.setOnChangeConfirmed(null); button.setOnAction(null); } } diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState4.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState4.java index 2f64ffd375..a6c9dccf33 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState4.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState4.java @@ -19,13 +19,22 @@ import bisq.application.DefaultApplicationService; import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannel; +import bisq.common.monetary.Coin; +import bisq.desktop.common.Browser; import bisq.desktop.common.threading.UIScheduler; +import bisq.desktop.common.threading.UIThread; import bisq.desktop.components.controls.BisqText; import bisq.desktop.components.controls.MaterialTextField; import bisq.desktop.components.overlay.Popup; import bisq.i18n.Res; +import bisq.oracle.explorer.ExplorerService; +import bisq.oracle.explorer.dto.Output; +import bisq.presentation.formatters.AmountFormatter; import bisq.trade.TradeException; import bisq.trade.bisq_easy.BisqEasyTrade; +import de.jensd.fx.fontawesome.AwesomeIcon; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; @@ -35,6 +44,8 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.TimeUnit; + @Slf4j public class SellerState4 extends BaseState { private final Controller controller; @@ -48,8 +59,13 @@ public View getView() { } private static class Controller extends BaseState.Controller { + private final ExplorerService explorerService; + private UIScheduler scheduler; + private Controller(DefaultApplicationService applicationService, BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(applicationService, bisqEasyTrade, channel); + + explorerService = applicationService.getOracleService().getExplorerService(); } @Override @@ -69,29 +85,23 @@ public void onActivate() { model.setTxId(model.getBisqEasyTrade().getTxId().get()); model.setBtcAddress(model.getBisqEasyTrade().getBtcAddress().get()); model.getBtcBalance().set(""); - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notInMempoolYet")); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation", 0)); - model.getBtcBalance().set(model.getFormattedBaseAmount()); - }) - .after(2000); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation", 1)); - sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", - model.getFormattedBaseAmount(), model.getBtcAddress())); - }) - .after(4000); - UIScheduler.run(() -> { - model.getConfirmations().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmation.plural", 2)); - sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", - model.getFormattedBaseAmount(), model.getBtcAddress())); - }) - .after(6000); + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.explorerLookup")); + requestTx(); } @Override public void onDeactivate() { super.onDeactivate(); + if (scheduler != null) { + scheduler.stop(); + scheduler = null; + } + } + + public void openExplorer() { + ExplorerService.Provider provider = explorerService.getSelectedProvider().get(); + String url = provider.getBaseUrl() + provider.getTxPath() + model.getTxId(); + Browser.open(url); } private void onComplete() { @@ -102,13 +112,46 @@ private void onComplete() { } } - /* private void onBtcConfirmed() { + private void requestTx() { + explorerService.requestTx(model.getTxId()) + .whenComplete((tx, throwable) -> { + UIThread.run(() -> { + if (scheduler != null) { + scheduler.stop(); + } + if (throwable == null) { + model.btcBalance.set( + tx.getOutputs().stream() + .filter(output -> output.getAddress().equals(model.getBtcAddress())) + .map(Output::getValue) + .map(Coin::asBtcFromValue) + .map(e -> AmountFormatter.formatAmountWithCode(e, false)) + .findAny() + .orElse("")); + model.getIsConfirmed().set(tx.getStatus().isConfirmed()); + if (tx.getStatus().isConfirmed()) { + onConfirmed(); + } else { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notConfirmed")); + scheduler = UIScheduler.run(this::requestTx).after(20, TimeUnit.SECONDS); + } + } else { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.txId.failed")); + log.warn("Transaction lookup failed", throwable); + } + }); + }); + } + + private void onConfirmed() { + model.getConfirmationState().set(Res.get("bisqEasy.tradeState.info.phase4.balance.help.confirmed")); + sendChatBotMessage(Res.get("bisqEasy.tradeState.info.phase4.chatBotMessage", model.getFormattedBaseAmount(), model.btcAddress)); try { bisqEasyTradeService.btcConfirmed(model.getBisqEasyTrade()); } catch (TradeException e) { new Popup().error(e).show(); } - }*/ + } } @Getter @@ -118,7 +161,8 @@ private static class Model extends BaseState.Model { @Setter protected String txId; private final StringProperty btcBalance = new SimpleStringProperty(); - private final StringProperty confirmations = new SimpleStringProperty(); + private final StringProperty confirmationState = new SimpleStringProperty(); + private final BooleanProperty isConfirmed = new SimpleBooleanProperty(); protected Model(BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(bisqEasyTrade, channel); @@ -135,12 +179,13 @@ private View(Model model, Controller controller) { BisqText infoHeadline = new BisqText(Res.get("bisqEasy.tradeState.info.seller.phase4.headline")); infoHeadline.getStyleClass().add("bisq-easy-trade-state-info-headline"); - txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.buyer.phase4.txId"), "", false); + txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.phase4.txId"), "", false); + txId.setIcon(AwesomeIcon.EXTERNAL_LINK); + txId.setIconTooltip(Res.get("bisqEasy.tradeState.info.phase4.txId.tooltip")); btcBalance = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.seller.phase4.balance"), "", false); - btcBalance.setHelpText(Res.get("bisqEasy.tradeState.info.phase4.balance.help.notInMempoolYet")); + btcBalance.setHelpText(Res.get("bisqEasy.tradeState.info.phase4.balance.help.explorerLookup")); button = new Button(Res.get("bisqEasy.tradeState.info.phase4.buttonText")); - button.setDefaultButton(true); VBox.setMargin(button, new Insets(5, 0, 5, 0)); root.getChildren().addAll( @@ -156,18 +201,25 @@ protected void onViewAttached() { super.onViewAttached(); txId.setText(model.getTxId()); + + button.defaultButtonProperty().bind(model.isConfirmed); btcBalance.textProperty().bind(model.getBtcBalance()); - btcBalance.helpHelpProperty().bind(model.getConfirmations()); + btcBalance.helpHelpProperty().bind(model.getConfirmationState()); + button.setOnAction(e -> controller.onComplete()); + txId.getIconButton().setOnAction(e -> controller.openExplorer()); } @Override protected void onViewDetached() { super.onViewDetached(); + button.defaultButtonProperty().unbind(); btcBalance.textProperty().unbind(); btcBalance.helpHelpProperty().unbind(); + button.setOnAction(null); + txId.getIconButton().setOnAction(null); } } } \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState5.java b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState5.java index 8fa9e2596f..ca56b43623 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState5.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/trade_apps/bisqEasy/chat/trade_state/states/SellerState5.java @@ -18,19 +18,33 @@ package bisq.desktop.primary.main.content.trade_apps.bisqEasy.chat.trade_state.states; import bisq.application.DefaultApplicationService; +import bisq.chat.bisqeasy.channel.BisqEasyChatChannelSelectionService; import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannel; +import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannelService; +import bisq.common.encoding.Csv; +import bisq.common.util.FileUtils; +import bisq.desktop.common.Browser; +import bisq.desktop.common.utils.FileChooserUtil; import bisq.desktop.components.controls.BisqText; import bisq.desktop.components.controls.MaterialTextField; import bisq.desktop.components.overlay.Popup; import bisq.i18n.Res; -import bisq.trade.TradeException; +import bisq.oracle.explorer.ExplorerService; +import bisq.settings.DontShowAgainService; import bisq.trade.bisq_easy.BisqEasyTrade; +import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.geometry.Insets; import javafx.scene.control.Button; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.io.File; +import java.io.IOException; +import java.util.List; + @Slf4j public class SellerState5 extends BaseState { private final Controller controller; @@ -44,8 +58,17 @@ public View getView() { } private static class Controller extends BaseState.Controller { + private final ExplorerService explorerService; + private final BisqEasyPrivateTradeChatChannelService bisqEasyPrivateTradeChatChannelService; + private final BisqEasyChatChannelSelectionService bisqEasyChatChannelSelectionService; + private Controller(DefaultApplicationService applicationService, BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(applicationService, bisqEasyTrade, channel); + + explorerService = applicationService.getOracleService().getExplorerService(); + bisqEasyPrivateTradeChatChannelService = applicationService.getChatService().getBisqEasyPrivateTradeChatChannelService(); + bisqEasyChatChannelSelectionService = applicationService.getChatService().getBisqEasyChatChannelSelectionService(); + } @Override @@ -61,6 +84,7 @@ protected View createView() { @Override public void onActivate() { super.onActivate(); + model.setTxId(model.getBisqEasyTrade().getTxId().get()); } @Override @@ -68,25 +92,78 @@ public void onDeactivate() { super.onDeactivate(); } - private void onTradeCompleted() { - try { - bisqEasyTradeService.tradeCompleted(model.getBisqEasyTrade()); - } catch (TradeException e) { - new Popup().error(e).show(); + private void onLeaveChannel() { + String dontShowAgainId = "leaveTradeChannel"; + if (DontShowAgainService.showAgain(dontShowAgainId)) { + new Popup().warning(Res.get("bisqEasy.channelSelection.private.leave.warn", + model.getChannel().getPeer().getUserName())) + .dontShowAgainId(dontShowAgainId) + .closeButtonText(Res.get("action.cancel")) + .actionButtonText(Res.get("bisqEasy.channelSelection.private.leave")) + .onAction(this::doLeaveChannel) + .show(); + } else { + doLeaveChannel(); } } + + private void doLeaveChannel() { + bisqEasyPrivateTradeChatChannelService.leaveChannel(model.getChannel()); + bisqEasyChatChannelSelectionService.maybeSelectFirstChannel(); + } + + private void onExportTrade() { + List> tradeData = List.of( + List.of( + "Trade ID", + "BTC amount", + model.getQuoteCode() + " amount", + "Transaction ID", + "Receiver address", + "Payment method" + ), + List.of( + model.getBisqEasyTrade().getId(), + model.getFormattedBaseAmount(), + model.getFormattedQuoteAmount(), + model.getTxId(), + model.getBisqEasyTrade().getBtcAddress().get(), + model.getBisqEasyTrade().getContract().getQuoteSidePaymentMethodSpec().getDisplayString() + ) + ); + + String csv = Csv.toCsv(tradeData); + File file = FileChooserUtil.openFile(getView().getRoot().getScene(), "BisqEasyTrade.csv"); + if (file != null) { + try { + FileUtils.writeToFile(csv, file); + } catch (IOException e) { + new Popup().error(e).show(); + } + } + } + + private void openExplorer() { + ExplorerService.Provider provider = explorerService.getSelectedProvider().get(); + String url = provider.getBaseUrl() + provider.getTxPath() + model.getTxId(); + Browser.open(url); + } } @Getter private static class Model extends BaseState.Model { + @Setter + protected String txId; + protected Model(BisqEasyTrade bisqEasyTrade, BisqEasyPrivateTradeChatChannel channel) { super(bisqEasyTrade, channel); } } public static class View extends BaseState.View { - private final Button button; + private final Button leaveButton, exportButton; private final MaterialTextField quoteAmount, baseAmount; + private final MaterialTextField txId; private View(Model model, Controller controller) { super(model, controller); @@ -94,33 +171,46 @@ private View(Model model, Controller controller) { BisqText infoHeadline = new BisqText(Res.get("bisqEasy.tradeState.info.seller.phase5.headline")); infoHeadline.getStyleClass().add("bisq-easy-trade-state-info-headline"); - button = new Button(Res.get("bisqEasy.tradeState.info.phase5.buttonText")); - button.setDefaultButton(true); + exportButton = new Button(Res.get("bisqEasy.tradeState.info.phase5.exportTrade")); + leaveButton = new Button(Res.get("bisqEasy.tradeState.info.phase5.leaveChannel")); quoteAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.seller.phase5.quoteAmount"), "", false); baseAmount = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.seller.phase5.baseAmount"), "", false); - VBox.setMargin(button, new Insets(5, 0, 5, 0)); + + txId = FormUtils.getTextField(Res.get("bisqEasy.tradeState.info.phase4.txId"), "", false); + txId.setIcon(AwesomeIcon.EXTERNAL_LINK); + txId.setIconTooltip(Res.get("bisqEasy.tradeState.info.phase4.txId.tooltip")); + + HBox buttons = new HBox(20, exportButton, leaveButton); + VBox.setMargin(buttons, new Insets(5, 0, 5, 0)); root.getChildren().addAll( infoHeadline, quoteAmount, baseAmount, - button); + txId, + buttons); } @Override protected void onViewAttached() { super.onViewAttached(); + txId.setText(model.getTxId()); + quoteAmount.setText(model.getFormattedQuoteAmount()); baseAmount.setText(model.getFormattedBaseAmount()); - button.setOnAction(e -> controller.onTradeCompleted()); + leaveButton.setOnAction(e -> controller.onLeaveChannel()); + exportButton.setOnAction(e -> controller.onExportTrade()); + txId.getIconButton().setOnAction(e -> controller.openExplorer()); } @Override protected void onViewDetached() { super.onViewDetached(); - button.setOnAction(null); + leaveButton.setOnAction(null); + exportButton.setOnAction(null); + txId.getIconButton().setOnAction(null); } } } \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/PaymentAccountsController.java b/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/PaymentAccountsController.java index 3bc44e2188..cf7f7e3243 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/PaymentAccountsController.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/PaymentAccountsController.java @@ -20,6 +20,7 @@ import bisq.account.AccountService; import bisq.account.accounts.Account; import bisq.account.accounts.UserDefinedFiatAccount; +import bisq.account.accounts.UserDefinedFiatAccountPayload; import bisq.account.payment_method.PaymentMethod; import bisq.application.DefaultApplicationService; import bisq.common.observable.Pin; @@ -34,6 +35,7 @@ import java.util.Comparator; import static bisq.desktop.common.view.NavigationTarget.CREATE_BISQ_EASY_PAYMENT_ACCOUNT; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -105,11 +107,13 @@ void onSaveAccount() { checkNotNull(model.getSelectedAccount()); String accountName = model.getSelectedAccount().getAccountName(); String accountData = model.getAccountData(); - UserDefinedFiatAccount newAccount = new UserDefinedFiatAccount(accountName, accountData); - - accountService.removePaymentAccount(model.getSelectedAccount()); - accountService.addPaymentAccount(newAccount); - accountService.setSelectedAccount(newAccount); + if (accountData != null) { + checkArgument(accountData.length() <= UserDefinedFiatAccountPayload.MAX_DATA_LENGTH, "Account data must not be longer than 1000 characters"); + UserDefinedFiatAccount newAccount = new UserDefinedFiatAccount(accountName, accountData); + accountService.removePaymentAccount(model.getSelectedAccount()); + accountService.addPaymentAccount(newAccount); + accountService.setSelectedAccount(newAccount); + } } void onDeleteAccount() { diff --git a/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/create/CreatePaymentAccountController.java b/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/create/CreatePaymentAccountController.java index 0e77f57136..62fb9a48b2 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/create/CreatePaymentAccountController.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/content/user/accounts/create/CreatePaymentAccountController.java @@ -19,6 +19,7 @@ import bisq.account.AccountService; import bisq.account.accounts.UserDefinedFiatAccount; +import bisq.account.accounts.UserDefinedFiatAccountPayload; import bisq.application.DefaultApplicationService; import bisq.desktop.common.view.Controller; import bisq.desktop.components.overlay.Popup; @@ -31,6 +32,7 @@ import org.fxmisc.easybind.Subscription; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class CreatePaymentAccountController implements Controller { @@ -80,8 +82,10 @@ protected void onSave() { .onAction(() -> model.setAccountName("")) .show(); } else { - UserDefinedFiatAccount newAccount = new UserDefinedFiatAccount(model.getAccountName(), - model.getAccountData()); + String accountData = model.getAccountData(); + checkNotNull(accountData); + checkArgument(accountData.length() <= UserDefinedFiatAccountPayload.MAX_DATA_LENGTH, "Account data must not be longer than 1000 characters"); + UserDefinedFiatAccount newAccount = new UserDefinedFiatAccount(model.getAccountName(), accountData); accountService.addPaymentAccount(newAccount); accountService.setSelectedAccount(newAccount); close(); diff --git a/desktop/src/main/java/bisq/desktop/primary/main/top/MarketSelection.java b/desktop/src/main/java/bisq/desktop/primary/main/top/MarketSelection.java index c8faf6cf82..9e0332f97e 100644 --- a/desktop/src/main/java/bisq/desktop/primary/main/top/MarketSelection.java +++ b/desktop/src/main/java/bisq/desktop/primary/main/top/MarketSelection.java @@ -149,8 +149,9 @@ protected void onViewAttached() { codes.textProperty().bind(model.codes); price.textProperty().bind(model.price); root.setOnMouseClicked(e -> { - if (model.items.isEmpty()) return; - + if (model.items.isEmpty()) { + return; + } new ComboBoxOverlay<>(root, model.items, c -> getListCell(), @@ -165,6 +166,7 @@ protected void onViewAttached() { @Override protected void onViewDetached() { codes.textProperty().unbind(); + price.textProperty().unbind(); root.setOnMouseClicked(null); } diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferController.java b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferController.java index 805ae3bf08..08358c472b 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferController.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferController.java @@ -20,8 +20,6 @@ import bisq.account.payment_method.FiatPaymentMethod; import bisq.application.DefaultApplicationService; import bisq.chat.ChatService; -import bisq.chat.bisqeasy.channel.BisqEasyChatChannelSelectionService; -import bisq.chat.bisqeasy.channel.priv.BisqEasyPrivateTradeChatChannelService; import bisq.chat.bisqeasy.channel.pub.BisqEasyPublicChatChannel; import bisq.chat.bisqeasy.channel.pub.BisqEasyPublicChatChannelService; import bisq.chat.bisqeasy.message.BisqEasyPublicChatMessage; @@ -35,12 +33,11 @@ import bisq.desktop.primary.overlay.bisq_easy.take_offer.TakeOfferController; import bisq.i18n.Res; import bisq.offer.Direction; -import bisq.offer.amount.AmountUtil; import bisq.offer.amount.OfferAmountFormatter; +import bisq.offer.amount.OfferAmountUtil; import bisq.offer.amount.spec.AmountSpec; import bisq.offer.amount.spec.RangeAmountSpec; import bisq.offer.bisq_easy.BisqEasyOffer; -import bisq.offer.bisq_easy.BisqEasyOfferService; import bisq.offer.payment_method.PaymentMethodSpecFormatter; import bisq.offer.payment_method.PaymentMethodSpecUtil; import bisq.offer.price.spec.FixPriceSpec; @@ -50,7 +47,6 @@ import bisq.presentation.formatters.PercentageFormatter; import bisq.presentation.formatters.PriceFormatter; import bisq.settings.SettingsService; -import bisq.support.MediationService; import bisq.user.identity.UserIdentity; import bisq.user.identity.UserIdentityService; import bisq.user.profile.UserProfile; @@ -77,28 +73,19 @@ public class CreateOfferReviewOfferController implements Controller { private final UserIdentityService userIdentityService; private final BisqEasyPublicChatChannelService bisqEasyPublicChatChannelService; private final UserProfileService userProfileService; - private final BisqEasyChatChannelSelectionService bisqEasyChatChannelSelectionService; private final Consumer mainButtonsVisibleHandler; - private final BisqEasyPrivateTradeChatChannelService bisqEasyPrivateTradeChatChannelService; - private final MediationService mediationService; - private final ChatService chatService; private final MarketPriceService marketPriceService; - private final BisqEasyOfferService bisqEasyOfferService; public CreateOfferReviewOfferController(DefaultApplicationService applicationService, Consumer mainButtonsVisibleHandler, Runnable resetHandler) { this.mainButtonsVisibleHandler = mainButtonsVisibleHandler; - bisqEasyOfferService = applicationService.getOfferService().getBisqEasyOfferService(); - chatService = applicationService.getChatService(); + ChatService chatService = applicationService.getChatService(); bisqEasyPublicChatChannelService = chatService.getBisqEasyPublicChatChannelService(); - bisqEasyChatChannelSelectionService = chatService.getBisqEasyChatChannelSelectionService(); reputationService = applicationService.getUserService().getReputationService(); settingsService = applicationService.getSettingsService(); userIdentityService = applicationService.getUserService().getUserIdentityService(); userProfileService = applicationService.getUserService().getUserProfileService(); - bisqEasyPrivateTradeChatChannelService = chatService.getBisqEasyPrivateTradeChatChannelService(); - mediationService = applicationService.getSupportService().getMediationService(); marketPriceService = applicationService.getOracleService().getMarketPriceService(); this.resetHandler = resetHandler; @@ -199,7 +186,7 @@ public void onActivate() { BisqEasyPublicChatMessage myOfferMessage = new BisqEasyPublicChatMessage(channel.getId(), userIdentity.getUserProfile().getId(), - Optional.of(bisqEasyOffer.getId()), + Optional.of(bisqEasyOffer), Optional.of(chatMessageText), Optional.empty(), new Date().getTime(), @@ -208,17 +195,11 @@ public void onActivate() { model.setMyOfferMessage(myOfferMessage); model.getMatchingOffers().setAll(channel.getChatMessages().stream() - .filter(chatMessage -> chatMessage.getBisqEasyOfferId().isPresent()) - .map(chatMessage -> { - String offerId = chatMessage.getBisqEasyOfferId().get(); - return bisqEasyOfferService.findOffer(offerId) - .map(offer -> new CreateOfferReviewOfferView.ListItem(offer, - userProfileService, - reputationService, - marketPriceService)) - .orElse(null); - }) - .filter(Objects::nonNull) + .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) + .map(chatMessage -> new CreateOfferReviewOfferView.ListItem(chatMessage.getBisqEasyOffer().get(), + userProfileService, + reputationService, + marketPriceService)) .filter(getTakeOfferPredicate()) .sorted(Comparator.comparing(CreateOfferReviewOfferView.ListItem::getReputationScore)) .limit(3) @@ -243,8 +224,6 @@ void onTakeOffer(CreateOfferReviewOfferView.ListItem listItem) { } void onPublishOffer() { - bisqEasyOfferService.publishOffer(model.getBisqEasyOffer()); - UserIdentity userIdentity = checkNotNull(userIdentityService.getSelectedUserIdentity()); bisqEasyPublicChatChannelService.publishChatMessage(model.getMyOfferMessage(), userIdentity) .thenAccept(result -> UIThread.run(() -> { @@ -283,7 +262,7 @@ private Predicate getTakeOfferPredi if (model.getMyOfferMessage() == null) { return false; } - if (model.getMyOfferMessage().getBisqEasyOfferId().isEmpty()) { + if (model.getMyOfferMessage().getBisqEasyOffer().isEmpty()) { return false; } @@ -297,14 +276,14 @@ private Predicate getTakeOfferPredi if (!peersOffer.getMarket().equals(bisqEasyOffer.getMarket())) { return false; } - Optional myQuoteSideMinOrFixedAmount = AmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, bisqEasyOffer); - Optional peersQuoteSideMaxOrFixedAmount = AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, peersOffer); + Optional myQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, bisqEasyOffer); + Optional peersQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, peersOffer); if (myQuoteSideMinOrFixedAmount.orElseThrow().getValue() > peersQuoteSideMaxOrFixedAmount.orElseThrow().getValue()) { return false; } - Optional myQuoteSideMaxOrFixedAmount = AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer); - Optional peersQuoteSideMinOrFixedAmount = AmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, peersOffer); + Optional myQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer); + Optional peersQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, peersOffer); if (myQuoteSideMaxOrFixedAmount.orElseThrow().getValue() < peersQuoteSideMinOrFixedAmount.orElseThrow().getValue()) { return false; } diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferView.java b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferView.java index 7086af7aa7..6e4ad55063 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferView.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/create_offer/review/CreateOfferReviewOfferView.java @@ -30,8 +30,8 @@ import bisq.desktop.primary.overlay.bisq_easy.create_offer.CreateOfferView; import bisq.i18n.Res; import bisq.offer.Direction; -import bisq.offer.amount.AmountUtil; import bisq.offer.amount.OfferAmountFormatter; +import bisq.offer.amount.OfferAmountUtil; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.offer.price.OfferPriceFormatter; import bisq.offer.price.PriceUtil; @@ -369,7 +369,7 @@ public ListItem(BisqEasyOffer bisqEasyOffer, userName = authorUserProfileId.map(UserProfile::getUserName).orElse(""); priceAsLong = PriceUtil.findQuote(marketPriceService, bisqEasyOffer).map(PriceQuote::getValue).orElse(0L); priceDisplayString = OfferPriceFormatter.formatQuote(marketPriceService, bisqEasyOffer, false); - amountAsLong = AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer).map(Monetary::getValue).orElse(0L); + amountAsLong = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer).map(Monetary::getValue).orElse(0L); amountDisplayString = OfferAmountFormatter.formatQuoteAmount(marketPriceService, bisqEasyOffer, false); reputationScore = authorUserProfileId.flatMap(reputationService::findReputationScore) .orElse(ReputationScore.NONE); diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/amount/TakeOfferAmountController.java b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/amount/TakeOfferAmountController.java index 0229d2c6d8..9d32f1d4f1 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/amount/TakeOfferAmountController.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/amount/TakeOfferAmountController.java @@ -23,8 +23,8 @@ import bisq.desktop.common.view.Controller; import bisq.desktop.primary.overlay.bisq_easy.components.AmountComponent; import bisq.i18n.Res; -import bisq.offer.amount.AmountUtil; import bisq.offer.amount.OfferAmountFormatter; +import bisq.offer.amount.OfferAmountUtil; import bisq.offer.amount.spec.AmountSpec; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.offer.price.PriceUtil; @@ -60,8 +60,8 @@ public void init(BisqEasyOffer bisqEasyOffer, Optional takersAmountS amountComponent.setDirection(bisqEasyOffer.getDirection()); Market market = bisqEasyOffer.getMarket(); amountComponent.setMarket(market); - amountComponent.setMinMaxRange(AmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, bisqEasyOffer).orElseThrow(), - AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer).orElseThrow()); + amountComponent.setMinMaxRange(OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, bisqEasyOffer).orElseThrow(), + OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, bisqEasyOffer).orElseThrow()); String direction = bisqEasyOffer.getTakersDirection().isBuy() ? Res.get("offer.buy").toUpperCase() : @@ -79,9 +79,9 @@ public void init(BisqEasyOffer bisqEasyOffer, Optional takersAmountS } takersAmountSpec.ifPresent(amountSpec -> { - AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, amountSpec, bisqEasyOffer.getPriceSpec(), market) + OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, amountSpec, bisqEasyOffer.getPriceSpec(), market) .ifPresent(amountComponent::setQuoteSideAmount); - AmountUtil.findBaseSideMaxOrFixedAmount(marketPriceService, amountSpec, bisqEasyOffer.getPriceSpec(), market) + OfferAmountUtil.findBaseSideMaxOrFixedAmount(marketPriceService, amountSpec, bisqEasyOffer.getPriceSpec(), market) .ifPresent(amountComponent::setBaseSideAmount); }); } diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/review/TakeOfferReviewController.java b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/review/TakeOfferReviewController.java index 6f43a09b89..20a2666eaa 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/review/TakeOfferReviewController.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/bisq_easy/take_offer/review/TakeOfferReviewController.java @@ -40,7 +40,7 @@ import bisq.i18n.Res; import bisq.identity.IdentityService; import bisq.offer.Direction; -import bisq.offer.amount.AmountUtil; +import bisq.offer.amount.OfferAmountUtil; import bisq.offer.amount.spec.FixedAmountSpec; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.offer.payment_method.FiatPaymentMethodSpec; @@ -107,9 +107,9 @@ public void init(BisqEasyOffer bisqEasyOffer) { priceInput.setDescription(Res.get("bisqEasy.takeOffer.review.price.sellersPrice", marketCodes)); if (bisqEasyOffer.getAmountSpec() instanceof FixedAmountSpec) { - AmountUtil.findBaseSideFixedAmount(marketPriceService, bisqEasyOffer) + OfferAmountUtil.findBaseSideFixedAmount(marketPriceService, bisqEasyOffer) .ifPresent(model::setTakersBaseSideAmount); - AmountUtil.findQuoteSideFixedAmount(marketPriceService, bisqEasyOffer) + OfferAmountUtil.findQuoteSideFixedAmount(marketPriceService, bisqEasyOffer) .ifPresent(model::setTakersQuoteSideAmount); } diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/create_profile/CreateProfileView.java b/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/create_profile/CreateProfileView.java index f6269c2b3d..90feaecfca 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/create_profile/CreateProfileView.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/create_profile/CreateProfileView.java @@ -63,7 +63,7 @@ public CreateProfileView(CreateProfileModel model, CreateProfileController contr subtitleLabel.getStyleClass().addAll("bisq-text-3", "wrap-text"); nickname = new MaterialTextField(Res.get("onboarding.createProfile.nickName"), Res.get("onboarding.createProfile.nickName.prompt")); - nickname.setMaxWidth(250); + nickname.setMaxWidth(315); roboIconView = new ImageView(); roboIconView.setCursor(Cursor.HAND); diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/password/OnboardingPasswordView.java b/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/password/OnboardingPasswordView.java index 4127162aec..b6aacdecef 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/password/OnboardingPasswordView.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/onboarding/password/OnboardingPasswordView.java @@ -28,6 +28,7 @@ import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -48,13 +49,18 @@ public OnboardingPasswordView(OnboardingPasswordModel model, OnboardingPasswordC headline.getStyleClass().addAll("bisq-text-headline-2", "wrap-text"); Label subtitleLabel = new Label(Res.get("onboarding.password.subTitle")); + subtitleLabel.setTextAlignment(TextAlignment.CENTER); subtitleLabel.getStyleClass().addAll("bisq-text-3", "wrap-text"); + subtitleLabel.setMinHeight(40); + subtitleLabel.setMaxWidth(375); password = new MaterialPasswordField(Res.get("onboarding.password.enterPassword")); password.setValidator(new PasswordValidator()); + password.setMaxWidth(315); confirmedPassword = new MaterialPasswordField(Res.get("onboarding.password.confirmPassword")); confirmedPassword.setValidator(confirmedPasswordValidator); + confirmedPassword.setMaxWidth(password.getMaxWidth()); setPasswordButton = new Button(Res.get("onboarding.password.button.savePassword")); setPasswordButton.setDefaultButton(true); diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacController.java b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacController.java index 0fea72393b..f704492475 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacController.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacController.java @@ -67,8 +67,12 @@ public void onQuit() { applicationService.shutdown().thenAccept(result -> Platform.exit()); } + public void onConfirm(boolean selected) { + model.getTacConfirmed().set(selected); + settingsService.setTacAccepted(selected); + } + void onAccept() { - settingsService.setTacAccepted(true); OverlayController.hide(() -> { if (completeHandler != null) { completeHandler.run(); diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacModel.java b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacModel.java index b800b01b3a..dc64aa8e6c 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacModel.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacModel.java @@ -18,8 +18,11 @@ package bisq.desktop.primary.overlay.tac; import bisq.desktop.common.view.Model; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import lombok.Getter; @Getter public class TacModel implements Model { + private final BooleanProperty tacConfirmed = new SimpleBooleanProperty(); } \ No newline at end of file diff --git a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacView.java b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacView.java index 716bc1ca49..58cf95eb3e 100644 --- a/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacView.java +++ b/desktop/src/main/java/bisq/desktop/primary/overlay/tac/TacView.java @@ -19,7 +19,6 @@ import bisq.desktop.common.threading.UIThread; import bisq.desktop.common.utils.KeyHandlerUtil; -import bisq.desktop.common.utils.Layout; import bisq.desktop.common.view.View; import bisq.desktop.primary.PrimaryStageModel; import bisq.desktop.primary.overlay.OverlayController; @@ -27,39 +26,33 @@ import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TextArea; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @Slf4j -public class TacView extends View { - private static final double SPACING = 20; +public class TacView extends View { private static final double PADDING = 30; - private static final double HEADLINE_HEIGHT = 30; - private static final double BUTTONS_HEIGHT = 31; - private final Label headline; - private final HBox buttons; private Scene rootScene; - private final TextArea tac; private final Button acceptButton, rejectButton; + private final CheckBox confirmCheckBox; private Subscription widthPin, heightPin; + private Subscription tacConfirmedPin; public TacView(TacModel model, TacController controller) { - super(new AnchorPane(), model, controller); - - root.setPrefWidth(PrimaryStageModel.MIN_WIDTH - 4 * PADDING); - root.setPrefHeight(PrimaryStageModel.MIN_HEIGHT - 4 * PADDING); + super(new VBox(20), model, controller); root.setPadding(new Insets(PADDING)); - headline = new Label(Res.get("tac.headline")); - headline.getStyleClass().addAll("bisq-text-headline-2", "wrap-text"); - Layout.pinToAnchorPane(headline, PADDING, PADDING, null, PADDING); + Label headline = new Label(Res.get("tac.headline")); + headline.getStyleClass().addAll("tac-headline", "wrap-text"); String text = "1. In no event, unless for damages caused by acts of intent and gross negligence, damages resulting from personal injury, " + "or damages ensuing from other instances where liability is required by applicable law or agreed to in writing, will any " + @@ -90,18 +83,22 @@ public TacView(TacModel model, TacController controller) { " get banned on Bisq 1. If the seller has used 'bonded BSQ' as reputation source the mediator will report the incident to the DAO and\n" + " make a proposal for confiscating their bonded BSQ.\n"; - tac = new TextArea(text); - Layout.pinToAnchorPane(tac, PADDING + SPACING + HEADLINE_HEIGHT, PADDING, - PADDING + 2 * SPACING + BUTTONS_HEIGHT, PADDING); + TextArea tac = new TextArea(text); + tac.getStyleClass().add("tac-text"); + tac.setWrapText(true); tac.setEditable(false); + confirmCheckBox = new CheckBox(Res.get("tac.confirm")); + acceptButton = new Button(Res.get("tac.accept")); - acceptButton.setDefaultButton(true); rejectButton = new Button(Res.get("tac.reject")); - buttons = new HBox(20, acceptButton, rejectButton); - Layout.pinToAnchorPane(buttons, null, PADDING, PADDING, PADDING); - root.getChildren().setAll(headline, tac, buttons); + rejectButton.getStyleClass().add("outlined-button"); + + HBox buttons = new HBox(20, acceptButton, rejectButton); + VBox.setVgrow(tac, Priority.ALWAYS); + VBox.setMargin(confirmCheckBox, new Insets(10, 0, 0, 10)); + root.getChildren().addAll(headline, tac, confirmCheckBox, buttons); } @Override @@ -117,6 +114,15 @@ protected void onViewAttached() { updateHeight(); }); + confirmCheckBox.setSelected(model.getTacConfirmed().get()); + + tacConfirmedPin = EasyBind.subscribe(model.getTacConfirmed(), confirmed -> { + acceptButton.setDisable(!confirmed); + acceptButton.setDefaultButton(confirmed); + }); + + confirmCheckBox.setOnAction(e -> controller.onConfirm(confirmCheckBox.isSelected())); + acceptButton.setOnAction(e -> controller.onAccept()); rejectButton.setOnAction(e -> controller.onReject()); @@ -127,10 +133,22 @@ protected void onViewAttached() { }); } + @Override + protected void onViewDetached() { + widthPin.unsubscribe(); + heightPin.unsubscribe(); + tacConfirmedPin.unsubscribe(); + acceptButton.setOnAction(null); + rejectButton.setOnAction(null); + rootScene.setOnKeyReleased(null); + } + private void updateHeight() { double height = OverlayController.getInstance().getApplicationRoot().getHeight(); if (height > 0) { - double paddedHeight = height - 4 * PADDING; + double scale = (height - PrimaryStageModel.MIN_HEIGHT) / (PrimaryStageModel.PREF_HEIGHT - PrimaryStageModel.MIN_HEIGHT); + double boundedScale = Math.max(0.25, Math.min(1, scale)); + double paddedHeight = height - 6 * PADDING * boundedScale; rootScene.getWindow().setHeight(paddedHeight); root.setPrefHeight(paddedHeight); } @@ -141,19 +159,10 @@ private void updateWidth() { if (width > 0) { double scale = (width - PrimaryStageModel.MIN_WIDTH) / (PrimaryStageModel.PREF_WIDTH - PrimaryStageModel.MIN_WIDTH); double boundedScale = Math.max(0.25, Math.min(1, scale)); - double padding = 4 * PADDING * boundedScale; + double padding = 6 * PADDING * boundedScale; double paddedWidth = width - padding; rootScene.getWindow().setWidth(paddedWidth); root.setPrefWidth(paddedWidth); } } - - @Override - protected void onViewDetached() { - widthPin.unsubscribe(); - heightPin.unsubscribe(); - acceptButton.setOnAction(null); - rejectButton.setOnAction(null); - rootScene.setOnKeyReleased(null); - } } \ No newline at end of file diff --git a/desktop/src/main/resources/bisq.css b/desktop/src/main/resources/bisq.css index af242348ac..c6e5802dfc 100644 --- a/desktop/src/main/resources/bisq.css +++ b/desktop/src/main/resources/bisq.css @@ -103,4 +103,23 @@ -fx-dark-text-color: -bisq-black; -fx-mid-text-color: -bisq-grey-dimmed; -fx-light-text-color: -bisq-white; +} + + +/******************************************************************************* + * TAC * + ******************************************************************************/ + +.tac-headline { + -fx-fill: -fx-light-text-color !important; + -fx-text-fill: -fx-light-text-color !important; + -fx-font-size: 1.5em !important; + -fx-font-family: "IBM Plex Sans Light" !important; +} + +.tac-text { + -fx-fill: -fx-light-text-color !important; + -fx-text-fill: -fx-light-text-color !important; + -fx-font-size: 1.1em !important; + -fx-font-family: "IBM Plex Sans Light" !important; } \ No newline at end of file diff --git a/desktop/src/main/resources/bisq_text.css b/desktop/src/main/resources/bisq_text.css index 02374feb67..cca259280c 100644 --- a/desktop/src/main/resources/bisq_text.css +++ b/desktop/src/main/resources/bisq_text.css @@ -44,7 +44,7 @@ .text-area { -fx-focus-color: transparent; -fx-faint-focus-color: transparent; - -fx-background-color: -bisq-grey-12; + -fx-background-color: transparent; -fx-background-radius: 4; -fx-border-style: none;; -fx-text-fill: -fx-light-text-color; diff --git a/i18n/src/main/resources/application.properties b/i18n/src/main/resources/application.properties index 5d5b7280f0..e2eec536db 100644 --- a/i18n/src/main/resources/application.properties +++ b/i18n/src/main/resources/application.properties @@ -21,6 +21,7 @@ loading.state.FAILED=Startup failed #################################################################### tac.headline=User Agreement +tac.confirm=I have read and understood tac.accept=Accept user Agreement tac.reject=Reject and quit application @@ -74,8 +75,8 @@ onboarding.createProfile.nickName=Profile nickname # Set password #################################################################### onboarding.password.button.skip=Skip -onboarding.password.subTitle=You can set up password protection now or skip that step and do it later at the \ - 'User options/Password' screen. +onboarding.password.subTitle=You can set up password protection now or skip that step \ + and do it later at the 'User options/Password' screen. onboarding.password.headline.setPassword=Set password protection onboarding.password.button.savePassword=Save password diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index 35a8345b11..46ad671996 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -336,12 +336,16 @@ bisqEasy.tradeState.phase.phase5=Completed # Trade State: Info (right side) -bisqEasy.tradeState.info.phase4.balance.help.notInMempoolYet=Not seen in mempool yet -bisqEasy.tradeState.info.phase4.balance.help.confirmation={0} confirmation (using 'mempool.info' block explorer) -bisqEasy.tradeState.info.phase4.balance.help.confirmation.plural={0} confirmations (using 'mempool.info' block explorer) -bisqEasy.tradeState.info.phase4.buttonText=Review completed trade +bisqEasy.tradeState.info.phase4.balance.help.explorerLookup=Looking up transaction at block explorer... +bisqEasy.tradeState.info.phase4.balance.help.notConfirmed=Transaction seen in mempool but not confirmed yet +bisqEasy.tradeState.info.phase4.balance.help.confirmed=Transaction is confirmed +bisqEasy.tradeState.info.phase4.buttonText=Skip waiting for confirmation bisqEasy.tradeState.info.phase4.chatBotMessage=The payment of ''{0}'' to address ''{1}'' has at least 1 blockchain confirmation -bisqEasy.tradeState.info.phase5.buttonText=Close trade +bisqEasy.tradeState.info.phase4.txId=Transaction ID +bisqEasy.tradeState.info.phase4.txId.tooltip=Open transaction in block explorer +bisqEasy.tradeState.info.phase4.txId.failed=Transaction lookup failed +bisqEasy.tradeState.info.phase5.exportTrade=Export trade data +bisqEasy.tradeState.info.phase5.leaveChannel=Leave trade channel # Trade State Info: Buyer bisqEasy.tradeState.info.buyer.phase1.headline=Wait for the seller's payment account data @@ -364,7 +368,6 @@ bisqEasy.tradeState.info.buyer.phase3.info=Once the seller has received your {0} bisqEasy.tradeState.info.buyer.phase4.headline=The seller has started the Bitcoin payment bisqEasy.tradeState.info.buyer.phase4.info=As soon the Bitcoin transaction is visible in the Bitcoin network you will see the payment. \ After 1 blockchain confirmation the payment can be considered as completed. -bisqEasy.tradeState.info.buyer.phase4.txId=Transaction ID bisqEasy.tradeState.info.buyer.phase4.balance=Received Bitcoin bisqEasy.tradeState.info.buyer.phase5.headline=Trade was successfully completed @@ -396,8 +399,8 @@ bisqEasy.tradeState.info.seller.phase3.chatBotMessage=I initiated the Bitcoin pa bisqEasy.tradeState.info.seller.phase4.headline=Waiting for blockchain confirmation bisqEasy.tradeState.info.seller.phase4.info=The Bitcoin payment require at least 1 blockchain confirmation to be considered complete. -bisqEasy.tradeState.info.seller.phase4.txId=Transaction ID bisqEasy.tradeState.info.seller.phase4.balance=Bitcoin payment + bisqEasy.tradeState.info.seller.phase5.headline=Trade was successfully completed bisqEasy.tradeState.info.seller.phase5.quoteAmount=You have received bisqEasy.tradeState.info.seller.phase5.baseAmount=You have sold diff --git a/i18n/src/main/resources/chat.properties b/i18n/src/main/resources/chat.properties index ffa6294d30..6cc04121eb 100644 --- a/i18n/src/main/resources/chat.properties +++ b/i18n/src/main/resources/chat.properties @@ -124,6 +124,7 @@ chat.message.contextMenu.copyMessage=Copy message chat.message.contextMenu.ignoreUser=Ignore user chat.message.contextMenu.reportUser=Report user to moderator +chat.message.offer.offerAlreadyTaken.warn=You cannot take that offer as you have taken that offer already. chat.message.delete.differentUserProfile.warn=This chat message was created with another user profile.\n\n\ Do you want to delete the message? chat.message.send.differentUserProfile.warn=You have used another user profile in that channel.\n\n\ diff --git a/offer/src/main/java/bisq/offer/OfferMessageService.java b/offer/src/main/java/bisq/offer/OfferMessageService.java index 3e2e31b37d..53a4336506 100644 --- a/offer/src/main/java/bisq/offer/OfferMessageService.java +++ b/offer/src/main/java/bisq/offer/OfferMessageService.java @@ -65,7 +65,7 @@ public CompletableFuture shutdown() { networkService.removeDataServiceListener(this); return CompletableFuture.completedFuture(true); } - + /////////////////////////////////////////////////////////////////////////////////////////////////// // DataService.Listener diff --git a/offer/src/main/java/bisq/offer/OfferService.java b/offer/src/main/java/bisq/offer/OfferService.java index 9d4d5b469e..152675666b 100644 --- a/offer/src/main/java/bisq/offer/OfferService.java +++ b/offer/src/main/java/bisq/offer/OfferService.java @@ -20,22 +20,22 @@ import bisq.common.application.Service; import bisq.identity.IdentityService; import bisq.network.NetworkService; -import bisq.offer.bisq_easy.BisqEasyOfferService; import bisq.persistence.PersistenceService; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CompletableFuture; +/** + * Not used for BisqEasy as we keep the offer in the chat message. + */ @Slf4j @Getter public class OfferService implements Service { - private final BisqEasyOfferService bisqEasyOfferService; - private final OfferMessageService offerMessageService; public OfferService(NetworkService networkService, IdentityService identityService, PersistenceService persistenceService) { - offerMessageService = new OfferMessageService(networkService, identityService); - bisqEasyOfferService = new BisqEasyOfferService(persistenceService, offerMessageService); + // offerMessageService = new OfferMessageService(networkService, identityService); + // multisigOfferService = new MultisigOfferService(persistenceService, offerMessageService); } @@ -45,13 +45,11 @@ public OfferService(NetworkService networkService, IdentityService identityServi public CompletableFuture initialize() { log.info("initialize"); - return offerMessageService.initialize() - .thenCompose(result -> bisqEasyOfferService.initialize()); + return CompletableFuture.completedFuture(true); } public CompletableFuture shutdown() { log.info("shutdown"); - return bisqEasyOfferService.shutdown() - .thenCompose(result -> offerMessageService.shutdown()); + return CompletableFuture.completedFuture(true); } } \ No newline at end of file diff --git a/offer/src/main/java/bisq/offer/amount/OfferAmountFormatter.java b/offer/src/main/java/bisq/offer/amount/OfferAmountFormatter.java index c006d2223d..b19ff6fb77 100644 --- a/offer/src/main/java/bisq/offer/amount/OfferAmountFormatter.java +++ b/offer/src/main/java/bisq/offer/amount/OfferAmountFormatter.java @@ -72,7 +72,7 @@ public static String formatBaseSideFixedAmount(MarketPriceService marketPriceSer PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findBaseSideFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findBaseSideFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Min @@ -89,7 +89,7 @@ public static String formatBaseSideMinAmount(MarketPriceService marketPriceServi PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findBaseSideMinAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findBaseSideMinAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Max @@ -106,7 +106,7 @@ public static String formatBaseSideMaxAmount(MarketPriceService marketPriceServi PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findBaseSideMaxAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findBaseSideMaxAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Max or fixed @@ -119,7 +119,7 @@ public static String formatBaseSideMaxOrFixedAmount(MarketPriceService marketPri PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findBaseSideMaxOrFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findBaseSideMaxOrFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Range (Min - Max) @@ -177,7 +177,7 @@ public static String formatQuoteSideFixedAmount(MarketPriceService marketPriceSe PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findQuoteSideFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findQuoteSideFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Min @@ -194,7 +194,7 @@ public static String formatQuoteSideMinAmount(MarketPriceService marketPriceServ PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findQuoteSideMinAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findQuoteSideMinAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Max @@ -211,7 +211,7 @@ public static String formatQuoteSideMaxAmount(MarketPriceService marketPriceServ PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findQuoteSideMaxAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findQuoteSideMaxAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Max or fixed @@ -224,7 +224,7 @@ public static String formatQuoteSideMaxOrFixedAmount(MarketPriceService marketPr PriceSpec priceSpec, Market market, boolean withCode) { - return AmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); + return OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, amountSpec, priceSpec, market).map(getFormatFunction(withCode)).orElse(Res.get("data.na")); } // Range (Min - Max) diff --git a/offer/src/main/java/bisq/offer/amount/AmountUtil.java b/offer/src/main/java/bisq/offer/amount/OfferAmountUtil.java similarity index 99% rename from offer/src/main/java/bisq/offer/amount/AmountUtil.java rename to offer/src/main/java/bisq/offer/amount/OfferAmountUtil.java index 8e53173a8e..b1e1cd9e9a 100644 --- a/offer/src/main/java/bisq/offer/amount/AmountUtil.java +++ b/offer/src/main/java/bisq/offer/amount/OfferAmountUtil.java @@ -34,7 +34,7 @@ * - fixPriceAmount, minAmount, maxAmount * - Combinations of fallbacks for fixPriceAmount, minAmount, maxAmount */ -public class AmountUtil { +public class OfferAmountUtil { /////////////////////////////////////////////////////////////////////////////////////////////////// // BaseAmount: If no BaseAmountSpec we calculate it from the QuoteAmountSpec with the PriceSpec diff --git a/offer/src/main/java/bisq/offer/bisq_easy/BisqEasyOfferService.java b/offer/src/main/java/bisq/offer/multisig/MultisigOfferService.java similarity index 78% rename from offer/src/main/java/bisq/offer/bisq_easy/BisqEasyOfferService.java rename to offer/src/main/java/bisq/offer/multisig/MultisigOfferService.java index 1e9eba676a..5585b44241 100644 --- a/offer/src/main/java/bisq/offer/bisq_easy/BisqEasyOfferService.java +++ b/offer/src/main/java/bisq/offer/multisig/MultisigOfferService.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.offer.bisq_easy; +package bisq.offer.multisig; import bisq.common.application.Service; import bisq.common.observable.Pin; @@ -36,30 +36,30 @@ @Slf4j @Getter -public class BisqEasyOfferService implements Service { - private final MyBisqEasyOffersService myBisqEasyOffersService; +public class MultisigOfferService implements Service { + private final MyMultisigOffersService myMultisigOffersService; private final OfferMessageService offerMessageService; @Getter - private final ObservableSet offers = new ObservableSet<>(); + private final ObservableSet offers = new ObservableSet<>(); private final CollectionObserver> offersObserver; private Pin offersObserverPin; - public BisqEasyOfferService(PersistenceService persistenceService, + public MultisigOfferService(PersistenceService persistenceService, OfferMessageService offerMessageService) { this.offerMessageService = offerMessageService; - myBisqEasyOffersService = new MyBisqEasyOffersService(persistenceService); + myMultisigOffersService = new MyMultisigOffersService(persistenceService); offersObserver = new CollectionObserver<>() { @Override public void add(Offer element) { - if (element instanceof BisqEasyOffer) { - processAddedBisqEasyOffer((BisqEasyOffer) element); + if (element instanceof MultisigOffer) { + processAddedOffer((MultisigOffer) element); } } @Override public void remove(Object element) { - if (element instanceof BisqEasyOffer) { - processRemovedBisqEasyOffer((BisqEasyOffer) element); + if (element instanceof MultisigOffer) { + processRemovedOffer((MultisigOffer) element); } } @@ -83,13 +83,13 @@ public CompletableFuture initialize() { // todo provide an API from network to get an event for that Scheduler.run(this::republishMyOffers).after(5000, TimeUnit.MILLISECONDS); - return myBisqEasyOffersService.initialize(); + return myMultisigOffersService.initialize(); } public CompletableFuture shutdown() { log.info("shutdown"); offersObserverPin.unbind(); - return removeAllOfferFromNetwork().thenCompose(e -> myBisqEasyOffersService.shutdown()); + return removeAllOfferFromNetwork().thenCompose(e -> myMultisigOffersService.shutdown()); } @@ -103,8 +103,8 @@ public CompletableFuture publishOffer(String of .orElse(CompletableFuture.failedFuture(new RuntimeException("Offer with not found. OfferID=" + offerId))); } - public CompletableFuture publishOffer(BisqEasyOffer offer) { - myBisqEasyOffersService.add(offer); + public CompletableFuture publishOffer(MultisigOffer offer) { + myMultisigOffersService.add(offer); return offerMessageService.addToNetwork(offer); } @@ -114,12 +114,12 @@ public CompletableFuture removeOffer(String off .orElse(CompletableFuture.failedFuture(new RuntimeException("Offer with not found. OfferID=" + offerId))); } - public CompletableFuture removeOffer(BisqEasyOffer offer) { - myBisqEasyOffersService.remove(offer); + public CompletableFuture removeOffer(MultisigOffer offer) { + myMultisigOffersService.remove(offer); return offerMessageService.removeFromNetwork(offer); } - public Optional findOffer(String offerId) { + public Optional findOffer(String offerId) { return offers.stream().filter(offer -> offer.getId().equals(offerId)).findAny(); } @@ -128,12 +128,12 @@ public Optional findOffer(String offerId) { // Private /////////////////////////////////////////////////////////////////////////////////////////////////// - private boolean processAddedBisqEasyOffer(BisqEasyOffer bisqEasyOffer) { - return offers.add(bisqEasyOffer); + private boolean processAddedOffer(MultisigOffer offer) { + return offers.add(offer); } - private boolean processRemovedBisqEasyOffer(BisqEasyOffer bisqEasyOffer) { - return offers.remove(bisqEasyOffer); + private boolean processRemovedOffer(MultisigOffer offer) { + return offers.remove(offer); } private void republishMyOffers() { diff --git a/offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersService.java b/offer/src/main/java/bisq/offer/multisig/MyMultisigOffersService.java similarity index 80% rename from offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersService.java rename to offer/src/main/java/bisq/offer/multisig/MyMultisigOffersService.java index e5fefe1433..d5a93dc18d 100644 --- a/offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersService.java +++ b/offer/src/main/java/bisq/offer/multisig/MyMultisigOffersService.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.offer.bisq_easy; +package bisq.offer.multisig; import bisq.common.application.Service; import bisq.common.observable.collection.ObservableSet; @@ -29,13 +29,13 @@ import java.util.concurrent.CompletableFuture; @Slf4j -public class MyBisqEasyOffersService implements PersistenceClient, Service { +public class MyMultisigOffersService implements PersistenceClient, Service { @Getter - private final MyBisqEasyOffersStore persistableStore = new MyBisqEasyOffersStore(); + private final MyMultisigOffersStore persistableStore = new MyMultisigOffersStore(); @Getter - private final Persistence persistence; + private final Persistence persistence; - public MyBisqEasyOffersService(PersistenceService persistenceService) { + public MyMultisigOffersService(PersistenceService persistenceService) { persistence = persistenceService.getOrCreatePersistence(this, persistableStore); } @@ -61,21 +61,21 @@ public CompletableFuture shutdown() { // API /////////////////////////////////////////////////////////////////////////////////////////////////// - public void add(BisqEasyOffer offer) { + public void add(MultisigOffer offer) { persistableStore.add(offer); persist(); } - public void remove(BisqEasyOffer offer) { + public void remove(MultisigOffer offer) { persistableStore.remove(offer); persist(); } - public ObservableSet getOffers() { + public ObservableSet getOffers() { return persistableStore.getOffers(); } - public Optional findOffer(String offerId) { + public Optional findOffer(String offerId) { return getOffers().stream() .filter(offer -> offer.getId().equals(offerId)) .findAny(); diff --git a/offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersStore.java b/offer/src/main/java/bisq/offer/multisig/MyMultisigOffersStore.java similarity index 64% rename from offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersStore.java rename to offer/src/main/java/bisq/offer/multisig/MyMultisigOffersStore.java index 071b84c0e2..03f2047d55 100644 --- a/offer/src/main/java/bisq/offer/bisq_easy/MyBisqEasyOffersStore.java +++ b/offer/src/main/java/bisq/offer/multisig/MyMultisigOffersStore.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.offer.bisq_easy; +package bisq.offer.multisig; import bisq.common.observable.collection.ObservableSet; import bisq.common.proto.ProtoResolver; @@ -29,56 +29,56 @@ import java.util.stream.Collectors; @Slf4j -public final class MyBisqEasyOffersStore implements PersistableStore { +public final class MyMultisigOffersStore implements PersistableStore { @Getter - private final ObservableSet offers = new ObservableSet<>(); + private final ObservableSet offers = new ObservableSet<>(); - public MyBisqEasyOffersStore() { + public MyMultisigOffersStore() { } - private MyBisqEasyOffersStore(Set offers) { + private MyMultisigOffersStore(Set offers) { this.offers.addAll(offers); } @Override - public MyBisqEasyOffersStore getClone() { - return new MyBisqEasyOffersStore(offers); + public MyMultisigOffersStore getClone() { + return new MyMultisigOffersStore(offers); } @Override - public void applyPersisted(MyBisqEasyOffersStore persisted) { + public void applyPersisted(MyMultisigOffersStore persisted) { offers.clear(); offers.addAll(persisted.getOffers()); } @Override - public bisq.offer.protobuf.MyBisqEasyOffersStore toProto() { - return bisq.offer.protobuf.MyBisqEasyOffersStore.newBuilder() - .addAllOffers(offers.stream().map(BisqEasyOffer::toProto).collect(Collectors.toList())) + public bisq.offer.protobuf.MyMultisigOffersStore toProto() { + return bisq.offer.protobuf.MyMultisigOffersStore.newBuilder() + .addAllOffers(offers.stream().map(MultisigOffer::toProto).collect(Collectors.toList())) .build(); } - public static MyBisqEasyOffersStore fromProto(bisq.offer.protobuf.MyBisqEasyOffersStore proto) { - return new MyBisqEasyOffersStore(proto.getOffersList().stream().map(BisqEasyOffer::fromProto).collect(Collectors.toSet())); + public static MyMultisigOffersStore fromProto(bisq.offer.protobuf.MyMultisigOffersStore proto) { + return new MyMultisigOffersStore(proto.getOffersList().stream().map(MultisigOffer::fromProto).collect(Collectors.toSet())); } @Override public ProtoResolver> getResolver() { return any -> { try { - return fromProto(any.unpack(bisq.offer.protobuf.MyBisqEasyOffersStore.class)); + return fromProto(any.unpack(bisq.offer.protobuf.MyMultisigOffersStore.class)); } catch (InvalidProtocolBufferException e) { throw new UnresolvableProtobufMessageException(e); } }; } - public void add(BisqEasyOffer offer) { + public void add(MultisigOffer offer) { offers.add(offer); } - public void remove(BisqEasyOffer offer) { + public void remove(MultisigOffer offer) { offers.remove(offer); } } \ No newline at end of file diff --git a/offer/src/main/proto/offer.proto b/offer/src/main/proto/offer.proto index e5e061d1b3..5adbc024e0 100644 --- a/offer/src/main/proto/offer.proto +++ b/offer/src/main/proto/offer.proto @@ -142,14 +142,12 @@ message OfferMessage { message BisqEasyOffer { } -message MyBisqEasyOffersStore { - repeated Offer offers = 1; -} - // MultiSig message MultisigOffer { } - +message MyMultisigOffersStore { + repeated Offer offers = 1; +} // Submarine message SubmarineOffer { diff --git a/oracle/src/main/java/bisq/oracle/OracleService.java b/oracle/src/main/java/bisq/oracle/OracleService.java index 0b130b2062..0aa0db21dd 100644 --- a/oracle/src/main/java/bisq/oracle/OracleService.java +++ b/oracle/src/main/java/bisq/oracle/OracleService.java @@ -19,6 +19,7 @@ import bisq.common.application.Service; import bisq.network.NetworkService; +import bisq.oracle.explorer.ExplorerService; import bisq.oracle.marketprice.MarketPriceService; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -28,33 +29,42 @@ @Slf4j @Getter public class OracleService implements Service { + @Getter public static class Config { private final String privateKey; private final String publicKey; private final com.typesafe.config.Config marketPrice; + private final com.typesafe.config.Config blockchainExplorer; public Config(String privateKey, String publicKey, - com.typesafe.config.Config marketPrice) { + com.typesafe.config.Config marketPrice, + com.typesafe.config.Config blockchainExplorer) { this.privateKey = privateKey; this.publicKey = publicKey; this.marketPrice = marketPrice; + this.blockchainExplorer = blockchainExplorer; } public static Config from(com.typesafe.config.Config config) { return new Config(config.getString("privateKey"), config.getString("publicKey"), - config.getConfig("marketPrice")); + config.getConfig("marketPrice"), + config.getConfig("blockchainExplorer")); } } private final MarketPriceService marketPriceService; + private final ExplorerService explorerService; public OracleService(Config config, String applicationVersion, NetworkService networkService) { marketPriceService = new MarketPriceService(MarketPriceService.Config.from(config.getMarketPrice()), networkService, applicationVersion); + explorerService = new ExplorerService(ExplorerService.Config.from(config.getBlockchainExplorer()), + networkService, + applicationVersion); } diff --git a/oracle/src/main/java/bisq/oracle/explorer/ExplorerService.java b/oracle/src/main/java/bisq/oracle/explorer/ExplorerService.java new file mode 100644 index 0000000000..7c3ce68c02 --- /dev/null +++ b/oracle/src/main/java/bisq/oracle/explorer/ExplorerService.java @@ -0,0 +1,164 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.oracle.explorer; + +import bisq.common.data.Pair; +import bisq.common.observable.Observable; +import bisq.common.threading.ExecutorFactory; +import bisq.common.util.ExceptionUtil; +import bisq.network.NetworkService; +import bisq.network.http.common.BaseHttpClient; +import bisq.network.p2p.node.transport.Transport; +import bisq.oracle.explorer.dto.Tx; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; + + +@Slf4j +public class ExplorerService { + public static final ExecutorService POOL = ExecutorFactory.newFixedThreadPool("BlockExplorerService.pool", 3); + + private volatile boolean shutdownStarted; + + @Getter + @ToString + public static final class Config { + public static Config from(com.typesafe.config.Config config) { + //todo move to conf + return new Config(List.of( + //https://mempool.space/api/tx/15e10745f15593a899cef391191bdd3d7c12412cc4696b7bcb669d0feadc8521 + new Provider("https://mempool.emzy.de/", Transport.Type.CLEAR), + new Provider("http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/", Transport.Type.TOR) + + /* new BlockchainExplorerService.Provider("https://mempool.space/tx/", "https://mempool.space/address/", "mempool.space (@wiz)", Transport.Type.CLEAR), + new BlockchainExplorerService.Provider("http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/tx/", "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/", "mempool.space Tor V3", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://mempool.bisq.services/tx/", "https://mempool.bisq.services/address/", "mempool.bisq.services (@devinbileck)", Transport.Type.TOR), + new BlockchainExplorerService.Provider("http://mempoolcutehjtynu4k4rd746acmssvj2vz4jbz4setb72clbpx2dfqd.onion/tx/", "http://mempoolcutehjtynu4k4rd746acmssvj2vz4jbz4setb72clbpx2dfqd.onion/address/", "mempool.bisq.services Tor V3", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://blockstream.info/tx/", "https://blockstream.info/address/", "Blockstream.info", Transport.Type.TOR), + new BlockchainExplorerService.Provider("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/tx/", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/address/", "Blockstream.info Tor V3", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://oxt.me/transaction/", "https://oxt.me/address/", "OXT", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://bitaps.com/", "https://bitaps.com/", "Bitaps", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://live.blockcypher.com/btc/tx/", "https://live.blockcypher.com/btc/address/", "Blockcypher", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://tradeblock.com/bitcoin/tx/", "https://tradeblock.com/bitcoin/address/", "Tradeblock", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/", "Biteasy", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://www.blockonomics.co/api/tx?txid=", "https://www.blockonomics.co/#/search?q=", "Blockonomics", Transport.Type.TOR), + new BlockchainExplorerService.Provider("http://chainflyer.bitflyer.jp/Transaction/", "http://chainflyer.bitflyer.jp/Address/", "Chainflyer", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://www.smartbit.com.au/tx/", "https://www.smartbit.com.au/address/", "Smartbit", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://chain.so/tx/BTC/", "https://chain.so/address/BTC/", "SoChain. Wow.", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://blockchain.info/tx/", "https://blockchain.info/address/", "Blockchain.info", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://insight.bitpay.com/tx/", "https://insight.bitpay.com/address/", "Insight", Transport.Type.TOR), + new BlockchainExplorerService.Provider("https://blockchair.com/bitcoin/transaction/", "https://blockchair.com/bitcoin/address/", "Blockchair", Transport.Type.TOR)*/ + )); + } + + private final List providers; + + public Config(List providers) { + this.providers = providers; + } + } + + private static class PendingRequestException extends Exception { + public PendingRequestException() { + super("We have a pending request"); + } + } + + @Getter + @ToString + @EqualsAndHashCode + public static final class Provider { + private final String baseUrl; + private final String apiPath; + private final String txPath; + private final String addressPath; + private final Transport.Type transportType; + + public Provider(String baseUrl, Transport.Type transportType) { + this(baseUrl, "api/", "tx/", "address/", transportType); + } + + public Provider(String baseUrl, String apiPath, String txPath, String addressPath, Transport.Type transportType) { + this.baseUrl = baseUrl; + this.apiPath = apiPath; + this.txPath = txPath; + this.addressPath = addressPath; + this.transportType = transportType; + } + } + + + private final ArrayList providers; + @Getter + private final Observable selectedProvider = new Observable<>(); + private Optional httpClient = Optional.empty(); + private final NetworkService networkService; + private final String userAgent; + + + public ExplorerService(Config conf, NetworkService networkService, String version) { + providers = new ArrayList<>(conf.providers); + checkArgument(providers.size() > 0); + selectedProvider.set(providers.get(0)); + this.networkService = networkService; + userAgent = "bisq-v2/" + version; + } + + public CompletableFuture initialize() { + log.info("initialize"); + return CompletableFuture.completedFuture(true); + } + + public CompletableFuture shutdown() { + shutdownStarted = true; + httpClient.ifPresent(BaseHttpClient::shutdown); + return CompletableFuture.completedFuture(true); + } + + public CompletableFuture requestTx(String txId) { + Provider provider = selectedProvider.get(); + BaseHttpClient httpClient = networkService.getHttpClient(provider.baseUrl, userAgent, provider.transportType); + + return CompletableFuture.supplyAsync(() -> { + long ts = System.currentTimeMillis(); + String param = provider.getApiPath() + provider.getTxPath() + txId; + try { + String json = httpClient.get(param, Optional.of(new Pair<>("User-Agent", userAgent))); + log.info("Requesting tx from {} took {} ms", httpClient.getBaseUrl() + param, System.currentTimeMillis() - ts); + return new ObjectMapper().readValue(json, Tx.class); + } catch (IOException e) { + if (!shutdownStarted) { + log.info("Requesting tx from {} failed. {}" + httpClient.getBaseUrl() + param, ExceptionUtil.getMostMeaningfulMessage(e)); + } + throw new RuntimeException(e); + } + }, POOL); + } +} diff --git a/oracle/src/main/java/bisq/oracle/explorer/dto/Input.java b/oracle/src/main/java/bisq/oracle/explorer/dto/Input.java new file mode 100644 index 0000000000..48d39bf1e4 --- /dev/null +++ b/oracle/src/main/java/bisq/oracle/explorer/dto/Input.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.oracle.explorer.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"txid", "vout", "prevout", "scriptsig", "scriptsig_asm", "witness", "is_coinbase", "sequence"}) +public class Input { + @JsonProperty("txid") + private String txId; + @JsonProperty("vout") + private Integer outputIndex; + @JsonProperty("prevout") + private Output prevOut; + @JsonProperty("scriptsig") + private String scriptSig; + @JsonProperty("scriptsig_asm") + private String scriptSigAsm; + @JsonProperty("witness") + private List witness; + @JsonProperty("is_coinbase") + private boolean isCoinbase; + private long sequence; +} \ No newline at end of file diff --git a/oracle/src/main/java/bisq/oracle/explorer/dto/Output.java b/oracle/src/main/java/bisq/oracle/explorer/dto/Output.java new file mode 100644 index 0000000000..b21e2b44db --- /dev/null +++ b/oracle/src/main/java/bisq/oracle/explorer/dto/Output.java @@ -0,0 +1,42 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.oracle.explorer.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"scriptpubkey", "scriptpubkey_asm", "scriptpubkey_type", "scriptpubkey_address", "value"}) +public class Output { + @JsonProperty("scriptpubkey") + private String scriptPubKey; + @JsonProperty("scriptpubkey_asm") + private String scriptPubKeyAsm; + @JsonProperty("scriptpubkey_type") + private String scriptPubKeyType; + @JsonProperty("scriptpubkey_address") + private String address; + private long value; +} \ No newline at end of file diff --git a/oracle/src/main/java/bisq/oracle/explorer/dto/Status.java b/oracle/src/main/java/bisq/oracle/explorer/dto/Status.java new file mode 100644 index 0000000000..ebd4dddac6 --- /dev/null +++ b/oracle/src/main/java/bisq/oracle/explorer/dto/Status.java @@ -0,0 +1,40 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.oracle.explorer.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"confirmed", "block_height", "block_hash", "block_time"}) +public class Status { + private boolean confirmed; + @JsonProperty("block_height") + private int blockHeight; + @JsonProperty("block_hash") + private String blockHash; + @JsonProperty("block_time") + private long blockTime; +} \ No newline at end of file diff --git a/oracle/src/main/java/bisq/oracle/explorer/dto/Tx.java b/oracle/src/main/java/bisq/oracle/explorer/dto/Tx.java new file mode 100644 index 0000000000..4ba2333475 --- /dev/null +++ b/oracle/src/main/java/bisq/oracle/explorer/dto/Tx.java @@ -0,0 +1,49 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.oracle.explorer.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({"txid", "version", "locktime", "vin", "vout", "size", "weight", "fee", "status"}) +public class Tx { + @JsonProperty("txid") + private String txId; + private Integer version; + @JsonProperty("locktime") + private Long lockTime; + @JsonProperty("vin") + private List inputs; + @JsonProperty("vout") + private List outputs; + private int size; + private int weight; + private int fee; + private Status status; + +} diff --git a/oracle/src/main/java/bisq/oracle/marketprice/MarketPriceServiceConfigFactory.java b/oracle/src/main/java/bisq/oracle/marketprice/MarketPriceServiceConfigFactory.java deleted file mode 100644 index f67fb6e518..0000000000 --- a/oracle/src/main/java/bisq/oracle/marketprice/MarketPriceServiceConfigFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.oracle.marketprice; - -/** - * Parses the program arguments which are relevant for that domain and stores it in the options field. - */ -public class MarketPriceServiceConfigFactory { - -} \ No newline at end of file diff --git a/settings/build.gradle b/settings/build.gradle index b8b8902601..188692f794 100644 --- a/settings/build.gradle +++ b/settings/build.gradle @@ -6,5 +6,7 @@ plugins { dependencies { implementation project(':persistence') + implementation("network:network") + implementation libs.google.guava } diff --git a/support/src/main/java/bisq/support/MediationService.java b/support/src/main/java/bisq/support/MediationService.java index 82d59f0fa0..120e335745 100644 --- a/support/src/main/java/bisq/support/MediationService.java +++ b/support/src/main/java/bisq/support/MediationService.java @@ -48,24 +48,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArraySet; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j public class MediationService implements Service, DataService.Listener, MessageListener { - // This method can be used for verification when taker provides mediators list. - // If mediator list was not matching the expected one present in the network it might have been a manipulation attempt. - public static Optional selectMediator(Set mediators, String makersProfileId, String takersProfileId) { - if (mediators.isEmpty()) { - return Optional.empty(); - } else if (mediators.iterator().hasNext()) { - return Optional.of(mediators.iterator().next().getUserProfile()); - } else { - String concat = makersProfileId + takersProfileId; - int index = new BigInteger(concat.getBytes(StandardCharsets.UTF_8)).mod(BigInteger.valueOf(mediators.size())).intValue(); - return Optional.of(new ArrayList<>(mediators).get(index).getUserProfile()); - } - } - private final NetworkService networkService; private final Set mediators = new CopyOnWriteArraySet<>(); private final UserIdentityService userIdentityService; @@ -153,24 +137,24 @@ public void requestMediation(BisqEasyPrivateTradeChatChannel privateTradeChannel networkService.confidentialSend(networkMessage, mediator.getNetworkId(), myUserIdentity.getNodeIdAndKeyPair()); } - // As maker might have different mediator data we use the taker to select. For verification, we still can add - // a method that taker need to provide the data for the selection to the maker which would reveal if the selection - // was faked. - public Optional takerSelectMediator(String makersProfileId, String takersProfileId) { - return selectMediator(mediators, makersProfileId, takersProfileId); + public Optional selectMediator(String makersUserProfileId, String takersUserProfileId) { + return selectMediator(mediators, makersUserProfileId, takersUserProfileId); } - public Optional takerSelectMediator(String makersProfileId) { - return userProfileService.findUserProfile(makersProfileId) - .flatMap(makerUserProfile -> { - UserIdentity myUserIdentity = checkNotNull(userIdentityService.getSelectedUserIdentity()); - return takerSelectMediator(makerUserProfile.getId(), myUserIdentity.getUserProfile().getId()); - }) - .stream() - .findAny(); + // This method can be used for verification when taker provides mediators list. + // If mediator list was not matching the expected one present in the network it might have been a manipulation attempt. + public Optional selectMediator(Set mediators, String makersProfileId, String takersProfileId) { + if (mediators.isEmpty()) { + return Optional.empty(); + } else if (mediators.size() == 1 && mediators.iterator().hasNext()) { + return Optional.of(mediators.iterator().next().getUserProfile()); + } else { + String concat = makersProfileId + takersProfileId; + int index = new BigInteger(concat.getBytes(StandardCharsets.UTF_8)).mod(BigInteger.valueOf(mediators.size())).intValue(); + return Optional.of(new ArrayList<>(mediators).get(index).getUserProfile()); + } } - /////////////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/trade/build.gradle b/trade/build.gradle index afc84a1d02..deafe7a719 100644 --- a/trade/build.gradle +++ b/trade/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation project(':offer') implementation project(':contract') implementation project(':support') + implementation project(':chat') implementation project(':oracle') implementation("network:network") diff --git a/trade/src/main/java/bisq/trade/ServiceProvider.java b/trade/src/main/java/bisq/trade/ServiceProvider.java index 96c0cb617a..8769d2baf6 100644 --- a/trade/src/main/java/bisq/trade/ServiceProvider.java +++ b/trade/src/main/java/bisq/trade/ServiceProvider.java @@ -17,33 +17,20 @@ package bisq.trade; -import bisq.contract.ContractService; -import bisq.identity.IdentityService; -import bisq.network.NetworkService; -import bisq.offer.OfferService; -import bisq.support.MediationService; -import bisq.support.SupportService; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Getter -public class ServiceProvider { - private final IdentityService identityService; - private final OfferService offerService; - private final ContractService contractService; - private final MediationService mediationService; - private final NetworkService networkService; - - public ServiceProvider(NetworkService networkService, - IdentityService identityService, - OfferService offerService, - ContractService contractService, - SupportService supportService) { - this.networkService = networkService; - this.identityService = identityService; - this.offerService = offerService; - this.contractService = contractService; - this.mediationService = supportService.getMediationService(); - } -} \ No newline at end of file +public interface ServiceProvider { + bisq.network.NetworkService getNetworkService(); + + bisq.identity.IdentityService getIdentityService(); + + bisq.persistence.PersistenceService getPersistenceService(); + + bisq.offer.OfferService getOfferService(); + + bisq.contract.ContractService getContractService(); + + bisq.support.SupportService getSupportService(); + + bisq.chat.ChatService getChatService(); + + bisq.oracle.OracleService getOracleService(); +} diff --git a/trade/src/main/java/bisq/trade/TradeService.java b/trade/src/main/java/bisq/trade/TradeService.java index 09afc6087c..996524115d 100644 --- a/trade/src/main/java/bisq/trade/TradeService.java +++ b/trade/src/main/java/bisq/trade/TradeService.java @@ -17,11 +17,13 @@ package bisq.trade; +import bisq.chat.ChatService; import bisq.common.application.Service; import bisq.contract.ContractService; import bisq.identity.IdentityService; import bisq.network.NetworkService; import bisq.offer.OfferService; +import bisq.oracle.OracleService; import bisq.persistence.PersistenceService; import bisq.support.SupportService; import bisq.trade.bisq_easy.BisqEasyTradeService; @@ -32,22 +34,35 @@ @Slf4j @Getter -public class TradeService implements Service { +public class TradeService implements Service, ServiceProvider { private final BisqEasyTradeService bisqEasyTradeService; + private final NetworkService networkService; + private final IdentityService identityService; + private final PersistenceService persistenceService; + private final OfferService offerService; + private final ContractService contractService; + private final SupportService supportService; + private final ChatService chatService; + private final OracleService oracleService; public TradeService(NetworkService networkService, IdentityService identityService, PersistenceService persistenceService, OfferService offerService, ContractService contractService, - SupportService supportService) { + SupportService supportService, + ChatService chatService, + OracleService oracleService) { + this.networkService = networkService; + this.identityService = identityService; + this.persistenceService = persistenceService; + this.offerService = offerService; + this.contractService = contractService; + this.supportService = supportService; + this.chatService = chatService; + this.oracleService = oracleService; - bisqEasyTradeService = new BisqEasyTradeService(networkService, - identityService, - persistenceService, - offerService, - contractService, - supportService); + bisqEasyTradeService = new BisqEasyTradeService(this); } diff --git a/trade/src/main/java/bisq/trade/bisq_easy/BisqEasyTradeService.java b/trade/src/main/java/bisq/trade/bisq_easy/BisqEasyTradeService.java index 923b16f433..7eaa6f64f7 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/BisqEasyTradeService.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/BisqEasyTradeService.java @@ -19,31 +19,21 @@ import bisq.common.application.Service; import bisq.common.monetary.Monetary; -import bisq.contract.ContractService; import bisq.contract.bisq_easy.BisqEasyContract; import bisq.identity.Identity; -import bisq.identity.IdentityService; import bisq.network.NetworkId; -import bisq.network.NetworkService; import bisq.network.p2p.message.NetworkMessage; import bisq.network.p2p.services.confidential.MessageListener; -import bisq.offer.OfferService; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.offer.payment_method.BitcoinPaymentMethodSpec; import bisq.offer.payment_method.FiatPaymentMethodSpec; import bisq.persistence.Persistence; import bisq.persistence.PersistenceClient; -import bisq.persistence.PersistenceService; -import bisq.support.MediationService; -import bisq.support.SupportService; import bisq.trade.ServiceProvider; import bisq.trade.TradeException; import bisq.trade.bisq_easy.protocol.*; import bisq.trade.bisq_easy.protocol.events.*; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyAccountDataMessage; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmBtcSentMessage; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmFiatSentMessage; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyTakeOfferRequest; +import bisq.trade.bisq_easy.protocol.messages.*; import bisq.trade.protocol.Protocol; import bisq.user.profile.UserProfile; import lombok.Getter; @@ -55,6 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @Getter @@ -63,33 +54,14 @@ public class BisqEasyTradeService implements PersistenceClient persistence; - private final IdentityService identityService; - private final OfferService offerService; - private final ContractService contractService; - private final MediationService mediationService; - private final NetworkService networkService; private final ServiceProvider serviceProvider; // We don't persist the protocol, only the model. private final Map tradeProtocolById = new ConcurrentHashMap<>(); - public BisqEasyTradeService(NetworkService networkService, - IdentityService identityService, - PersistenceService persistenceService, - OfferService offerService, - ContractService contractService, - SupportService supportService) { - this.networkService = networkService; - this.identityService = identityService; - this.offerService = offerService; - this.contractService = contractService; - this.mediationService = supportService.getMediationService(); - serviceProvider = new ServiceProvider(networkService, - identityService, - offerService, - contractService, - supportService); - persistence = persistenceService.getOrCreatePersistence(this, persistableStore); + public BisqEasyTradeService(ServiceProvider serviceProvider) { + persistence = serviceProvider.getPersistenceService().getOrCreatePersistence(this, persistableStore); + this.serviceProvider = serviceProvider; } @@ -98,7 +70,7 @@ public BisqEasyTradeService(NetworkService networkService, /////////////////////////////////////////////////////////////////////////////////////////////////// public CompletableFuture initialize() { - networkService.addMessageListener(this); + serviceProvider.getNetworkService().addMessageListener(this); persistableStore.getTradeById().values().forEach(this::createAndAddTradeProtocol); @@ -106,7 +78,7 @@ public CompletableFuture initialize() { } public CompletableFuture shutdown() { - networkService.removeMessageListener(this); + serviceProvider.getNetworkService().removeMessageListener(this); return CompletableFuture.completedFuture(true); } @@ -119,6 +91,8 @@ public CompletableFuture shutdown() { public void onMessage(NetworkMessage networkMessage) { if (networkMessage instanceof BisqEasyTakeOfferRequest) { onBisqEasyTakeOfferMessage((BisqEasyTakeOfferRequest) networkMessage); + } else if (networkMessage instanceof BisqEasyTakeOfferResponse) { + onBisqEasyTakeOfferResponse((BisqEasyTakeOfferResponse) networkMessage); } else if (networkMessage instanceof BisqEasyAccountDataMessage) { onBisqEasySendAccountDataMessage((BisqEasyAccountDataMessage) networkMessage); } else if (networkMessage instanceof BisqEasyConfirmFiatSentMessage) { @@ -126,6 +100,7 @@ public void onMessage(NetworkMessage networkMessage) { } else if (networkMessage instanceof BisqEasyConfirmBtcSentMessage) { onBisqEasyConfirmBtcSentMessage((BisqEasyConfirmBtcSentMessage) networkMessage); } + } @@ -134,8 +109,8 @@ public void onMessage(NetworkMessage networkMessage) { /////////////////////////////////////////////////////////////////////////////////////////////////// private void onBisqEasyTakeOfferMessage(BisqEasyTakeOfferRequest message) { - NetworkId sender = message.getSender(); - BisqEasyContract bisqEasyContract = message.getBisqEasyContract(); + NetworkId sender = checkNotNull(message.getSender()); + BisqEasyContract bisqEasyContract = checkNotNull(message.getBisqEasyContract()); boolean isBuyer = bisqEasyContract.getOffer().getMakersDirection().isBuy(); Identity myIdentity = serviceProvider.getIdentityService().findAnyIdentityByNodeId(bisqEasyContract.getOffer().getMakerNetworkId().getNodeId()).orElseThrow(); BisqEasyTrade tradeModel = new BisqEasyTrade(isBuyer, false, myIdentity, bisqEasyContract, sender); @@ -155,6 +130,17 @@ private void onBisqEasyTakeOfferMessage(BisqEasyTakeOfferRequest message) { } } + private void onBisqEasyTakeOfferResponse(BisqEasyTakeOfferResponse message) { + findProtocol(message.getTradeId()).ifPresent(protocol -> { + try { + protocol.handle(message); + persist(); + } catch (TradeException e) { + log.error("Error at processing message " + message, e); + } + }); + } + private void onBisqEasySendAccountDataMessage(BisqEasyAccountDataMessage message) { findProtocol(message.getTradeId()).ifPresent(protocol -> { try { @@ -199,7 +185,7 @@ public BisqEasyTrade onTakeOffer(Identity takerIdentity, Monetary quoteSideAmount, BitcoinPaymentMethodSpec bitcoinPaymentMethodSpec, FiatPaymentMethodSpec fiatPaymentMethodSpec) throws TradeException { - Optional mediator = serviceProvider.getMediationService().takerSelectMediator(bisqEasyOffer.getMakersUserProfileId()); + Optional mediator = serviceProvider.getSupportService().getMediationService().selectMediator(bisqEasyOffer.getMakersUserProfileId(), takerIdentity.getId()); NetworkId takerNetworkId = takerIdentity.getNetworkId(); BisqEasyContract contract = new BisqEasyContract(bisqEasyOffer, takerNetworkId, @@ -246,12 +232,6 @@ public void btcConfirmed(BisqEasyTrade tradeModel) throws TradeException { persist(); } - public void tradeCompleted(BisqEasyTrade tradeModel) throws TradeException { - BisqEasyProtocol protocol = findProtocol(tradeModel.getId()).orElseThrow(); - protocol.handle(new BisqEasyTradeCompletedEvent()); - persist(); - } - /////////////////////////////////////////////////////////////////////////////////////////////////// // Misc API diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsMakerProtocol.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsMakerProtocol.java index 09f0b39dd7..ef54812e8e 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsMakerProtocol.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsMakerProtocol.java @@ -19,7 +19,10 @@ import bisq.trade.ServiceProvider; import bisq.trade.bisq_easy.BisqEasyTrade; -import bisq.trade.bisq_easy.protocol.events.*; +import bisq.trade.bisq_easy.protocol.events.BisqEasyBtcConfirmedEvent; +import bisq.trade.bisq_easy.protocol.events.BisqEasyBtcConfirmedEventHandler; +import bisq.trade.bisq_easy.protocol.events.BisqEasyConfirmFiatSentEvent; +import bisq.trade.bisq_easy.protocol.events.BisqEasyConfirmFiatSentEventHandler; import bisq.trade.bisq_easy.protocol.messages.*; import static bisq.trade.bisq_easy.protocol.BisqEasyTradeState.*; @@ -36,10 +39,10 @@ public void configTransitions() { .from(INIT) .on(BisqEasyTakeOfferRequest.class) .run(BisqEasyTakeOfferRequestHandler.class) - .to(MAKER_RECEIVED_TAKE_OFFER_REQUEST); + .to(MAKER_SENT_TAKE_OFFER_RESPONSE); addTransition() - .from(MAKER_RECEIVED_TAKE_OFFER_REQUEST) + .from(MAKER_SENT_TAKE_OFFER_RESPONSE) .on(BisqEasyAccountDataMessage.class) .run(BisqEasyAccountDataMessageHandler.class) .to(BUYER_RECEIVED_ACCOUNT_DATA); @@ -61,10 +64,5 @@ public void configTransitions() { .on(BisqEasyBtcConfirmedEvent.class) .run(BisqEasyBtcConfirmedEventHandler.class) .to(BTC_CONFIRMED); - - addTransition() - .from(BTC_CONFIRMED) - .on(BisqEasyTradeCompletedEvent.class) - .to(COMPLETED); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsTakerProtocol.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsTakerProtocol.java index 08f7f782f7..4973356603 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsTakerProtocol.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyBuyerAsTakerProtocol.java @@ -20,10 +20,7 @@ import bisq.trade.ServiceProvider; import bisq.trade.bisq_easy.BisqEasyTrade; import bisq.trade.bisq_easy.protocol.events.*; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyAccountDataMessage; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyAccountDataMessageHandler; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmBtcSentMessage; -import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmBtcSentMessageHandler; +import bisq.trade.bisq_easy.protocol.messages.*; import static bisq.trade.bisq_easy.protocol.BisqEasyTradeState.*; @@ -39,10 +36,16 @@ public void configTransitions() { .from(INIT) .on(BisqEasyTakeOfferEvent.class) .run(BisqEasyTakeOfferEventHandler.class) - .to(TAKER_SEND_TAKE_OFFER_REQUEST); + .to(TAKER_SENT_TAKE_OFFER_REQUEST); addTransition() - .from(TAKER_SEND_TAKE_OFFER_REQUEST) + .from(TAKER_SENT_TAKE_OFFER_REQUEST) + .on(BisqEasyTakeOfferResponse.class) + .run(BisqEasyTakeOfferResponseHandler.class) + .to(TAKER_RECEIVED_TAKE_OFFER_RESPONSE); + + addTransition() + .from(TAKER_RECEIVED_TAKE_OFFER_RESPONSE) .on(BisqEasyAccountDataMessage.class) .run(BisqEasyAccountDataMessageHandler.class) .to(BUYER_RECEIVED_ACCOUNT_DATA); @@ -64,10 +67,5 @@ public void configTransitions() { .on(BisqEasyBtcConfirmedEvent.class) .run(BisqEasyBtcConfirmedEventHandler.class) .to(BTC_CONFIRMED); - - addTransition() - .from(BTC_CONFIRMED) - .on(BisqEasyTradeCompletedEvent.class) - .to(COMPLETED); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsMakerProtocol.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsMakerProtocol.java index fe8b1dd472..874740f4d9 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsMakerProtocol.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsMakerProtocol.java @@ -38,10 +38,10 @@ public void configTransitions() { .from(INIT) .on(BisqEasyTakeOfferRequest.class) .run(BisqEasyTakeOfferRequestHandler.class) - .to(MAKER_RECEIVED_TAKE_OFFER_REQUEST); + .to(MAKER_SENT_TAKE_OFFER_RESPONSE); addTransition() - .from(MAKER_RECEIVED_TAKE_OFFER_REQUEST) + .from(MAKER_SENT_TAKE_OFFER_RESPONSE) .on(BisqEasyAccountDataEvent.class) .run(BisqEasyAccountDataEventHandler.class) .to(SELLER_SENT_ACCOUNT_DATA); @@ -63,10 +63,5 @@ public void configTransitions() { .on(BisqEasyBtcConfirmedEvent.class) .run(BisqEasyBtcConfirmedEventHandler.class) .to(BTC_CONFIRMED); - - addTransition() - .from(BTC_CONFIRMED) - .on(BisqEasyTradeCompletedEvent.class) - .to(COMPLETED); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsTakerProtocol.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsTakerProtocol.java index 8c09db7b21..fecb2a6a58 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsTakerProtocol.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasySellerAsTakerProtocol.java @@ -22,6 +22,8 @@ import bisq.trade.bisq_easy.protocol.events.*; import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmFiatSentMessage; import bisq.trade.bisq_easy.protocol.messages.BisqEasyConfirmFiatSentMessageHandler; +import bisq.trade.bisq_easy.protocol.messages.BisqEasyTakeOfferResponse; +import bisq.trade.bisq_easy.protocol.messages.BisqEasyTakeOfferResponseHandler; import static bisq.trade.bisq_easy.protocol.BisqEasyTradeState.*; @@ -36,10 +38,16 @@ public void configTransitions() { .from(INIT) .on(BisqEasyTakeOfferEvent.class) .run(BisqEasyTakeOfferEventHandler.class) - .to(TAKER_SEND_TAKE_OFFER_REQUEST); + .to(TAKER_SENT_TAKE_OFFER_REQUEST); addTransition() - .from(TAKER_SEND_TAKE_OFFER_REQUEST) + .from(TAKER_SENT_TAKE_OFFER_REQUEST) + .on(BisqEasyTakeOfferResponse.class) + .run(BisqEasyTakeOfferResponseHandler.class) + .to(TAKER_RECEIVED_TAKE_OFFER_RESPONSE); + + addTransition() + .from(TAKER_RECEIVED_TAKE_OFFER_RESPONSE) .on(BisqEasyAccountDataEvent.class) .run(BisqEasyAccountDataEventHandler.class) .to(SELLER_SENT_ACCOUNT_DATA); @@ -61,10 +69,5 @@ public void configTransitions() { .on(BisqEasyBtcConfirmedEvent.class) .run(BisqEasyBtcConfirmedEventHandler.class) .to(BTC_CONFIRMED); - - addTransition() - .from(BTC_CONFIRMED) - .on(BisqEasyTradeCompletedEvent.class) - .to(COMPLETED); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyTradeState.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyTradeState.java index ea79aff1ae..ed604e896c 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyTradeState.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/BisqEasyTradeState.java @@ -26,8 +26,9 @@ public enum BisqEasyTradeState implements State { INIT, - TAKER_SEND_TAKE_OFFER_REQUEST, - MAKER_RECEIVED_TAKE_OFFER_REQUEST, + TAKER_SENT_TAKE_OFFER_REQUEST, + MAKER_SENT_TAKE_OFFER_RESPONSE, + TAKER_RECEIVED_TAKE_OFFER_RESPONSE, SELLER_SENT_ACCOUNT_DATA, BUYER_RECEIVED_ACCOUNT_DATA, @@ -38,8 +39,7 @@ public enum BisqEasyTradeState implements State { SELLER_SENT_BTC_SENT_CONFIRMATION, BUYER_RECEIVED_BTC_SENT_CONFIRMATION, - BTC_CONFIRMED, - COMPLETED(true); + BTC_CONFIRMED(true); private final boolean isFinalState; diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyAccountDataEventHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyAccountDataEventHandler.java index 767fa83728..6c70f99130 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyAccountDataEventHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyAccountDataEventHandler.java @@ -34,10 +34,10 @@ public void handle(Event event) { BisqEasyAccountDataEvent bisqEasyTakeOfferEvent = (BisqEasyAccountDataEvent) event; String paymentAccountData = bisqEasyTakeOfferEvent.getPaymentAccountData(); commitToModel(paymentAccountData); - sendMessage(new BisqEasyAccountDataMessage(model.getId(), model.getMyIdentity().getNetworkId(), paymentAccountData)); + sendMessage(new BisqEasyAccountDataMessage(trade.getId(), trade.getMyIdentity().getNetworkId(), paymentAccountData)); } private void commitToModel(String paymentAccountData) { - model.getPaymentAccountData().set(paymentAccountData); + trade.getPaymentAccountData().set(paymentAccountData); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmBtcSentEventHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmBtcSentEventHandler.java index ea45e27f14..6a282b3d50 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmBtcSentEventHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmBtcSentEventHandler.java @@ -34,10 +34,10 @@ public void handle(Event event) { BisqEasyConfirmBtcSentEvent bisqEasyConfirmBtcSentEvent = (BisqEasyConfirmBtcSentEvent) event; String txId = bisqEasyConfirmBtcSentEvent.getTxId(); commitToModel(txId); - sendMessage(new BisqEasyConfirmBtcSentMessage(model.getId(), model.getMyIdentity().getNetworkId(), txId)); + sendMessage(new BisqEasyConfirmBtcSentMessage(trade.getId(), trade.getMyIdentity().getNetworkId(), txId)); } private void commitToModel(String txId) { - model.getTxId().set(txId); + trade.getTxId().set(txId); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmFiatSentEventHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmFiatSentEventHandler.java index 2a574bd426..591b59b505 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmFiatSentEventHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyConfirmFiatSentEventHandler.java @@ -34,10 +34,10 @@ public void handle(Event event) { BisqEasyConfirmFiatSentEvent bisqEasyConfirmFiatSentEvent = (BisqEasyConfirmFiatSentEvent) event; String btcAddress = bisqEasyConfirmFiatSentEvent.getBtcAddress(); commitToModel(btcAddress); - sendMessage(new BisqEasyConfirmFiatSentMessage(model.getId(), model.getMyIdentity().getNetworkId(), btcAddress)); + sendMessage(new BisqEasyConfirmFiatSentMessage(trade.getId(), trade.getMyIdentity().getNetworkId(), btcAddress)); } private void commitToModel(String btcAddress) { - model.getBtcAddress().set(btcAddress); + trade.getBtcAddress().set(btcAddress); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTakeOfferEventHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTakeOfferEventHandler.java index cd18fe8394..02f504ed3d 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTakeOfferEventHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTakeOfferEventHandler.java @@ -38,15 +38,15 @@ public void handle(Event event) { BisqEasyTakeOfferEvent bisqEasyTakeOfferEvent = (BisqEasyTakeOfferEvent) event; BisqEasyContract bisqEasyContract = bisqEasyTakeOfferEvent.getBisqEasyContract(); try { - ContractSignatureData myContractSignatureData = serviceProvider.getContractService().signContract(bisqEasyContract, model.getMyIdentity().getKeyPair()); - commitToModel(myContractSignatureData); - sendMessage(new BisqEasyTakeOfferRequest(model.getId(), model.getMyIdentity().getNetworkId(), bisqEasyContract, myContractSignatureData)); + ContractSignatureData contractSignatureData = serviceProvider.getContractService().signContract(bisqEasyContract, trade.getMyIdentity().getKeyPair()); + commitToModel(contractSignatureData); + sendMessage(new BisqEasyTakeOfferRequest(trade.getId(), trade.getMyIdentity().getNetworkId(), bisqEasyContract, contractSignatureData)); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } - private void commitToModel(ContractSignatureData takersContractSignatureData) { - model.getMyself().getContractSignatureData().set(takersContractSignatureData); + private void commitToModel(ContractSignatureData contractSignatureData) { + trade.getTaker().getContractSignatureData().set(contractSignatureData); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTradeCompletedEvent.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTradeCompletedEvent.java deleted file mode 100644 index bcf6e759a0..0000000000 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/events/BisqEasyTradeCompletedEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.trade.bisq_easy.protocol.events; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; - -@ToString(callSuper = true) -@Getter -@EqualsAndHashCode(callSuper = true) -public class BisqEasyTradeCompletedEvent extends BisqEasyTradeEvent { - - public BisqEasyTradeCompletedEvent() { - } -} \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyAccountDataMessageHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyAccountDataMessageHandler.java index 683987ca5d..355a9522cf 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyAccountDataMessageHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyAccountDataMessageHandler.java @@ -17,6 +17,7 @@ package bisq.trade.bisq_easy.protocol.messages; +import bisq.account.accounts.UserDefinedFiatAccountPayload; import bisq.common.fsm.Event; import bisq.common.util.StringUtils; import bisq.trade.ServiceProvider; @@ -29,12 +30,10 @@ @Slf4j public class BisqEasyAccountDataMessageHandler extends TradeMessageHandler { - public BisqEasyAccountDataMessageHandler(ServiceProvider serviceProvider, - BisqEasyTrade model) { + public BisqEasyAccountDataMessageHandler(ServiceProvider serviceProvider, BisqEasyTrade model) { super(serviceProvider, model); } - @Override public void handle(Event event) { BisqEasyAccountDataMessage message = (BisqEasyAccountDataMessage) event; @@ -44,11 +43,13 @@ public void handle(Event event) { @Override protected void verifyMessage(BisqEasyAccountDataMessage message) { - //todo + super.verifyMessage(message); + checkArgument(StringUtils.isNotEmpty(message.getPaymentAccountData())); + checkArgument(message.getPaymentAccountData().length() <= UserDefinedFiatAccountPayload.MAX_DATA_LENGTH); } private void commitToModel(String paymentAccountData) { - model.getPaymentAccountData().set(paymentAccountData); + trade.getPaymentAccountData().set(paymentAccountData); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmBtcSentMessageHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmBtcSentMessageHandler.java index c57e51b9ec..267cfe71a4 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmBtcSentMessageHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmBtcSentMessageHandler.java @@ -29,8 +29,7 @@ @Slf4j public class BisqEasyConfirmBtcSentMessageHandler extends TradeMessageHandler { - public BisqEasyConfirmBtcSentMessageHandler(ServiceProvider serviceProvider, - BisqEasyTrade model) { + public BisqEasyConfirmBtcSentMessageHandler(ServiceProvider serviceProvider, BisqEasyTrade model) { super(serviceProvider, model); } @@ -44,11 +43,14 @@ public void handle(Event event) { @Override protected void verifyMessage(BisqEasyConfirmBtcSentMessage message) { - //todo + super.verifyMessage(message); + checkArgument(StringUtils.isNotEmpty(message.getTxId())); + // We leave it flexible so that users can use other than BTC mainnet data as txId + checkArgument(message.getTxId().length() <= 100); } private void commitToModel(String txId) { - model.getTxId().set(txId); + trade.getTxId().set(txId); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmFiatSentMessageHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmFiatSentMessageHandler.java index 76f1c7838d..be8fcdb119 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmFiatSentMessageHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyConfirmFiatSentMessageHandler.java @@ -29,8 +29,7 @@ @Slf4j public class BisqEasyConfirmFiatSentMessageHandler extends TradeMessageHandler { - public BisqEasyConfirmFiatSentMessageHandler(ServiceProvider serviceProvider, - BisqEasyTrade model) { + public BisqEasyConfirmFiatSentMessageHandler(ServiceProvider serviceProvider, BisqEasyTrade model) { super(serviceProvider, model); } @@ -44,11 +43,14 @@ public void handle(Event event) { @Override protected void verifyMessage(BisqEasyConfirmFiatSentMessage message) { - //todo + super.verifyMessage(message); + checkArgument(StringUtils.isNotEmpty(message.getBtcAddress())); + // We leave it flexible so that users can use other than a BTC address data as btcAddress + checkArgument(message.getBtcAddress().length() <= 100); } private void commitToModel(String btcAddress) { - model.getBtcAddress().set(btcAddress); + trade.getBtcAddress().set(btcAddress); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferRequestHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferRequestHandler.java index cb8798920a..578ee39e73 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferRequestHandler.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferRequestHandler.java @@ -18,24 +18,29 @@ package bisq.trade.bisq_easy.protocol.messages; import bisq.common.fsm.Event; +import bisq.common.monetary.Monetary; +import bisq.contract.ContractService; import bisq.contract.ContractSignatureData; import bisq.contract.bisq_easy.BisqEasyContract; -import bisq.identity.Identity; +import bisq.offer.amount.OfferAmountUtil; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.trade.ServiceProvider; import bisq.trade.bisq_easy.BisqEasyTrade; import bisq.trade.protocol.events.TradeMessageHandler; +import bisq.trade.protocol.events.TradeMessageSender; import bisq.user.profile.UserProfile; import lombok.extern.slf4j.Slf4j; import java.security.GeneralSecurityException; import java.util.Optional; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j -public class BisqEasyTakeOfferRequestHandler extends TradeMessageHandler { +public class BisqEasyTakeOfferRequestHandler extends TradeMessageHandler implements TradeMessageSender { - public BisqEasyTakeOfferRequestHandler(ServiceProvider serviceProvider, - BisqEasyTrade model) { + public BisqEasyTakeOfferRequestHandler(ServiceProvider serviceProvider, BisqEasyTrade model) { super(serviceProvider, model); } @@ -44,14 +49,17 @@ public void handle(Event event) { BisqEasyTakeOfferRequest message = (BisqEasyTakeOfferRequest) event; verifyMessage(message); - BisqEasyContract bisqEasyContract = message.getBisqEasyContract(); + BisqEasyContract contract = message.getBisqEasyContract(); ContractSignatureData takersContractSignatureData = message.getContractSignatureData(); - BisqEasyOffer bisqEasyOffer = bisqEasyContract.getOffer(); - Identity myIdentity = serviceProvider.getIdentityService().findAnyIdentityByNodeId(bisqEasyOffer.getMakerNetworkId().getNodeId()).orElseThrow(); + ContractService contractService = serviceProvider.getContractService(); try { - ContractSignatureData makersContractSignatureData = serviceProvider.getContractService().signContract(bisqEasyContract, myIdentity.getKeyPair()); + checkArgument(contractService.verifyContractSignature(contract, takersContractSignatureData)); + + ContractSignatureData makersContractSignatureData = contractService.signContract(contract, trade.getMyIdentity().getKeyPair()); commitToModel(takersContractSignatureData, makersContractSignatureData); - //todo sent my sig to peer + + BisqEasyTakeOfferResponse response = new BisqEasyTakeOfferResponse(trade.getId(), trade.getMyself().getNetworkId(), makersContractSignatureData); + sendMessage(response, serviceProvider, trade); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } @@ -59,14 +67,40 @@ public void handle(Event event) { @Override protected void verifyMessage(BisqEasyTakeOfferRequest message) { - BisqEasyContract bisqEasyContract = message.getBisqEasyContract(); - BisqEasyOffer bisqEasyOffer = bisqEasyContract.getOffer(); - Optional mediator = serviceProvider.getMediationService().takerSelectMediator(bisqEasyOffer.getMakersUserProfileId()); - //todo + super.verifyMessage(message); + + BisqEasyContract takersContract = checkNotNull(message.getBisqEasyContract()); + BisqEasyOffer takersOffer = checkNotNull(takersContract.getOffer()); + serviceProvider.getChatService().getBisqEasyPublicChatChannelService().getChannels().stream() + .flatMap(channel -> channel.getChatMessages().stream()) + .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) + .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) + .filter(offer -> offer.getMakerNetworkId().equals(trade.getMyIdentity().getNetworkId())) + .filter(offer -> offer.equals(takersOffer)) + .findAny() + .orElseThrow(); + + checkArgument(message.getSender().equals(takersContract.getTaker().getNetworkId())); + + Monetary baseSideMinAmount = OfferAmountUtil.findBaseSideMinOrFixedAmount(serviceProvider.getOracleService().getMarketPriceService(), takersOffer).orElseThrow(); + Monetary baseSideMaxAmount = OfferAmountUtil.findBaseSideMaxOrFixedAmount(serviceProvider.getOracleService().getMarketPriceService(), takersOffer).orElseThrow(); + checkArgument(takersContract.getBaseSideAmount() >= baseSideMinAmount.getValue()); + checkArgument(takersContract.getBaseSideAmount() <= baseSideMaxAmount.getValue()); + + Monetary quoteSideMinAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(serviceProvider.getOracleService().getMarketPriceService(), takersOffer).orElseThrow(); + Monetary quoteSideMaxAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(serviceProvider.getOracleService().getMarketPriceService(), takersOffer).orElseThrow(); + checkArgument(takersContract.getQuoteSideAmount() >= quoteSideMinAmount.getValue()); + checkArgument(takersContract.getQuoteSideAmount() <= quoteSideMaxAmount.getValue()); + + checkArgument(takersOffer.getBaseSidePaymentMethodSpecs().contains(takersContract.getBaseSidePaymentMethodSpec())); + checkArgument(takersOffer.getQuoteSidePaymentMethodSpecs().contains(takersContract.getQuoteSidePaymentMethodSpec())); + + Optional mediator = serviceProvider.getSupportService().getMediationService().selectMediator(takersOffer.getMakersUserProfileId(), trade.getTaker().getNetworkId().getId()); + checkArgument(mediator.equals(takersContract.getMediator()), "Mediators do not match"); } private void commitToModel(ContractSignatureData takersContractSignatureData, ContractSignatureData makersContractSignatureData) { - model.getPeer().getContractSignatureData().set(takersContractSignatureData); - model.getMyself().getContractSignatureData().set(makersContractSignatureData); + trade.getTaker().getContractSignatureData().set(takersContractSignatureData); + trade.getMaker().getContractSignatureData().set(makersContractSignatureData); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponse.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponse.java new file mode 100644 index 0000000000..2d43234591 --- /dev/null +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponse.java @@ -0,0 +1,70 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.trade.bisq_easy.protocol.messages; + +import bisq.contract.ContractSignatureData; +import bisq.network.NetworkId; +import bisq.network.p2p.services.data.storage.MetaData; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@ToString(callSuper = true) +@Getter +@EqualsAndHashCode(callSuper = true) +public class BisqEasyTakeOfferResponse extends BisqEasyTradeMessage { + public final static long TTL = TimeUnit.DAYS.toMillis(10); + + private final ContractSignatureData contractSignatureData; + + public BisqEasyTakeOfferResponse(String tradeId, NetworkId sender, ContractSignatureData contractSignatureData) { + this(tradeId, + sender, + contractSignatureData, + new MetaData(TTL, 100000, BisqEasyTakeOfferResponse.class.getSimpleName())); + } + + private BisqEasyTakeOfferResponse(String tradeId, NetworkId sender, ContractSignatureData contractSignatureData, MetaData metaData) { + super(tradeId, sender, metaData); + + this.contractSignatureData = contractSignatureData; + } + + @Override + protected bisq.trade.protobuf.TradeMessage toTradeMessageProto() { + return getTradeMessageBuilder() + .setBisqEasyTradeMessage(bisq.trade.protobuf.BisqEasyTradeMessage.newBuilder() + .setBisqEasyTakeOfferResponse( + bisq.trade.protobuf.BisqEasyTakeOfferResponse.newBuilder() + .setContractSignatureData(contractSignatureData.toProto()))) + .build(); + } + + public static BisqEasyTakeOfferResponse fromProto(bisq.trade.protobuf.TradeMessage proto) { + bisq.trade.protobuf.BisqEasyTakeOfferResponse response = proto.getBisqEasyTradeMessage().getBisqEasyTakeOfferResponse(); + return new BisqEasyTakeOfferResponse( + proto.getTradeId(), + NetworkId.fromProto(proto.getSender()), + ContractSignatureData.fromProto(response.getContractSignatureData()), + MetaData.fromProto(proto.getMetaData())); + } +} \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponseHandler.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponseHandler.java new file mode 100644 index 0000000000..ff55a48cdf --- /dev/null +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferResponseHandler.java @@ -0,0 +1,67 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.trade.bisq_easy.protocol.messages; + +import bisq.common.fsm.Event; +import bisq.contract.ContractService; +import bisq.contract.ContractSignatureData; +import bisq.trade.ServiceProvider; +import bisq.trade.bisq_easy.BisqEasyTrade; +import bisq.trade.protocol.events.TradeMessageHandler; +import bisq.trade.protocol.events.TradeMessageSender; +import lombok.extern.slf4j.Slf4j; + +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class BisqEasyTakeOfferResponseHandler extends TradeMessageHandler implements TradeMessageSender { + + public BisqEasyTakeOfferResponseHandler(ServiceProvider serviceProvider, BisqEasyTrade model) { + super(serviceProvider, model); + } + + @Override + public void handle(Event event) { + BisqEasyTakeOfferResponse message = (BisqEasyTakeOfferResponse) event; + verifyMessage(message); + commitToModel(message.getContractSignatureData()); + } + + @Override + protected void verifyMessage(BisqEasyTakeOfferResponse message) { + super.verifyMessage(message); + + ContractSignatureData makersContractSignatureData = message.getContractSignatureData(); + ContractSignatureData takersContractSignatureData = trade.getTaker().getContractSignatureData().get(); + checkArgument(Arrays.equals(makersContractSignatureData.getContractHash(), takersContractSignatureData.getContractHash())); + + ContractService contractService = serviceProvider.getContractService(); + try { + checkArgument(contractService.verifyContractSignature(trade.getContract(), makersContractSignatureData)); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private void commitToModel(ContractSignatureData makersContractSignatureData) { + trade.getMaker().getContractSignatureData().set(makersContractSignatureData); + } +} \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTradeMessage.java b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTradeMessage.java index ff07c6a955..26bd180675 100644 --- a/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTradeMessage.java +++ b/trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTradeMessage.java @@ -44,6 +44,9 @@ public static BisqEasyTradeMessage fromProto(bisq.trade.protobuf.TradeMessage pr case BISQEASYTAKEOFFERREQUEST: { return BisqEasyTakeOfferRequest.fromProto(proto); } + case BISQEASYTAKEOFFERRESPONSE: { + return BisqEasyTakeOfferResponse.fromProto(proto); + } case BISQEASYACCOUNTDATAMESSAGE: { return BisqEasyAccountDataMessage.fromProto(proto); } diff --git a/trade/src/main/java/bisq/trade/multisig/MultisigTradeService.java b/trade/src/main/java/bisq/trade/multisig/MultisigTradeService.java index 27353dec4e..0e7f08b6f6 100644 --- a/trade/src/main/java/bisq/trade/multisig/MultisigTradeService.java +++ b/trade/src/main/java/bisq/trade/multisig/MultisigTradeService.java @@ -18,21 +18,14 @@ package bisq.trade.multisig; import bisq.common.application.Service; -import bisq.contract.ContractService; import bisq.contract.multisig.MultisigContract; import bisq.identity.Identity; -import bisq.identity.IdentityService; import bisq.network.NetworkId; -import bisq.network.NetworkService; import bisq.network.p2p.message.NetworkMessage; import bisq.network.p2p.services.confidential.MessageListener; -import bisq.offer.OfferService; import bisq.offer.multisig.MultisigOffer; import bisq.persistence.Persistence; import bisq.persistence.PersistenceClient; -import bisq.persistence.PersistenceService; -import bisq.support.MediationService; -import bisq.support.SupportService; import bisq.trade.ServiceProvider; import bisq.trade.TradeException; import bisq.trade.multisig.protocol.*; @@ -54,33 +47,14 @@ public class MultisigTradeService implements PersistenceClient persistence; - private final IdentityService identityService; - private final OfferService offerService; - private final ContractService contractService; - private final MediationService mediationService; - private final NetworkService networkService; private final ServiceProvider serviceProvider; // We don't persist the protocol, only the model. private final Map tradeProtocolById = new ConcurrentHashMap<>(); - public MultisigTradeService(NetworkService networkService, - IdentityService identityService, - PersistenceService persistenceService, - OfferService offerService, - ContractService contractService, - SupportService supportService) { - this.networkService = networkService; - this.identityService = identityService; - this.offerService = offerService; - this.contractService = contractService; - this.mediationService = supportService.getMediationService(); - serviceProvider = new ServiceProvider(networkService, - identityService, - offerService, - contractService, - supportService); - persistence = persistenceService.getOrCreatePersistence(this, persistableStore); + public MultisigTradeService(ServiceProvider serviceProvider) { + persistence = serviceProvider.getPersistenceService().getOrCreatePersistence(this, persistableStore); + this.serviceProvider = serviceProvider; } @@ -89,7 +63,7 @@ public MultisigTradeService(NetworkService networkService, /////////////////////////////////////////////////////////////////////////////////////////////////// public CompletableFuture initialize() { - networkService.addMessageListener(this); + serviceProvider.getNetworkService().addMessageListener(this); persistableStore.getTradeById().values().forEach(this::createAndAddTradeProtocol); @@ -97,7 +71,7 @@ public CompletableFuture initialize() { } public CompletableFuture shutdown() { - networkService.removeMessageListener(this); + serviceProvider.getNetworkService().removeMessageListener(this); return CompletableFuture.completedFuture(true); } diff --git a/trade/src/main/java/bisq/trade/protocol/events/SendTradeMessageHandler.java b/trade/src/main/java/bisq/trade/protocol/events/SendTradeMessageHandler.java index 4c0eaa1888..7341bcc8f0 100644 --- a/trade/src/main/java/bisq/trade/protocol/events/SendTradeMessageHandler.java +++ b/trade/src/main/java/bisq/trade/protocol/events/SendTradeMessageHandler.java @@ -26,18 +26,13 @@ import java.util.concurrent.CompletableFuture; @Slf4j -public abstract class SendTradeMessageHandler> extends TradeEventHandler { +public abstract class SendTradeMessageHandler> extends TradeEventHandler implements TradeMessageSender { protected SendTradeMessageHandler(ServiceProvider serviceProvider, M model) { super(serviceProvider, model); } protected CompletableFuture sendMessage(BisqEasyTradeMessage message) { - log.error("send {} to {} (sender={})", message.getClass().getSimpleName(), model.getPeer().getNetworkId(), model.getMyIdentity().getNetworkId()); - return serviceProvider.getNetworkService().confidentialSend(message, model.getPeer().getNetworkId(), model.getMyIdentity().getNodeIdAndKeyPair()) - .whenComplete((result, throwable) -> { - log.error("result={}", result); - //todo store info if message arrive or stored in mailbox - }); + return sendMessage(message, serviceProvider, trade); } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/protocol/events/TradeEventHandler.java b/trade/src/main/java/bisq/trade/protocol/events/TradeEventHandler.java index b83b7e8937..f7df45538b 100644 --- a/trade/src/main/java/bisq/trade/protocol/events/TradeEventHandler.java +++ b/trade/src/main/java/bisq/trade/protocol/events/TradeEventHandler.java @@ -23,10 +23,10 @@ public abstract class TradeEventHandler> implements EventHandler { protected final ServiceProvider serviceProvider; - protected final M model; + protected final M trade; - protected TradeEventHandler(ServiceProvider serviceProvider, M model) { + protected TradeEventHandler(ServiceProvider serviceProvider, M trade) { this.serviceProvider = serviceProvider; - this.model = model; + this.trade = trade; } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/protocol/events/TradeMessageHandler.java b/trade/src/main/java/bisq/trade/protocol/events/TradeMessageHandler.java index 498d757e6a..32a4a7e166 100644 --- a/trade/src/main/java/bisq/trade/protocol/events/TradeMessageHandler.java +++ b/trade/src/main/java/bisq/trade/protocol/events/TradeMessageHandler.java @@ -21,11 +21,16 @@ import bisq.trade.Trade; import bisq.trade.bisq_easy.protocol.messages.BisqEasyTradeMessage; +import static com.google.common.base.Preconditions.checkArgument; + public abstract class TradeMessageHandler, S extends BisqEasyTradeMessage> extends TradeEventHandler { protected TradeMessageHandler(ServiceProvider serviceProvider, M model) { super(serviceProvider, model); } - protected abstract void verifyMessage(S message); + protected void verifyMessage(S message) { + checkArgument(message.getTradeId().equals(trade.getId())); + checkArgument(message.getSender().equals(trade.getPeer().getNetworkId())); + } } \ No newline at end of file diff --git a/trade/src/main/java/bisq/trade/protocol/events/TradeMessageSender.java b/trade/src/main/java/bisq/trade/protocol/events/TradeMessageSender.java new file mode 100644 index 0000000000..251f88fe0c --- /dev/null +++ b/trade/src/main/java/bisq/trade/protocol/events/TradeMessageSender.java @@ -0,0 +1,35 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.trade.protocol.events; + +import bisq.network.NetworkService; +import bisq.trade.ServiceProvider; +import bisq.trade.Trade; +import bisq.trade.bisq_easy.protocol.messages.BisqEasyTradeMessage; + +import java.util.concurrent.CompletableFuture; + +public interface TradeMessageSender> { + default CompletableFuture sendMessage(BisqEasyTradeMessage message, ServiceProvider serviceProvider, M model) { + return serviceProvider.getNetworkService().confidentialSend(message, model.getPeer().getNetworkId(), model.getMyIdentity().getNodeIdAndKeyPair()) + .whenComplete((result, throwable) -> { + System.out.println("sendMessage " + message + ". result=" + result); + //todo store info if message arrive or stored in mailbox + }); + } +} diff --git a/trade/src/main/java/bisq/trade/submarine/SubmarineTradeService.java b/trade/src/main/java/bisq/trade/submarine/SubmarineTradeService.java index 32d04938e5..845d7a589e 100644 --- a/trade/src/main/java/bisq/trade/submarine/SubmarineTradeService.java +++ b/trade/src/main/java/bisq/trade/submarine/SubmarineTradeService.java @@ -18,21 +18,14 @@ package bisq.trade.submarine; import bisq.common.application.Service; -import bisq.contract.ContractService; import bisq.contract.submarine.SubmarineContract; import bisq.identity.Identity; -import bisq.identity.IdentityService; import bisq.network.NetworkId; -import bisq.network.NetworkService; import bisq.network.p2p.message.NetworkMessage; import bisq.network.p2p.services.confidential.MessageListener; -import bisq.offer.OfferService; import bisq.offer.submarine.SubmarineOffer; import bisq.persistence.Persistence; import bisq.persistence.PersistenceClient; -import bisq.persistence.PersistenceService; -import bisq.support.MediationService; -import bisq.support.SupportService; import bisq.trade.ServiceProvider; import bisq.trade.TradeException; import bisq.trade.protocol.Protocol; @@ -54,33 +47,14 @@ public class SubmarineTradeService implements PersistenceClient persistence; - private final IdentityService identityService; - private final OfferService offerService; - private final ContractService contractService; - private final MediationService mediationService; - private final NetworkService networkService; private final ServiceProvider serviceProvider; // We don't persist the protocol, only the model. private final Map tradeProtocolById = new ConcurrentHashMap<>(); - public SubmarineTradeService(NetworkService networkService, - IdentityService identityService, - PersistenceService persistenceService, - OfferService offerService, - ContractService contractService, - SupportService supportService) { - this.networkService = networkService; - this.identityService = identityService; - this.offerService = offerService; - this.contractService = contractService; - this.mediationService = supportService.getMediationService(); - serviceProvider = new ServiceProvider(networkService, - identityService, - offerService, - contractService, - supportService); - persistence = persistenceService.getOrCreatePersistence(this, persistableStore); + public SubmarineTradeService(ServiceProvider serviceProvider) { + this.serviceProvider = serviceProvider; + persistence = serviceProvider.getPersistenceService().getOrCreatePersistence(this, persistableStore); } @@ -89,7 +63,7 @@ public SubmarineTradeService(NetworkService networkService, /////////////////////////////////////////////////////////////////////////////////////////////////// public CompletableFuture initialize() { - networkService.addMessageListener(this); + serviceProvider.getNetworkService().addMessageListener(this); persistableStore.getTradeById().values().forEach(this::createAndAddTradeProtocol); @@ -97,7 +71,7 @@ public CompletableFuture initialize() { } public CompletableFuture shutdown() { - networkService.removeMessageListener(this); + serviceProvider.getNetworkService().removeMessageListener(this); return CompletableFuture.completedFuture(true); } diff --git a/trade/src/main/proto/protocol.proto b/trade/src/main/proto/trade.proto similarity index 92% rename from trade/src/main/proto/protocol.proto rename to trade/src/main/proto/trade.proto index 9891526d70..2e2f459955 100644 --- a/trade/src/main/proto/protocol.proto +++ b/trade/src/main/proto/trade.proto @@ -86,15 +86,19 @@ message BisqEasyTradeStore { message BisqEasyTradeMessage { oneof message { BisqEasyTakeOfferRequest bisqEasyTakeOfferRequest = 20; - BisqEasyAccountDataMessage bisqEasyAccountDataMessage = 21; - BisqEasyConfirmFiatSentMessage bisqEasyConfirmFiatSentMessage = 22; - BisqEasyConfirmBtcSentMessage bisqEasyConfirmBtcSentMessage = 23; + BisqEasyTakeOfferResponse bisqEasyTakeOfferResponse = 21; + BisqEasyAccountDataMessage bisqEasyAccountDataMessage = 22; + BisqEasyConfirmFiatSentMessage bisqEasyConfirmFiatSentMessage = 23; + BisqEasyConfirmBtcSentMessage bisqEasyConfirmBtcSentMessage = 24; } } message BisqEasyTakeOfferRequest { contract.Contract bisqEasyContract = 1; contract.ContractSignatureData contractSignatureData = 2; } +message BisqEasyTakeOfferResponse { + contract.ContractSignatureData contractSignatureData = 1; +} message BisqEasyAccountDataMessage { string paymentAccountData = 1; }