From fcc120444dcce95f763f36e99825cac5694afd67 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 26 Nov 2024 14:47:27 +0700 Subject: [PATCH] Add getOfferbookMarket endpoint --- .../trade_details/TradeDetailsController.java | 4 +- .../trade_state/TradeStateController.java | 5 +- .../list/ChatMessageListItem.java | 3 +- .../bisq/bisq_easy/BisqEasyServiceUtil.java | 34 +-- .../market_price/MarketPriceRestApi.java | 6 +- build.gradle.kts | 4 + chat/build.gradle.kts | 1 + .../bisqeasy/offerbook/OfferbookRestApi.java | 271 ++++++++++++++++-- .../bisq/common/rest_api/RestApiBase.java | 30 ++ .../offer/price/spec/PriceSpecFormatter.java | 49 ++++ .../bisq/rest_api/RestApiResourceConfig.java | 5 +- .../user/identity/UserIdentityRestApi.java | 20 +- 12 files changed, 355 insertions(+), 77 deletions(-) create mode 100644 offer/src/main/java/bisq/offer/price/spec/PriceSpecFormatter.java diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_details/TradeDetailsController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_details/TradeDetailsController.java index b3c834b873..4d3019a6df 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_details/TradeDetailsController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_details/TradeDetailsController.java @@ -18,7 +18,6 @@ package bisq.desktop.main.content.bisq_easy.open_trades.trade_details; import bisq.account.payment_method.BitcoinPaymentRail; -import bisq.bisq_easy.BisqEasyServiceUtil; import bisq.bisq_easy.NavigationTarget; import bisq.chat.bisqeasy.open_trades.BisqEasyOpenTradeChannel; import bisq.contract.bisq_easy.BisqEasyContract; @@ -29,6 +28,7 @@ import bisq.desktop.overlay.OverlayController; import bisq.i18n.Res; import bisq.offer.price.spec.FixPriceSpec; +import bisq.offer.price.spec.PriceSpecFormatter; import bisq.presentation.formatters.DateFormatter; import bisq.presentation.formatters.PriceFormatter; import bisq.trade.bisq_easy.BisqEasyTrade; @@ -96,7 +96,7 @@ public void onActivate() { model.setPriceCodes(trade.getOffer().getMarket().getMarketCodes()); model.setPriceSpec(trade.getOffer().getPriceSpec() instanceof FixPriceSpec ? "" - : String.format("(%s)", BisqEasyServiceUtil.getFormattedPriceSpec(trade.getOffer().getPriceSpec(), true))); + : String.format("(%s)", PriceSpecFormatter.getFormattedPriceSpec(trade.getOffer().getPriceSpec(), true))); model.setPaymentMethod(contract.getQuoteSidePaymentMethodSpec().getShortDisplayString()); model.setSettlementMethod(contract.getBaseSidePaymentMethodSpec().getShortDisplayString()); model.setTradeId(trade.getId()); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_state/TradeStateController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_state/TradeStateController.java index 7f838ba280..64cd410da1 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_state/TradeStateController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/open_trades/trade_state/TradeStateController.java @@ -38,6 +38,7 @@ import bisq.desktop.main.content.bisq_easy.open_trades.trade_state.states.*; import bisq.i18n.Res; import bisq.offer.price.spec.PriceSpec; +import bisq.offer.price.spec.PriceSpecFormatter; import bisq.settings.DontShowAgainService; import bisq.support.mediation.MediationRequestService; import bisq.trade.bisq_easy.BisqEasyTrade; @@ -165,10 +166,10 @@ public void onActivate() { model.getBuyerPriceDescriptionApprovalOverlay().set( Res.get("bisqEasy.tradeState.acceptOrRejectSellersPrice.description.buyersPrice", - BisqEasyServiceUtil.getFormattedPriceSpec(bisqEasyTrade.getOffer().getPriceSpec()))); + PriceSpecFormatter.getFormattedPriceSpec(bisqEasyTrade.getOffer().getPriceSpec()))); model.getSellerPriceDescriptionApprovalOverlay().set( Res.get("bisqEasy.tradeState.acceptOrRejectSellersPrice.description.sellersPrice", - BisqEasyServiceUtil.getFormattedPriceSpec(bisqEasyTrade.getContract().getAgreedPriceSpec()))); + PriceSpecFormatter.getFormattedPriceSpec(bisqEasyTrade.getContract().getAgreedPriceSpec()))); }); } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessageListItem.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessageListItem.java index 96625acb92..3d34243218 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessageListItem.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessageListItem.java @@ -56,6 +56,7 @@ import bisq.offer.payment_method.PaymentMethodSpecFormatter; import bisq.offer.payment_method.PaymentMethodSpecUtil; import bisq.offer.price.spec.PriceSpec; +import bisq.offer.price.spec.PriceSpecFormatter; import bisq.presentation.formatters.DateFormatter; import bisq.trade.Trade; import bisq.trade.bisq_easy.BisqEasyTradeService; @@ -270,7 +271,7 @@ public Optional> getBisqEasyOfferAmountAndPriceSpec() { boolean hasAmountRange = amountSpec instanceof RangeAmountSpec; Market market = offer.getMarket(); String quoteAmountAsString = OfferAmountFormatter.formatQuoteAmount(marketPriceService, amountSpec, priceSpec, market, hasAmountRange, true); - String priceSpecAsString = BisqEasyServiceUtil.getFormattedPriceSpec(priceSpec); + String priceSpecAsString = PriceSpecFormatter.getFormattedPriceSpec(priceSpec); return Optional.of(new Pair<>(quoteAmountAsString, priceSpecAsString)); } return Optional.empty(); diff --git a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyServiceUtil.java b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyServiceUtil.java index e5a6a8fa04..da67bebfe6 100644 --- a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyServiceUtil.java +++ b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyServiceUtil.java @@ -32,11 +32,8 @@ import bisq.offer.amount.spec.RangeAmountSpec; import bisq.offer.bisq_easy.BisqEasyOffer; import bisq.offer.payment_method.PaymentMethodSpecFormatter; -import bisq.offer.price.spec.FixPriceSpec; -import bisq.offer.price.spec.FloatPriceSpec; import bisq.offer.price.spec.PriceSpec; -import bisq.presentation.formatters.PercentageFormatter; -import bisq.presentation.formatters.PriceFormatter; +import bisq.offer.price.spec.PriceSpecFormatter; import bisq.trade.Trade; import bisq.trade.bisq_easy.BisqEasyTrade; import bisq.trade.bisq_easy.BisqEasyTradeService; @@ -74,31 +71,6 @@ public static boolean authorNotBannedOrIgnored(UserProfileService userProfileSer return true; } - public static String getFormattedPriceSpec(PriceSpec priceSpec) { - return getFormattedPriceSpec(priceSpec, false); - } - - public static String getFormattedPriceSpec(PriceSpec priceSpec, boolean abbreviated) { - String priceInfo; - if (priceSpec instanceof FixPriceSpec fixPriceSpec) { - String price = PriceFormatter.formatWithCode(fixPriceSpec.getPriceQuote()); - priceInfo = Res.get("bisqEasy.tradeWizard.review.chatMessage.fixPrice", price); - } else if (priceSpec instanceof FloatPriceSpec floatPriceSpec) { - String percent = PercentageFormatter.formatToPercentWithSymbol(Math.abs(floatPriceSpec.getPercentage())); - priceInfo = Res.get(floatPriceSpec.getPercentage() >= 0 - ? abbreviated - ? "bisqEasy.tradeWizard.review.chatMessage.floatPrice.plus" - : "bisqEasy.tradeWizard.review.chatMessage.floatPrice.above" - : abbreviated - ? "bisqEasy.tradeWizard.review.chatMessage.floatPrice.minus" - : "bisqEasy.tradeWizard.review.chatMessage.floatPrice.below" - , percent); - } else { - priceInfo = Res.get("bisqEasy.tradeWizard.review.chatMessage.marketPrice"); - } - return priceInfo; - } - public static boolean isMaker(UserIdentityService userIdentityService, BisqEasyOffer bisqEasyOffer) { return bisqEasyOffer.isMyOffer(userIdentityService.getMyUserProfileIds()); } @@ -121,7 +93,7 @@ public static String createBasicOfferBookMessage(MarketPriceService marketPriceS String fiatPaymentMethodNames, AmountSpec amountSpec, PriceSpec priceSpec) { - String priceInfo = String.format("%s %s", Res.get("bisqEasy.tradeWizard.review.chatMessage.price"), getFormattedPriceSpec(priceSpec)); + String priceInfo = String.format("%s %s", Res.get("bisqEasy.tradeWizard.review.chatMessage.price"), PriceSpecFormatter.getFormattedPriceSpec(priceSpec)); boolean hasAmountRange = amountSpec instanceof RangeAmountSpec; String quoteAmountAsString = OfferAmountFormatter.formatQuoteAmount(marketPriceService, amountSpec, priceSpec, market, hasAmountRange, true); return Res.get("bisqEasy.tradeWizard.review.chatMessage.offerDetails", quoteAmountAsString, bitcoinPaymentMethodNames, fiatPaymentMethodNames, priceInfo); @@ -156,7 +128,7 @@ public static String createOfferBookMessageFromPeerPerspective(String messageOwn AmountSpec amountSpec, PriceSpec priceSpec) { String ownerNickName = StringUtils.truncate(messageOwnerNickName, 28); - String priceInfo = String.format("%s %s", Res.get("bisqEasy.tradeWizard.review.chatMessage.price"), getFormattedPriceSpec(priceSpec)); + String priceInfo = String.format("%s %s", Res.get("bisqEasy.tradeWizard.review.chatMessage.price"), PriceSpecFormatter.getFormattedPriceSpec(priceSpec)); boolean hasAmountRange = amountSpec instanceof RangeAmountSpec; String quoteAmountAsString = OfferAmountFormatter.formatQuoteAmount(marketPriceService, amountSpec, priceSpec, market, hasAmountRange, true); return buildOfferBookMessage(ownerNickName, direction, quoteAmountAsString, bitcoinPaymentMethodNames, fiatPaymentMethodNames, priceInfo); diff --git a/bonded-roles/src/main/java/bisq/bonded_roles/market_price/MarketPriceRestApi.java b/bonded-roles/src/main/java/bisq/bonded_roles/market_price/MarketPriceRestApi.java index 78aac26866..e431867b72 100644 --- a/bonded-roles/src/main/java/bisq/bonded_roles/market_price/MarketPriceRestApi.java +++ b/bonded-roles/src/main/java/bisq/bonded_roles/market_price/MarketPriceRestApi.java @@ -76,13 +76,13 @@ public Response getQuotes() { )); if (result.isEmpty()) { - return buildResponse(Response.Status.NOT_FOUND, "No market price quotes found."); + return buildNotFoundResponse("No market price quotes found."); } - return buildResponse(Response.Status.OK, new MarketPriceResponse(result)); + return buildOkResponse(new MarketPriceResponse(result)); } catch (Exception ex) { log.error("Failed to retrieve market price quotes", ex); - return buildResponse(Response.Status.INTERNAL_SERVER_ERROR, "An error occurred while retrieving market prices."); + return buildErrorResponse("An error occurred while retrieving market prices."); } } diff --git a/build.gradle.kts b/build.gradle.kts index f9568ae7ec..7b8097f97b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,10 @@ tasks.register("buildAll") { ":apps:desktop:desktop-app:build", ":apps:desktop:desktop-app:installDist", ":apps:desktop:desktop-app-launcher:generateInstallers", + ":apps:desktop:webcam-app:build", + ":apps:oracle-node-app:build", + ":apps:rest-api-app:build", + ":apps:node-monitor-web-app:build", // ":REPLACEME:build", ).forEach { exec { diff --git a/chat/build.gradle.kts b/chat/build.gradle.kts index 18e7cd6db5..7aaf2ccb11 100644 --- a/chat/build.gradle.kts +++ b/chat/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(project(":persistence")) implementation(project(":security")) implementation(project(":identity")) + implementation(project(":account")) implementation(project(":user")) implementation(project(":offer")) implementation(project(":settings")) diff --git a/chat/src/main/java/bisq/chat/bisqeasy/offerbook/OfferbookRestApi.java b/chat/src/main/java/bisq/chat/bisqeasy/offerbook/OfferbookRestApi.java index 34fe710399..b37995c4f4 100644 --- a/chat/src/main/java/bisq/chat/bisqeasy/offerbook/OfferbookRestApi.java +++ b/chat/src/main/java/bisq/chat/bisqeasy/offerbook/OfferbookRestApi.java @@ -17,22 +17,46 @@ package bisq.chat.bisqeasy.offerbook; +import bisq.account.payment_method.PaymentMethod; +import bisq.bonded_roles.market_price.MarketPriceService; import bisq.common.currency.Market; +import bisq.common.currency.MarketRepository; +import bisq.common.rest_api.RestApiBase; +import bisq.common.util.StringUtils; +import bisq.i18n.Res; +import bisq.offer.Direction; +import bisq.offer.amount.OfferAmountFormatter; +import bisq.offer.amount.spec.AmountSpec; +import bisq.offer.amount.spec.RangeAmountSpec; +import bisq.offer.bisq_easy.BisqEasyOffer; +import bisq.offer.payment_method.PaymentMethodSpecUtil; +import bisq.offer.price.spec.PriceSpec; +import bisq.offer.price.spec.PriceSpecFormatter; +import bisq.presentation.formatters.DateFormatter; +import bisq.user.UserService; +import bisq.user.identity.UserIdentityService; +import bisq.user.profile.UserProfile; +import bisq.user.profile.UserProfileService; +import bisq.user.reputation.ReputationService; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import lombok.Getter; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import java.text.DateFormat; +import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Slf4j @@ -40,12 +64,22 @@ @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = "Bisq Easy Offerbook API") -public class OfferbookRestApi { +public class OfferbookRestApi extends RestApiBase { private final BisqEasyOfferbookChannelService bisqEasyOfferbookChannelService; + private final MarketPriceService marketPriceService; + private final UserProfileService userProfileService; + private final ReputationService reputationService; + private final UserIdentityService userIdentityService; - public OfferbookRestApi(BisqEasyOfferbookChannelService bisqEasyOfferbookChannelService) { + public OfferbookRestApi(BisqEasyOfferbookChannelService bisqEasyOfferbookChannelService, + MarketPriceService marketPriceService, + UserService userService) { this.bisqEasyOfferbookChannelService = bisqEasyOfferbookChannelService; + this.marketPriceService = marketPriceService; + userProfileService = userService.getUserProfileService(); + userIdentityService = userService.getUserIdentityService(); + reputationService = userService.getReputationService(); } /** @@ -68,6 +102,8 @@ public Response getMarkets() { try { List markets = bisqEasyOfferbookChannelService.getChannels().stream() .map(BisqEasyOfferbookChannel::getMarket) + .filter(market -> marketPriceService.getMarketPriceByCurrencyMap().isEmpty() || + marketPriceService.getMarketPriceByCurrencyMap().containsKey(market)) .collect(Collectors.toList()); return buildOkResponse(markets); } catch (Exception e) { @@ -108,27 +144,210 @@ public Response getNumOffersByMarketCode() { } } - /** - * Builds a successful 200 OK response. - * - * @param entity The response entity. - * @return The HTTP response. - */ - private Response buildOkResponse(Object entity) { - return Response.status(Response.Status.OK) - .entity(entity) - .build(); + @Operation( + summary = "Retrieve Offers for a Market", + description = "Fetches a list of offers for the specified currency code. " + + "The market is determined using the 'BTC/{currencyCode}' format.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Offers retrieved successfully.", + content = @Content(schema = @Schema(implementation = OfferListItemDto.class)) + ), + @ApiResponse( + responseCode = "404", + description = "No offers found for the specified currency code." + ), + @ApiResponse( + responseCode = "500", + description = "Internal server error occurred while processing the request." + ) + } + ) + @GET + @Path("markets/{currencyCode}/offers") + public Response getOffers(@PathParam("currencyCode") String currencyCode) { + try { + String marketCodes = "BTC/" + currencyCode.toUpperCase(); + return findOffer(marketCodes) + .map(this::buildOkResponse) + .orElseGet(() -> { + log.warn("No offers found for market: {}", marketCodes); + return buildNotFoundResponse("No offers found for the specified market."); + }); + + } catch (Exception e) { + log.error("Error while fetching offers for currency code: {}", currencyCode, e); + return buildErrorResponse("An unexpected error occurred while processing the request."); + } } - /** - * Builds an error response with a 500 status. - * - * @param message The error message. - * @return The HTTP response. - */ - private Response buildErrorResponse(String message) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", message)) - .build(); + private Optional> findOffer(String marketCodes) { + return MarketRepository.findAnyFiatMarketByMarketCodes(marketCodes) + .flatMap(market -> bisqEasyOfferbookChannelService.findChannel(market) + .map(channel -> channel.getChatMessages() + .stream() + .filter(BisqEasyOfferbookMessage::hasBisqEasyOffer) + .map(message -> { + BisqEasyOffer bisqEasyOffer = message.getBisqEasyOffer().orElseThrow(); + long date = message.getDate(); + String formattedDate = DateFormatter.formatDateTime(new Date(date), DateFormat.MEDIUM, DateFormat.SHORT, + true, " " + Res.get("temporal.at") + " "); + String authorUserProfileId = message.getAuthorUserProfileId(); + Optional senderUserProfile = userProfileService.findUserProfile(authorUserProfileId); + String nym = senderUserProfile.map(UserProfile::getNym).orElse(""); + String userName = senderUserProfile.map(UserProfile::getUserName).orElse(""); + + ReputationScoreDto reputationScore = senderUserProfile.flatMap(reputationService::findReputationScore) + .map(score -> new ReputationScoreDto( + score.getTotalScore(), + score.getFiveSystemScore(), + score.getRanking() + )) + .orElse(new ReputationScoreDto(0, 0, 0)); + AmountSpec amountSpec = bisqEasyOffer.getAmountSpec(); + PriceSpec priceSpec = bisqEasyOffer.getPriceSpec(); + boolean hasAmountRange = amountSpec instanceof RangeAmountSpec; + // Market market= bisqEasyOffer.getMarket(); + String formattedQuoteAmount = OfferAmountFormatter.formatQuoteAmount( + marketPriceService, + amountSpec, + priceSpec, + market, + hasAmountRange, + true + ); + String formattedPrice = PriceSpecFormatter.getFormattedPriceSpec(priceSpec, true); + + List quoteSidePaymentMethods = PaymentMethodSpecUtil.getPaymentMethods(bisqEasyOffer.getQuoteSidePaymentMethodSpecs()) + .stream() + .map(PaymentMethod::getName) + .collect(Collectors.toList()); + + List baseSidePaymentMethods = PaymentMethodSpecUtil.getPaymentMethods(bisqEasyOffer.getBaseSidePaymentMethodSpecs()) + .stream() + .map(PaymentMethod::getName) + .collect(Collectors.toList()); + + String supportedLanguageCodes = Joiner.on(",").join(bisqEasyOffer.getSupportedLanguageCodes()); + boolean isMyMessage = message.isMyMessage(userIdentityService); + Direction direction = bisqEasyOffer.getDirection(); + String offerTitle = getOfferTitle(message, direction, isMyMessage); + String messageId = message.getId(); + String offerId = bisqEasyOffer.getId(); + return new OfferListItemDto(messageId, + offerId, + isMyMessage, + direction, + offerTitle, + date, + formattedDate, + nym, + userName, + reputationScore, + formattedQuoteAmount, + formattedPrice, + quoteSidePaymentMethods, + baseSidePaymentMethods, + supportedLanguageCodes); + }) + .collect(Collectors.toList()) + ) + ); } + + private String getOfferTitle(BisqEasyOfferbookMessage message, Direction direction, boolean isMyMessage) { + if (isMyMessage) { + String directionString = StringUtils.capitalize(Res.get("offer." + direction.name().toLowerCase())); + return Res.get("bisqEasy.tradeWizard.review.chatMessage.myMessageTitle", directionString); + } else { + return message.getText(); + } + } + + @Getter + @ToString + @Schema(name = "OfferListItem", description = "Detailed information about an offer in the offerbook.") + public static class OfferListItemDto { + @Schema(description = "Unique identifier for the message.", example = "msg-123456") + private final String messageId; + @Schema(description = "Unique identifier for the offer.", example = "offer-987654") + private final String offerId; + @JsonProperty("isMyMessage") + @Schema(description = "Indicates whether this message belongs to the current user.", example = "true") + private final boolean isMyMessage; + @Schema(description = "Direction of the offer (buy or sell).", implementation = Direction.class) + private final Direction direction; + @Schema(description = "Title of the offer.", example = "Buy 1 BTC at $30,000") + private final String offerTitle; + @Schema(description = "Timestamp of the offer in milliseconds since epoch.", example = "1672531200000") + private final long date; + @Schema(description = "Formatted date string for the offer.", example = "2023-01-01 12:00:00") + private final String formattedDate; + @Schema(description = "Anonymous pseudonym of the user.", example = "Nym123") + private final String nym; + @Schema(description = "Username of the offer's creator.", example = "Alice") + private final String userName; + @Schema(description = "Reputation score of the user who created the offer.", implementation = ReputationScoreDto.class) + private final ReputationScoreDto reputationScore; + @Schema(description = "Formatted amount for the quoted currency.", example = "30,000 USD") + private final String formattedQuoteAmount; + @Schema(description = "Formatted price of the offer.", example = "$30,000 per BTC") + private final String formattedPrice; + @Schema(description = "List of payment methods supported by the quote side.", example = "[\"Bank Transfer\", \"PayPal\"]") + private final List quoteSidePaymentMethods; + @Schema(description = "List of payment methods supported by the base side.", example = "[\"Cash Deposit\"]") + private final List baseSidePaymentMethods; + @Schema(description = "Supported language codes for the offer.", example = "en,es,fr") + private final String supportedLanguageCodes; + + public OfferListItemDto(String messageId, + String offerId, + boolean isMyMessage, + Direction direction, + String offerTitle, + long date, + String formattedDate, + String nym, + String userName, + ReputationScoreDto reputationScore, + String formattedQuoteAmount, + String formattedPrice, + List quoteSidePaymentMethods, + List baseSidePaymentMethods, + String supportedLanguageCodes) { + this.messageId = messageId; + this.offerId = offerId; + this.isMyMessage = isMyMessage; + this.direction = direction; + this.offerTitle = offerTitle; + this.date = date; + this.formattedDate = formattedDate; + this.nym = nym; + this.userName = userName; + this.reputationScore = reputationScore; + this.formattedQuoteAmount = formattedQuoteAmount; + this.formattedPrice = formattedPrice; + this.quoteSidePaymentMethods = quoteSidePaymentMethods; + this.baseSidePaymentMethods = baseSidePaymentMethods; + this.supportedLanguageCodes = supportedLanguageCodes; + } + } + + @Getter + @Schema(name = "ReputationScoreDto", description = "User reputation details including total score, 5-star rating, and ranking.") + public static class ReputationScoreDto { + @Schema(description = "Total reputation score of the user.", example = "1500") + private final long totalScore; + @Schema(description = "5-star system equivalent score (out of 5).", example = "4.8") + private final double fiveSystemScore; + @Schema(description = "User's ranking among peers.", example = "12") + private final int ranking; + public ReputationScoreDto(long totalScore, double fiveSystemScore, int ranking) { + this.totalScore = totalScore; + this.fiveSystemScore = fiveSystemScore; + this.ranking = ranking; + } + } + } diff --git a/common/src/main/java/bisq/common/rest_api/RestApiBase.java b/common/src/main/java/bisq/common/rest_api/RestApiBase.java index 13af01861a..0831a3df88 100644 --- a/common/src/main/java/bisq/common/rest_api/RestApiBase.java +++ b/common/src/main/java/bisq/common/rest_api/RestApiBase.java @@ -19,8 +19,38 @@ import jakarta.ws.rs.core.Response; +import java.util.Map; + public abstract class RestApiBase { protected Response buildResponse(Response.Status status, Object entity) { return Response.status(status).entity(entity).build(); } + + /** + * Builds a successful 200 OK response. + * + * @param entity The response entity. + * @return The HTTP response. + */ + protected Response buildOkResponse(Object entity) { + return Response.status(Response.Status.OK).entity(entity).build(); + } + + protected Response buildNotFoundResponse(String message) { + return Response.status(Response.Status.NOT_FOUND) + .entity(message) + .build(); + } + + /** + * Builds an error response with a 500 status. + * + * @param errorMessage The error message. + * @return The HTTP response. + */ + protected Response buildErrorResponse(String errorMessage) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", errorMessage)) + .build(); + } } diff --git a/offer/src/main/java/bisq/offer/price/spec/PriceSpecFormatter.java b/offer/src/main/java/bisq/offer/price/spec/PriceSpecFormatter.java new file mode 100644 index 0000000000..2350e1035f --- /dev/null +++ b/offer/src/main/java/bisq/offer/price/spec/PriceSpecFormatter.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.offer.price.spec; + +import bisq.i18n.Res; +import bisq.presentation.formatters.PercentageFormatter; +import bisq.presentation.formatters.PriceFormatter; + +public class PriceSpecFormatter { + public static String getFormattedPriceSpec(PriceSpec priceSpec) { + return getFormattedPriceSpec(priceSpec, false); + } + + public static String getFormattedPriceSpec(PriceSpec priceSpec, boolean abbreviated) { + String priceInfo; + if (priceSpec instanceof FixPriceSpec fixPriceSpec) { + String price = PriceFormatter.formatWithCode(fixPriceSpec.getPriceQuote()); + priceInfo = Res.get("bisqEasy.tradeWizard.review.chatMessage.fixPrice", price); + } else if (priceSpec instanceof FloatPriceSpec floatPriceSpec) { + String percent = PercentageFormatter.formatToPercentWithSymbol(Math.abs(floatPriceSpec.getPercentage())); + priceInfo = Res.get(floatPriceSpec.getPercentage() >= 0 + ? abbreviated + ? "bisqEasy.tradeWizard.review.chatMessage.floatPrice.plus" + : "bisqEasy.tradeWizard.review.chatMessage.floatPrice.above" + : abbreviated + ? "bisqEasy.tradeWizard.review.chatMessage.floatPrice.minus" + : "bisqEasy.tradeWizard.review.chatMessage.floatPrice.below" + , percent); + } else { + priceInfo = Res.get("bisqEasy.tradeWizard.review.chatMessage.marketPrice"); + } + return priceInfo; + } +} diff --git a/rest-api/src/main/java/bisq/rest_api/RestApiResourceConfig.java b/rest-api/src/main/java/bisq/rest_api/RestApiResourceConfig.java index 4e6ae91591..818ec81d67 100644 --- a/rest-api/src/main/java/bisq/rest_api/RestApiResourceConfig.java +++ b/rest-api/src/main/java/bisq/rest_api/RestApiResourceConfig.java @@ -33,7 +33,10 @@ public RestApiResourceConfig(RestApiService.Config config, protected void configure() { bind(new UserIdentityRestApi(userService.getUserIdentityService())).to(UserIdentityRestApi.class); bind(new MarketPriceRestApi(bondedRolesService.getMarketPriceService())).to(MarketPriceRestApi.class); - bind(new OfferbookRestApi(chatService.getBisqEasyOfferbookChannelService())).to(OfferbookRestApi.class); + bind(new OfferbookRestApi(chatService.getBisqEasyOfferbookChannelService(), + bondedRolesService.getMarketPriceService(), + userService)) + .to(OfferbookRestApi.class); } }); } diff --git a/user/src/main/java/bisq/user/identity/UserIdentityRestApi.java b/user/src/main/java/bisq/user/identity/UserIdentityRestApi.java index 6f9dbfaab6..460d9b9e19 100644 --- a/user/src/main/java/bisq/user/identity/UserIdentityRestApi.java +++ b/user/src/main/java/bisq/user/identity/UserIdentityRestApi.java @@ -18,7 +18,6 @@ package bisq.user.identity; import bisq.common.rest_api.RestApiBase; -import bisq.common.rest_api.error.RestApiException; import bisq.security.DigestUtil; import bisq.user.profile.UserProfile; import io.swagger.v3.oas.annotations.Operation; @@ -71,7 +70,7 @@ public Response createPreparedData() { return buildResponse(Response.Status.CREATED, preparedData); } catch (Exception e) { log.error("Error generating prepared data", e); - throw new RestApiException(Response.Status.INTERNAL_SERVER_ERROR, "Could not generate prepared data."); + return buildErrorResponse("Could not generate prepared data."); } } @@ -90,10 +89,9 @@ public Response createPreparedData() { public Response getUserIdentity(@PathParam("id") String id) { Optional userIdentity = userIdentityService.findUserIdentity(id); if (userIdentity.isEmpty()) { - throw new RestApiException(Response.Status.NOT_FOUND, - "Could not find user identity for ID: " + id); + return buildNotFoundResponse("Could not find user identity for ID: " + id); } - return buildResponse(Response.Status.OK, userIdentity.get()); + return buildOkResponse(userIdentity.get()); } @GET @@ -112,7 +110,7 @@ public Response getUserIdentityIds() { .stream() .map(UserIdentity::getId) .collect(Collectors.toList()); - return buildResponse(Response.Status.OK, ids); + return buildOkResponse(ids); } @GET @@ -130,10 +128,10 @@ public Response getUserIdentityIds() { public Response getSelectedUserProfile() { UserIdentity selectedUserIdentity = userIdentityService.getSelectedUserIdentity(); if (selectedUserIdentity == null) { - throw new RestApiException(Response.Status.NOT_FOUND, "No selected user identity found."); + return buildNotFoundResponse("No selected user identity found."); } UserProfile userProfile = selectedUserIdentity.getUserProfile(); - return buildResponse(Response.Status.OK, userProfile); + return buildOkResponse(userProfile); } @POST @@ -167,12 +165,12 @@ public Response createUserIdentityAndPublishUserProfile(CreateUserIdentityReques return buildResponse(Response.Status.CREATED, new UserProfileResponse(userIdentity.getId())); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new RestApiException(Response.Status.INTERNAL_SERVER_ERROR, "Thread was interrupted."); + return buildErrorResponse("Thread was interrupted."); } catch (IllegalArgumentException e) { - throw new RestApiException(Response.Status.BAD_REQUEST, "Invalid input: " + e.getMessage()); + return buildResponse(Response.Status.BAD_REQUEST, "Invalid input: " + e.getMessage()); } catch (Exception e) { log.error("Error creating user identity", e); - throw new RestApiException(Response.Status.INTERNAL_SERVER_ERROR, "An unexpected error occurred."); + return buildErrorResponse("An unexpected error occurred."); } }