From 2b028242bf7b3b4e2fde76177eb4fb479463d054 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Dec 2024 18:19:25 -0500 Subject: [PATCH] support no deposit offers with passphrase --- .../haveno/apitest/method/MethodTest.java | 6 +- .../apitest/method/offer/CancelOfferTest.java | 2 +- .../offer/CreateOfferUsingFixedPriceTest.java | 6 +- ...CreateOfferUsingMarketPriceMarginTest.java | 10 +- .../method/offer/CreateXMROffersTest.java | 8 +- .../method/offer/ValidateCreateOfferTest.java | 6 +- .../method/trade/TakeBuyBTCOfferTest.java | 2 +- ...keBuyBTCOfferWithNationalBankAcctTest.java | 2 +- .../method/trade/TakeBuyXMROfferTest.java | 2 +- .../method/trade/TakeSellBTCOfferTest.java | 2 +- .../method/trade/TakeSellXMROfferTest.java | 2 +- .../LongRunningOfferDeactivationTest.java | 4 +- .../apitest/scenario/bot/RandomOffer.java | 6 +- .../cli/request/OffersServiceRequest.java | 2 +- .../witness/AccountAgeWitnessService.java | 8 +- .../main/java/haveno/core/api/CoreApi.java | 22 +- .../haveno/core/api/CoreOffersService.java | 22 +- .../haveno/core/api/CoreTradesService.java | 2 +- .../java/haveno/core/api/model/OfferInfo.java | 19 +- .../java/haveno/core/api/model/TradeInfo.java | 16 +- .../api/model/builder/OfferInfoBuilder.java | 18 + .../haveno/core/offer/CreateOfferService.java | 112 +- .../main/java/haveno/core/offer/Offer.java | 18 + .../haveno/core/offer/OfferFilterService.java | 2 +- .../java/haveno/core/offer/OfferPayload.java | 25 +- .../java/haveno/core/offer/OfferUtil.java | 19 +- .../java/haveno/core/offer/OpenOffer.java | 14 +- .../haveno/core/offer/OpenOfferManager.java | 109 +- .../tasks/SendOfferAvailabilityRequest.java | 3 +- .../offer/placeoffer/tasks/ValidateOffer.java | 22 +- .../core/offer/takeoffer/TakeOfferModel.java | 3 +- .../core/payment/PaymentAccountUtil.java | 2 +- .../java/haveno/core/payment/TradeLimits.java | 11 + .../validation/SecurityDepositValidator.java | 4 +- .../haveno/core/trade/ArbitratorTrade.java | 10 +- .../haveno/core/trade/BuyerAsMakerTrade.java | 11 +- .../haveno/core/trade/BuyerAsTakerTrade.java | 9 +- .../java/haveno/core/trade/BuyerTrade.java | 6 +- .../main/java/haveno/core/trade/Contract.java | 57 +- .../java/haveno/core/trade/HavenoUtils.java | 42 +- .../haveno/core/trade/SellerAsMakerTrade.java | 9 +- .../haveno/core/trade/SellerAsTakerTrade.java | 9 +- .../java/haveno/core/trade/SellerTrade.java | 6 +- .../main/java/haveno/core/trade/Trade.java | 60 +- .../java/haveno/core/trade/TradeManager.java | 27 +- .../core/trade/messages/DepositRequest.java | 17 +- .../core/trade/messages/InitTradeRequest.java | 11 +- .../trade/messages/SignContractRequest.java | 11 +- .../haveno/core/trade/protocol/TradePeer.java | 1 - .../ArbitratorProcessDepositRequest.java | 87 +- .../tasks/ArbitratorProcessReserveTx.java | 62 +- ...tratorSendInitTradeOrMultisigRequests.java | 3 +- .../tasks/BuyerPreparePaymentSentMessage.java | 2 +- ...MakerSendInitTradeRequestToArbitrator.java | 3 +- .../tasks/MaybeSendSignContractRequest.java | 81 +- .../tasks/ProcessSignContractRequest.java | 20 +- .../protocol/tasks/SendDepositRequest.java | 4 +- .../tasks/TakerReserveTradeFunds.java | 97 +- ...TakerSendInitTradeRequestToArbitrator.java | 7 +- .../TakerSendInitTradeRequestToMaker.java | 8 +- .../java/haveno/core/user/Preferences.java | 27 +- .../haveno/core/user/PreferencesPayload.java | 17 +- .../java/haveno/core/util/coin/CoinUtil.java | 34 +- .../haveno/core/xmr/wallet/Restrictions.java | 37 +- core/src/main/resources/bip39_english.txt | 2048 +++++++++++++++++ .../resources/i18n/displayStrings.properties | 15 +- .../haveno/daemon/grpc/GrpcOffersService.java | 4 +- .../haveno/daemon/grpc/GrpcTradesService.java | 1 + .../paymentmethods/PaymentMethodForm.java | 6 +- .../src/main/java/haveno/desktop/images.css | 4 + .../main/offer/MutableOfferDataModel.java | 81 +- .../desktop/main/offer/MutableOfferView.java | 166 +- .../main/offer/MutableOfferViewModel.java | 128 +- .../desktop/main/offer/OfferDataModel.java | 4 + .../main/offer/offerbook/OfferBookView.java | 48 +- .../offer/offerbook/OfferBookViewModel.java | 10 + .../offer/takeoffer/TakeOfferDataModel.java | 2 +- .../main/offer/takeoffer/TakeOfferView.java | 6 +- .../main/overlays/editor/PasswordPopup.java | 245 ++ .../overlays/windows/OfferDetailsWindow.java | 80 +- .../DuplicateOfferDataModel.java | 17 +- .../editoffer/EditOfferDataModel.java | 14 +- .../editoffer/EditOfferViewModel.java | 2 +- .../openoffer/OpenOffersViewModel.java | 2 +- .../pendingtrades/PendingTradesDataModel.java | 24 +- .../pendingtrades/steps/TradeStepView.java | 56 +- .../main/java/haveno/desktop/theme-dark.css | 9 + .../main/java/haveno/desktop/theme-light.css | 5 + .../haveno/desktop/util/DisplayUtils.java | 8 +- desktop/src/main/resources/images/lock.png | Bin 22292 -> 0 bytes desktop/src/main/resources/images/lock@2x.png | Bin 0 -> 721 bytes .../createoffer/CreateOfferDataModelTest.java | 2 +- .../createoffer/CreateOfferViewModelTest.java | 3 +- proto/src/main/proto/grpc.proto | 8 +- proto/src/main/proto/pb.proto | 10 +- 95 files changed, 3528 insertions(+), 766 deletions(-) create mode 100644 core/src/main/resources/bip39_english.txt create mode 100644 desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java delete mode 100644 desktop/src/main/resources/images/lock.png create mode 100644 desktop/src/main/resources/images/lock@2x.png diff --git a/apitest/src/test/java/haveno/apitest/method/MethodTest.java b/apitest/src/test/java/haveno/apitest/method/MethodTest.java index 739c7c03c7b..01c7a3bfd36 100644 --- a/apitest/src/test/java/haveno/apitest/method/MethodTest.java +++ b/apitest/src/test/java/haveno/apitest/method/MethodTest.java @@ -43,7 +43,7 @@ import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; -import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; @@ -157,8 +157,8 @@ protected final haveno.core.payment.PaymentAccount createPaymentAccount(GrpcClie return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); } - public static final Supplier defaultBuyerSecurityDepositPct = () -> { - var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent()); + public static final Supplier defaultSecurityDepositPct = () -> { + var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent()); if (defaultPct.precision() != 2) throw new IllegalStateException(format( "Unexpected decimal precision, expected 2 but actual is %d%n." diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java index a7776dd6837..18676055072 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java @@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest { 10000000L, 10000000L, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAccountId, NO_TRIGGER_PRICE); }; diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 38d83f696d5..49c01d5ae4c 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -49,7 +49,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { 10_000_000L, 10_000_000L, "36000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId()); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -97,7 +97,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { 10_000_000L, 10_000_000L, "30000.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId()); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -145,7 +145,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { 10_000_000L, 5_000_000L, "29500.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), eurAccount.getId()); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index cc6f53acdc4..f4dff640c19 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -66,7 +66,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); @@ -114,7 +114,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), nzdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); @@ -162,7 +162,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), gbpAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); @@ -210,7 +210,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), brlAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer)); @@ -259,7 +259,7 @@ public void testCreateUSDBTCBuyOfferWithTriggerPrice() { 10_000_000L, 5_000_000L, 0.0, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), triggerPrice); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java index 4b7032c86f8..8571c0c59d9 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java @@ -62,7 +62,7 @@ public void testCreateFixedPriceBuy1BTCFor200KXMROffer() { 100_000_000L, 75_000_000L, "0.005", // FIXED PRICE IN BTC FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -108,7 +108,7 @@ public void testCreateFixedPriceSell1BTCFor200KXMROffer() { 100_000_000L, 50_000_000L, "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -156,7 +156,7 @@ public void testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice() { 100_000_000L, 75_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), triggerPrice); log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); @@ -211,7 +211,7 @@ public void testCreatePriceMarginBasedSell1BTCOffer() { 100_000_000L, 50_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java index f299801c5a9..e8745c1b2ca 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java @@ -47,7 +47,7 @@ public void testAmtTooLargeShouldThrowException() { 100000000000L, // exceeds amount limit 100000000000L, "10000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId())); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); } @@ -63,7 +63,7 @@ public void testNoMatchingEURPaymentAccountShouldThrowException() { 10000000L, 10000000L, "40000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), chfAccount.getId())); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); assertEquals(expectedError, exception.getMessage()); @@ -80,7 +80,7 @@ public void testNoMatchingCADPaymentAccountShouldThrowException() { 10000000L, 10000000L, "63000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId())); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); assertEquals(expectedError, exception.getMessage()); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java index fee6e798ba9..abbec38ff6d 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -52,7 +52,7 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java index 10be976c8be..3199a6b0534 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -96,7 +96,7 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { 1_000_000L, 1_000_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesPaymentAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java index 40289e1d50b..f61203400bf 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java @@ -65,7 +65,7 @@ public void testTakeAlicesSellBTCForXMROffer(final TestInfo testInfo) { 15_000_000L, 7_500_000L, "0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 5000); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java index a4257597178..4f43c21cd71 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java @@ -58,7 +58,7 @@ public void testTakeAlicesSellOffer(final TestInfo testInfo) { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java index 68daa640508..9a769b5f044 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java @@ -71,7 +71,7 @@ public void testTakeAlicesBuyBTCForXMROffer(final TestInfo testInfo) { 20_000_000L, 10_500_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); diff --git a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java index 13b72ff79f9..356b77ef8a5 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -57,7 +57,7 @@ public void testSellOfferAutoDisable(final TestInfo testInfo) { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("SELL offer {} created with margin based price {}.", @@ -103,7 +103,7 @@ public void testBuyOfferAutoDisable(final TestInfo testInfo) { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("BUY offer {} created with margin based price {}.", diff --git a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java index eebaec8b049..4e8b86afacf 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java +++ b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java @@ -28,7 +28,7 @@ import java.util.Objects; import java.util.function.Supplier; -import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct; +import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct; import static haveno.cli.CurrencyFormat.formatInternalFiatPrice; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.common.util.MathUtils.scaleDownByPowerOf10; @@ -119,7 +119,7 @@ public RandomOffer create() throws InvalidRandomOfferException { amount, minAmount, priceMargin, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), "0" /*no trigger price*/); } else { this.offer = botClient.createOfferAtFixedPrice(paymentAccount, @@ -128,7 +128,7 @@ public RandomOffer create() throws InvalidRandomOfferException { amount, minAmount, fixedOfferPrice, - defaultBuyerSecurityDepositPct.get()); + defaultSecurityDepositPct.get()); } this.id = offer.getId(); return this; diff --git a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java index eaa0cac150f..2fcb3426d18 100644 --- a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java @@ -81,7 +81,7 @@ public OfferInfo createOffer(String direction, .setUseMarketBasedPrice(useMarketBasedPrice) .setPrice(fixedPrice) .setMarketPriceMarginPct(marketPriceMarginPct) - .setBuyerSecurityDepositPct(securityDepositPct) + .setSecurityDepositPct(securityDepositPct) .setPaymentAccountId(paymentAcctId) .setTriggerPrice(triggerPrice) .build(); diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java index 4d91d75effb..7af2172bc48 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java @@ -40,6 +40,7 @@ import haveno.core.offer.OfferRestrictions; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.PaymentAccount; +import haveno.core.payment.TradeLimits; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; @@ -498,10 +499,15 @@ public long getMyAccountAge(PaymentAccountPayload paymentAccountPayload) { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } - public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) { + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { if (paymentAccount == null) return 0; + if (buyerAsTakerWithoutDeposit) { + TradeLimits tradeLimits = new TradeLimits(); + return tradeLimits.getMaxTradeLimitWithoutBuyerAsTakerDeposit().longValueExact(); + } + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode); if (hasTradeLimitException(accountAgeWitness)) { diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 14ec0f6d088..3fb56220c24 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -419,10 +419,12 @@ public void postOffer(String currencyCode, double marketPriceMargin, long amountAsLong, long minAmountAsLong, - double buyerSecurityDeposit, + double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, @@ -432,10 +434,12 @@ public void postOffer(String currencyCode, marketPriceMargin, amountAsLong, minAmountAsLong, - buyerSecurityDeposit, + securityDepositPct, triggerPriceAsString, reserveExactAmount, paymentAccountId, + isPrivateOffer, + buyerAsTakerWithoutDeposit, resultHandler, errorMessageHandler); } @@ -448,8 +452,10 @@ public Offer editOffer(String offerId, double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { return coreOffersService.editOffer(offerId, currencyCode, direction, @@ -458,8 +464,10 @@ public Offer editOffer(String offerId, marketPriceMargin, amount, minAmount, - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -535,9 +543,11 @@ public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionExcep public void takeOffer(String offerId, String paymentAccountId, long amountAsLong, + String passphrase, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = coreOffersService.getOffer(offerId); + offer.setPassphrase(passphrase); coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 5bdd2f16053..82328278874 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -172,10 +172,12 @@ void postOffer(String currencyCode, double marketPriceMargin, long amountAsLong, long minAmountAsLong, - double securityDeposit, + double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); @@ -199,8 +201,10 @@ void postOffer(String currencyCode, price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - securityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); @@ -223,8 +227,10 @@ Offer editOffer(String offerId, double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { return createOfferService.createAndGetOffer(offerId, direction, currencyCode.toUpperCase(), @@ -233,8 +239,10 @@ Offer editOffer(String offerId, price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 431ab9a6524..14f10b4f007 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -132,7 +132,7 @@ void takeOffer(Offer offer, // adjust amount for fixed-price offer (based on TakeOfferViewModel) String currencyCode = offer.getCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java index b489aaa8bb8..79361661565 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -78,6 +78,9 @@ public class OfferInfo implements Payload { @Nullable private final String splitOutputTxHash; private final long splitOutputTxFee; + private final boolean isPrivateOffer; + private final String passphraseHash; + private final String passphrase; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); @@ -111,6 +114,9 @@ public OfferInfo(OfferInfoBuilder builder) { this.arbitratorSigner = builder.getArbitratorSigner(); this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxFee = builder.getSplitOutputTxFee(); + this.isPrivateOffer = builder.isPrivateOffer(); + this.passphraseHash = builder.getPassphraseHash(); + this.passphrase = builder.getPassphrase(); } public static OfferInfo toOfferInfo(Offer offer) { @@ -137,6 +143,7 @@ public static OfferInfo toMyOfferInfo(OpenOffer openOffer) { .withIsActivated(isActivated) .withSplitOutputTxHash(openOffer.getSplitOutputTxHash()) .withSplitOutputTxFee(openOffer.getSplitOutputTxFee()) + .withPassphrase(openOffer.getPassphrase()) .build(); } @@ -177,7 +184,9 @@ private static OfferInfoBuilder getBuilder(Offer offer) { .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withVersionNumber(offer.getOfferPayload().getVersionNr()) .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) - .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()); + .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) + .withIsPrivateOffer(offer.isPrivateOffer()) + .withPassphraseHash(offer.getPassphraseHash()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -215,9 +224,12 @@ public haveno.proto.grpc.OfferInfo toProtoMessage() { .setPubKeyRing(pubKeyRing) .setVersionNr(versionNumber) .setProtocolVersion(protocolVersion) - .setSplitOutputTxFee(splitOutputTxFee); + .setSplitOutputTxFee(splitOutputTxFee) + .setIsPrivateOffer(isPrivateOffer); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); + Optional.ofNullable(passphraseHash).ifPresent(builder::setPassphraseHash); + Optional.ofNullable(passphrase).ifPresent(builder::setPassphrase); return builder.build(); } @@ -255,6 +267,9 @@ public static OfferInfo fromProto(haveno.proto.grpc.OfferInfo proto) { .withArbitratorSigner(proto.getArbitratorSigner()) .withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxFee(proto.getSplitOutputTxFee()) + .withIsPrivateOffer(proto.getIsPrivateOffer()) + .withPassphraseHash(proto.getPassphraseHash()) + .withPassphrase(proto.getPassphrase()) .build(); } } diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index fa94fd27f27..8df26368baf 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -172,14 +172,14 @@ public static TradeInfo toTradeInfo(Trade trade) { .withAmount(trade.getAmount().longValueExact()) .withMakerFee(trade.getMakerFee().longValueExact()) .withTakerFee(trade.getTakerFee().longValueExact()) - .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact()) - .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact()) - .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact()) - .withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact()) - .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact()) - .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact()) - .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact()) - .withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact()) + .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact()) + .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact()) + .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact()) + .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact()) + .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact()) + .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact()) + .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact()) + .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact()) .withTotalTxFee(trade.getTotalTxFee().longValueExact()) .withPrice(toPreciseTradePrice.apply(trade)) .withVolume(toRoundedVolume.apply(trade)) diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java index 35d532f67ff..0e5cbac99cd 100644 --- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java +++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java @@ -63,6 +63,9 @@ public final class OfferInfoBuilder { private String arbitratorSigner; private String splitOutputTxHash; private long splitOutputTxFee; + private boolean isPrivateOffer; + private String passphraseHash; + private String passphrase; public OfferInfoBuilder withId(String id) { this.id = id; @@ -234,6 +237,21 @@ public OfferInfoBuilder withSplitOutputTxFee(long splitOutputTxFee) { return this; } + public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) { + this.isPrivateOffer = isPrivateOffer; + return this; + } + + public OfferInfoBuilder withPassphraseHash(String passphraseHash) { + this.passphraseHash = passphraseHash; + return this; + } + + public OfferInfoBuilder withPassphrase(String passphrase) { + this.passphrase = passphrase; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index afd4366a3b4..60fa8eb2507 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -33,10 +33,8 @@ import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatisticsManager; -import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; -import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; @@ -102,9 +100,10 @@ public Offer createAndGetOffer(String offerId, Price fixedPrice, boolean useMarketBasedPrice, double marketPriceMargin, - double securityDepositAsDouble, - PaymentAccount paymentAccount) { - + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { log.info("create and get offer with offerId={}, " + "currencyCode={}, " + "direction={}, " + @@ -113,7 +112,9 @@ public Offer createAndGetOffer(String offerId, "marketPriceMargin={}, " + "amount={}, " + "minAmount={}, " + - "securityDeposit={}", + "securityDepositPct={}, " + + "isPrivateOffer={}, " + + "buyerAsTakerWithoutDeposit={}", offerId, currencyCode, direction, @@ -122,7 +123,16 @@ public Offer createAndGetOffer(String offerId, marketPriceMargin, amount, minAmount, - securityDepositAsDouble); + securityDepositPct, + isPrivateOffer, + buyerAsTakerWithoutDeposit); + + + // verify buyer as taker security deposit + boolean isBuyerMaker = offerUtil.isBuyOffer(direction); + if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { + throw new IllegalArgumentException("Buyer as taker deposit is required for public offers"); + } // verify fixed price xor market price with margin if (fixedPrice != null) { @@ -143,10 +153,17 @@ public Offer createAndGetOffer(String offerId, } // adjust amount and min amount for fixed-price offer - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); if (fixedPrice != null) { - amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); - minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + } + + // generate one-time passphrase for private offer + String passphrase = null; + String passphraseHash = null; + if (isPrivateOffer) { + passphrase = HavenoUtils.generatePassphrase(); + passphraseHash = HavenoUtils.getPassphraseHash(passphrase); } long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L; @@ -161,21 +178,16 @@ public Offer createAndGetOffer(String offerId, String bankId = PaymentAccountUtil.getBankId(paymentAccount); List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); - - // reserved for future use cases - // Use null values if not set - boolean isPrivateOffer = false; + boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; long upperClosePrice = 0; - String hashOfChallenge = null; - Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, - currencyCode, - direction); + Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction); offerUtil.validateOfferData( - securityDepositAsDouble, + securityDepositPct, paymentAccount, currencyCode); @@ -189,11 +201,11 @@ public Offer createAndGetOffer(String offerId, useMarketBasedPriceValue, amountAsLong, minAmountAsLong, - HavenoUtils.MAKER_FEE_PCT, - HavenoUtils.TAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT, - securityDepositAsDouble, - securityDepositAsDouble, + hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers + securityDepositPct, baseCurrencyCode, counterCurrencyCode, paymentAccount.getPaymentMethod().getId(), @@ -211,7 +223,7 @@ public Offer createAndGetOffer(String offerId, upperClosePrice, lowerClosePrice, isPrivateOffer, - hashOfChallenge, + passphraseHash, extraDataMap, Version.TRADE_PROTOCOL_VERSION, null, @@ -219,38 +231,10 @@ public Offer createAndGetOffer(String offerId, null); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); + offer.setPassphrase(passphrase); return offer; } - public BigInteger getReservedFundsForOffer(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { - - BigInteger reservedFundsForOffer = getSecurityDeposit(direction, - amount, - buyerSecurityDeposit, - sellerSecurityDeposit); - if (!offerUtil.isBuyOffer(direction)) - reservedFundsForOffer = reservedFundsForOffer.add(amount); - - return reservedFundsForOffer; - } - - public BigInteger getSecurityDeposit(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { - return offerUtil.isBuyOffer(direction) ? - getBuyerSecurityDeposit(amount, buyerSecurityDeposit) : - getSellerSecurityDeposit(amount, sellerSecurityDeposit); - } - - public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit : - Restrictions.getSellerSecurityDepositAsPercent(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -259,26 +243,4 @@ private boolean isMarketPriceAvailable(String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } - - private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); - } - - private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); - } - - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); - } } diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 675fbeb550c..4c53c407aa0 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -115,6 +115,12 @@ public enum State { @Setter transient private boolean isReservedFundsSpent; + @JsonExclude + @Getter + @Setter + @Nullable + transient private String passphrase; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -337,6 +343,18 @@ public double getSellerSecurityDepositPct() { return offerPayload.getSellerSecurityDepositPct(); } + public boolean isPrivateOffer() { + return offerPayload.isPrivateOffer(); + } + + public String getPassphraseHash() { + return offerPayload.getPassphraseHash(); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; + } + public BigInteger getMaxTradeLimit() { return BigInteger.valueOf(offerPayload.getMaxTradeLimit()); } diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java index 51ac8cdee72..e64a1ee6eb9 100644 --- a/core/src/main/java/haveno/core/offer/OfferFilterService.java +++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java @@ -201,7 +201,7 @@ public boolean isMyInsufficientTradeLimit(Offer offer) { accountAgeWitnessService); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection())) + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) .orElse(0L); long offerMinAmount = offer.getMinAmount().longValueExact(); log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index fa05685dee7..9dfcabbaacc 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -156,7 +156,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Reserved for possible future use to support private trades where the taker needs to have an accessKey private final boolean isPrivateOffer; @Nullable - private final String hashOfChallenge; + private final String passphraseHash; /////////////////////////////////////////////////////////////////////////////////////////// @@ -195,7 +195,7 @@ public OfferPayload(String id, long lowerClosePrice, long upperClosePrice, boolean isPrivateOffer, - @Nullable String hashOfChallenge, + @Nullable String passphraseHash, @Nullable Map extraDataMap, int protocolVersion, @Nullable NodeAddress arbitratorSigner, @@ -238,7 +238,7 @@ public OfferPayload(String id, this.lowerClosePrice = lowerClosePrice; this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; - this.hashOfChallenge = hashOfChallenge; + this.passphraseHash = passphraseHash; } public byte[] getHash() { @@ -284,7 +284,7 @@ public byte[] getSignatureHash() { lowerClosePrice, upperClosePrice, isPrivateOffer, - hashOfChallenge, + passphraseHash, extraDataMap, protocolVersion, arbitratorSigner, @@ -328,12 +328,17 @@ public BigInteger getMaxSellerSecurityDeposit() { public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct()); - return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted); + boolean isBuyerTaker = getDirection() == OfferDirection.SELL; + if (isPrivateOffer() && isBuyerTaker) { + return securityDepositUnadjusted; + } else { + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); + } } public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct()); - return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted); + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -376,7 +381,7 @@ public protobuf.StoragePayload toProtoMessage() { Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); - Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); + Optional.ofNullable(passphraseHash).ifPresent(builder::setPassphraseHash); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); @@ -392,7 +397,7 @@ public static OfferPayload fromProto(protobuf.OfferPayload proto) { null : new ArrayList<>(proto.getAcceptedCountryCodesList()); List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? null : new ArrayList<>(proto.getReserveTxKeyImagesList()); - String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); + String passphraseHash = ProtoUtil.stringOrNullFromProto(proto.getPassphraseHash()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); @@ -428,7 +433,7 @@ public static OfferPayload fromProto(protobuf.OfferPayload proto) { proto.getLowerClosePrice(), proto.getUpperClosePrice(), proto.getIsPrivateOffer(), - hashOfChallenge, + passphraseHash, extraDataMapMap, proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, @@ -475,7 +480,7 @@ public String toString() { ",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n upperClosePrice=" + upperClosePrice + ",\r\n isPrivateOffer=" + isPrivateOffer + - ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + + ",\r\n passphraseHash='" + passphraseHash + '\'' + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + "\r\n} "; diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 72593ab5e78..3d4c1c376e5 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -58,8 +58,8 @@ import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; -import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; -import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.HashMap; @@ -120,9 +120,10 @@ public boolean isBuyOffer(OfferDirection direction) { public long getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, - OfferDirection direction) { + OfferDirection direction, + boolean buyerAsTakerWithoutDeposit) { return paymentAccount != null - ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) + ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) : 0; } @@ -228,16 +229,16 @@ public Map getExtraDataMap(PaymentAccount paymentAccount, return extraDataMap.isEmpty() ? null : extraDataMap; } - public void validateOfferData(double buyerSecurityDeposit, + public void validateOfferData(double securityDeposit, PaymentAccount paymentAccount, String currencyCode) { checkNotNull(p2PService.getAddress(), "Address must not be null"); - checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), + checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(), "securityDeposit must not exceed " + - getMaxBuyerSecurityDepositAsPercent()); - checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), + getMaxSecurityDepositAsPercent()); + checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(), "securityDeposit must not be less than " + - getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit); + getMinSecurityDepositAsPercent() + " but was " + securityDeposit); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index b0df3e0e359..fbdf23f8d66 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -96,6 +96,9 @@ public enum State { @Getter private String reserveTxKey; @Getter + @Setter + private String passphrase; + @Getter private final long triggerPrice; @Getter @Setter @@ -107,7 +110,6 @@ public enum State { @Getter @Setter transient int numProcessingAttempts = 0; - public OpenOffer(Offer offer) { this(offer, 0, false); } @@ -120,6 +122,7 @@ public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; + this.passphrase = offer.getPassphrase(); state = State.PENDING; } @@ -137,6 +140,7 @@ public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxKey = openOffer.reserveTxKey; + this.passphrase = openOffer.passphrase; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -153,7 +157,8 @@ private OpenOffer(Offer offer, long splitOutputTxFee, @Nullable String reserveTxHash, @Nullable String reserveTxHex, - @Nullable String reserveTxKey) { + @Nullable String reserveTxKey, + @Nullable String passphrase) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; @@ -164,6 +169,7 @@ private OpenOffer(Offer offer, this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; + this.passphrase = passphrase; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -184,6 +190,7 @@ public protobuf.Tradable toProtoMessage() { Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); + Optional.ofNullable(passphrase).ifPresent(e -> builder.setPassphrase(passphrase)); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -199,7 +206,8 @@ public static Tradable fromProto(protobuf.OpenOffer proto) { proto.getSplitOutputTxFee(), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), - ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey())); + ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())); return openOffer; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c8390a5af02..3bc899e0e16 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -79,6 +79,7 @@ import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.BtcWalletService; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.core.xmr.wallet.TradeWalletService; @@ -1307,7 +1308,7 @@ private void handleSignOfferRequest(SignOfferRequest request, NodeAddress peer) NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { errorMessage = "Cannot sign offer because we are not a registered arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } @@ -1315,47 +1316,109 @@ private void handleSignOfferRequest(SignOfferRequest request, NodeAddress peer) // verify arbitrator is signer of offer payload if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) { errorMessage = "Cannot sign offer because offer payload is for a different arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - // verify maker's trade fee + // private offers must have passphrase hash Offer offer = new Offer(request.getOfferPayload()); - if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId; - log.info(errorMessage); + if (offer.isPrivateOffer() && (offer.getPassphraseHash() == null || offer.getPassphraseHash().length() == 0)) { + errorMessage = "Private offer must have passphrase hash for offer " + request.offerId; + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - // verify taker's trade fee - if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { - errorMessage = "Wrong taker fee for offer " + request.offerId; - log.info(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; + // verify maker and taker fees + boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getPassphraseHash() != null && offer.getPassphraseHash().length() > 0 && offer.getTakerFeePct() == 0; + if (hasBuyerAsTakerWithoutDeposit) { + + // verify maker's trade fee + if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) { + errorMessage = "Wrong maker fee for offer " + request.offerId; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's trade fee + if (offer.getTakerFeePct() != 0) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify maker security deposit + if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's security deposit + if (offer.getBuyerSecurityDepositPct() != 0) { + errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } else { + + // verify maker's trade fee + if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { + errorMessage = "Wrong maker fee for offer " + request.offerId; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's trade fee + if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify seller's security deposit + if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify buyer's security deposit + if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // security deposits must be equal + if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { + errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } } // verify penalty fee if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { errorMessage = "Wrong penalty fee for offer " + request.offerId; - log.info(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; - } - - // verify security deposits are equal - if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { - errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT); - BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.MAKER_FEE_PCT); + BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( @@ -1710,7 +1773,7 @@ private void maybeUpdatePersistedOffers() { originalOfferPayload.getLowerClosePrice(), originalOfferPayload.getUpperClosePrice(), originalOfferPayload.isPrivateOffer(), - originalOfferPayload.getHashOfChallenge(), + originalOfferPayload.getPassphraseHash(), updatedExtraDataMap, protocolVersion, originalOfferPayload.getArbitratorSigner(), diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index 493ded6759d..201eb62e3fc 100644 --- a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -88,7 +88,8 @@ protected void run() { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + null); // passphrase is required when offer taken // save trade request to later send to arbitrator model.setTradeRequest(tradeRequest); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java index dfb1b2fa34e..3b1a01beebd 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java @@ -21,6 +21,7 @@ import haveno.common.taskrunner.TaskRunner; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; @@ -63,8 +64,21 @@ public static void validateOffer(Offer offer, AccountAgeWitnessService accountAg checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); - if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); - if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + offer.isPrivateOffer(); + if (offer.isPrivateOffer()) { + boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY; + if (isBuyerMaker) { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct()); + } else { + if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + } else { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + // We remove those checks to be more flexible with future changes. /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, @@ -82,9 +96,9 @@ public static void validateOffer(Offer offer, AccountAgeWitnessService accountAg /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ - long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection()); + long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); checkArgument(offer.getAmount().longValueExact() <= maxAmount, - "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR"); + "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); checkNotNull(offer.getPrice(), "Price is null"); diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java index 85d0b99bcff..49c6da12e94 100644 --- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java @@ -148,7 +148,8 @@ private void updateBalance() { private long getMaxTradeLimit() { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), + offer.hasBuyerAsTakerWithoutDeposit()); } @NotNull diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java index 4d7b4ad0c4a..11575aeb62b 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java @@ -124,7 +124,7 @@ public static boolean isAmountValidForOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService) { boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact(); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); return !hasChargebackRisk || hasValidAccountAgeWitness; } diff --git a/core/src/main/java/haveno/core/payment/TradeLimits.java b/core/src/main/java/haveno/core/payment/TradeLimits.java index e2de303ed5c..f2b855139fa 100644 --- a/core/src/main/java/haveno/core/payment/TradeLimits.java +++ b/core/src/main/java/haveno/core/payment/TradeLimits.java @@ -31,6 +31,8 @@ @Singleton public class TradeLimits { private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that. + private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.5); // max trade limit without deposit from buyer + @Nullable @Getter private static TradeLimits INSTANCE; @@ -57,6 +59,15 @@ public BigInteger getMaxTradeLimit() { return MAX_TRADE_LIMIT; } + /** + * The maximum trade limit for a buyer without a deposit. + * + * @return the maximum trade limit for a buyer without a deposit + */ + public BigInteger getMaxTradeLimitWithoutBuyerAsTakerDeposit() { + return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT; + } + // We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account // age witness is not considered anymore (> 2 months). diff --git a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java index 7bf873ce4f7..4545a4e2105 100644 --- a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java +++ b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java @@ -59,7 +59,7 @@ public ValidationResult validate(String input) { private ValidationResult validateIfNotTooLowPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double minPercentage = Restrictions.getMinBuyerSecurityDepositAsPercent(); + double minPercentage = Restrictions.getMinSecurityDepositAsPercent(); if (percentage < minPercentage) return new ValidationResult(false, Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage))); @@ -73,7 +73,7 @@ private ValidationResult validateIfNotTooLowPercentageValue(String input) { private ValidationResult validateIfNotTooHighPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double maxPercentage = Restrictions.getMaxBuyerSecurityDepositAsPercent(); + double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent(); if (percentage > maxPercentage) return new ValidationResult(false, Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); diff --git a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java index 93f03dece5a..44d3af902a7 100644 --- a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java +++ b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java @@ -28,6 +28,8 @@ import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + /** * Trade in the context of an arbitrator. */ @@ -42,8 +44,9 @@ public ArbitratorTrade(Offer offer, String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { - super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress); + NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { + super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, passphrase); } @Override @@ -81,7 +84,8 @@ public static Tradable fromProto(protobuf.ArbitratorTrade arbitratorTradeProto, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java index 99fad94ebe2..16f2314a642 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java @@ -28,6 +28,8 @@ import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + @Slf4j public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { @@ -43,7 +45,8 @@ public BuyerAsMakerTrade(Offer offer, String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { + NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -52,7 +55,8 @@ public BuyerAsMakerTrade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -85,7 +89,8 @@ public static Tradable fromProto(protobuf.BuyerAsMakerTrade buyerAsMakerTradePro uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java index 1cb7b20d475..731fc56eed1 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java @@ -44,7 +44,8 @@ public BuyerAsTakerTrade(Offer offer, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public BuyerAsTakerTrade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } @@ -87,7 +89,8 @@ public static Tradable fromProto(protobuf.BuyerAsTakerTrade buyerAsTakerTradePro uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerTrade.java b/core/src/main/java/haveno/core/trade/BuyerTrade.java index dbf73db1012..6312e42ea82 100644 --- a/core/src/main/java/haveno/core/trade/BuyerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerTrade.java @@ -38,7 +38,8 @@ public abstract class BuyerTrade extends Trade { String uid, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress makerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -47,7 +48,8 @@ public abstract class BuyerTrade extends Trade { uid, takerNodeAddress, makerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } @Override diff --git a/core/src/main/java/haveno/core/trade/Contract.java b/core/src/main/java/haveno/core/trade/Contract.java index b0950c552f8..9a88eaff56b 100644 --- a/core/src/main/java/haveno/core/trade/Contract.java +++ b/core/src/main/java/haveno/core/trade/Contract.java @@ -36,6 +36,7 @@ import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; +import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; @@ -53,6 +54,7 @@ import javax.annotation.Nullable; import java.math.BigInteger; +import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @@ -79,6 +81,7 @@ public final class Contract implements NetworkPayload { private final String makerPayoutAddressString; private final String takerPayoutAddressString; private final String makerDepositTxHash; + @Nullable private final String takerDepositTxHash; public Contract(OfferPayload offerPayload, @@ -99,7 +102,7 @@ public Contract(OfferPayload offerPayload, String makerPayoutAddressString, String takerPayoutAddressString, String makerDepositTxHash, - String takerDepositTxHash) { + @Nullable String takerDepositTxHash) { this.offerPayload = offerPayload; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; @@ -134,31 +137,9 @@ public Contract(OfferPayload offerPayload, // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { - return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), - proto.getTradeAmount(), - proto.getTradePrice(), - NodeAddress.fromProto(proto.getBuyerNodeAddress()), - NodeAddress.fromProto(proto.getSellerNodeAddress()), - NodeAddress.fromProto(proto.getArbitratorNodeAddress()), - proto.getIsBuyerMakerAndSellerTaker(), - proto.getMakerAccountId(), - proto.getTakerAccountId(), - proto.getMakerPaymentMethodId(), - proto.getTakerPaymentMethodId(), - proto.getMakerPaymentAccountPayloadHash().toByteArray(), - proto.getTakerPaymentAccountPayloadHash().toByteArray(), - PubKeyRing.fromProto(proto.getMakerPubKeyRing()), - PubKeyRing.fromProto(proto.getTakerPubKeyRing()), - proto.getMakerPayoutAddressString(), - proto.getTakerPayoutAddressString(), - proto.getMakerDepositTxHash(), - proto.getTakerDepositTxHash()); - } - @Override public protobuf.Contract toProtoMessage() { - return protobuf.Contract.newBuilder() + protobuf.Contract.Builder builder = protobuf.Contract.newBuilder() .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) .setTradeAmount(tradeAmount) .setTradePrice(tradePrice) @@ -176,9 +157,31 @@ public protobuf.Contract toProtoMessage() { .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) .setMakerPayoutAddressString(makerPayoutAddressString) .setTakerPayoutAddressString(takerPayoutAddressString) - .setMakerDepositTxHash(makerDepositTxHash) - .setTakerDepositTxHash(takerDepositTxHash) - .build(); + .setMakerDepositTxHash(makerDepositTxHash); + Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash); + return builder.build(); + } + + public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { + return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), + proto.getTradeAmount(), + proto.getTradePrice(), + NodeAddress.fromProto(proto.getBuyerNodeAddress()), + NodeAddress.fromProto(proto.getSellerNodeAddress()), + NodeAddress.fromProto(proto.getArbitratorNodeAddress()), + proto.getIsBuyerMakerAndSellerTaker(), + proto.getMakerAccountId(), + proto.getTakerAccountId(), + proto.getMakerPaymentMethodId(), + proto.getTakerPaymentMethodId(), + proto.getMakerPaymentAccountPayloadHash().toByteArray(), + proto.getTakerPaymentAccountPayloadHash().toByteArray(), + PubKeyRing.fromProto(proto.getMakerPubKeyRing()), + PubKeyRing.fromProto(proto.getTakerPubKeyRing()), + proto.getMakerPayoutAddressString(), + proto.getTakerPayoutAddressString(), + proto.getMakerDepositTxHash(), + ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash())); } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 06133e1689b..d396d07f30c 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -28,6 +28,7 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.file.FileUtil; +import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; import haveno.core.api.XmrConnectionService; @@ -48,7 +49,10 @@ import java.math.BigInteger; import java.net.InetAddress; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.PrivateKey; +import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; @@ -87,9 +91,10 @@ public class HavenoUtils { // configure fees public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true; + public static final double PENALTY_FEE_PCT = 0.02; // 2% public static final double MAKER_FEE_PCT = 0.0015; // 0.15% public static final double TAKER_FEE_PCT = 0.0075; // 0.75% - public static final double PENALTY_FEE_PCT = 0.02; // 2% + public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee for buyer as taker with no deposit // other configuration public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes @@ -286,6 +291,41 @@ public static BigInteger parseXmr(String input) { // ------------------------ SIGNING AND VERIFYING ------------------------- + public static String generatePassphrase() { + try { + + // load bip39 words + String fileName = "bip39_english.txt"; + File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); + if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); + List bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + + // select 8 words randomly + List passphraseWords = new ArrayList(); + SecureRandom secureRandom = new SecureRandom(); + for (int i = 0; i < 8; i++) { + passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size()))); + } + return String.join(" ", passphraseWords); + } catch (Exception e) { + throw new IllegalStateException("Failed to generate passphrase", e); + } + } + + public static String getPassphraseHash(String passphrase) { + if (passphrase == null) return null; + + // tokenize passphrase + String[] words = passphrase.toLowerCase().split(" "); + + // collect first 4 letters of each word, which are unique in bip39 + List prefixes = new ArrayList(); + for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); + + // hash the result + return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes())); + } + public static byte[] sign(KeyRing keyRing, String message) { return sign(keyRing.getSignatureKeyPair().getPrivate(), message); } diff --git a/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java index c31c3253427..78cf958aa2d 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java @@ -44,7 +44,8 @@ public SellerAsMakerTrade(Offer offer, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public SellerAsMakerTrade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } @@ -87,7 +89,8 @@ public static Tradable fromProto(protobuf.SellerAsMakerTrade sellerAsMakerTradeP uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java index afca9346d16..c5e4d2ccc6f 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java @@ -44,7 +44,8 @@ public SellerAsTakerTrade(Offer offer, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public SellerAsTakerTrade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } @@ -87,7 +89,8 @@ public static Tradable fromProto(protobuf.SellerAsTakerTrade sellerAsTakerTradeP uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/SellerTrade.java b/core/src/main/java/haveno/core/trade/SellerTrade.java index 457ea10aed7..470576b5473 100644 --- a/core/src/main/java/haveno/core/trade/SellerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerTrade.java @@ -36,7 +36,8 @@ public abstract class SellerTrade extends Trade { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(offer, tradeAmount, tradePrice, @@ -45,7 +46,8 @@ public abstract class SellerTrade extends Trade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } @Override diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index b540351179b..fdeaf99d9a1 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -486,6 +486,8 @@ public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodSt private IdlePayoutSyncer idlePayoutSyncer; @Getter private boolean isCompleted; + @Getter + private final String passphrase; /////////////////////////////////////////////////////////////////////////////////////////// // Constructors @@ -500,7 +502,8 @@ protected Trade(Offer offer, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { super(); this.offer = offer; this.amount = tradeAmount.longValueExact(); @@ -511,6 +514,7 @@ protected Trade(Offer offer, this.uid = uid; this.takeOfferDate = new Date().getTime(); this.tradeListeners = new ArrayList(); + this.passphrase = passphrase; getMaker().setNodeAddress(makerNodeAddress); getTaker().setNodeAddress(takerNodeAddress); @@ -534,7 +538,8 @@ protected Trade(Offer offer, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String passphrase) { this(offer, tradeAmount, @@ -544,7 +549,8 @@ protected Trade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); } // TODO: remove these constructors @@ -559,7 +565,8 @@ protected Trade(Offer offer, NodeAddress arbitratorNodeAddress, XmrWalletService xmrWalletService, ProcessModel processModel, - String uid) { + String uid, + @Nullable String passphrase) { this(offer, tradeAmount, @@ -569,7 +576,8 @@ protected Trade(Offer offer, uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + passphrase); setAmount(tradeAmount); } @@ -1233,7 +1241,7 @@ private MoneroTxWallet doCreatePayoutTx() { Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); @@ -1324,7 +1332,7 @@ private void doProcessPayoutTx(String payoutTxHex, boolean sign, boolean publish MoneroWallet wallet = getWallet(); Contract contract = getContract(); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); // describe payout tx @@ -2091,9 +2099,9 @@ private void setStartTimeFromUnlockedTxs() { final long tradeTime = getTakeOfferDate().getTime(); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); - if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); + if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); - long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight()); + long maxHeight = Math.max(getMakerDepositTx().getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight()); long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); // If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date. @@ -2125,7 +2133,7 @@ public boolean isDepositFailed() { public boolean isDepositsPublished() { if (isDepositFailed()) return false; - return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null; + return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTxHash() != null); } public boolean isFundsLockedIn() { @@ -2277,7 +2285,11 @@ public BigInteger getMakerFee() { } public BigInteger getTakerFee() { - return offer.getTakerFee(getAmount()); + return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount()); + } + + public BigInteger getSecurityDepositBeforeMiningFee() { + return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee(); } public BigInteger getBuyerSecurityDepositBeforeMiningFee() { @@ -2288,6 +2300,14 @@ public BigInteger getSellerSecurityDepositBeforeMiningFee() { return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount()); } + public boolean isBuyerAsTakerWithoutDeposit() { + return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getBuyer() == getTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + } + @Override public BigInteger getTotalTxFee() { return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees @@ -2303,7 +2323,7 @@ public String getErrorMessage() { } public boolean isTxChainInvalid() { - return processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null; + return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit()); } /** @@ -2537,7 +2557,7 @@ private void doPollWallet() { if (isPayoutUnlocked()) return; // skip if deposit txs unknown or not requested - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if daemon not synced if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; @@ -2553,7 +2573,7 @@ private void doPollWallet() { // get txs from trade wallet MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); + Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && hasBuyerAsTakerWithoutDeposit())); if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible List txs; if (!updatePool) txs = wallet.getTxs(query); @@ -2565,22 +2585,22 @@ private void doPollWallet() { } } setDepositTxs(txs); - if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen + if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen setStateDepositsSeen(); // set actual security deposits if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger buyerSecurityDeposit = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); getBuyer().setSecurityDeposit(buyerSecurityDeposit); getSeller().setSecurityDeposit(sellerSecurityDeposit); } // check for deposit txs confirmation - if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed(); // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { setStateDepositsUnlocked(); } } @@ -2750,7 +2770,7 @@ private boolean isWalletMissingData() { log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } - if (getTakerDepositTx() == null) { + if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) { log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } @@ -2913,6 +2933,7 @@ public Message toProtoMessage() { Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(passphrase).ifPresent(e -> builder.setPassphrase(passphrase)); return builder.build(); } @@ -2982,6 +3003,7 @@ public String toString() { ",\n refundResultState=" + refundResultState + ",\n refundResultStateProperty=" + refundResultStateProperty + ",\n isCompleted=" + isCompleted + + ",\n passphrase='" + passphrase + '\'' + "\n}"; } } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index dc0427c4afe..8b085d1c42a 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -561,6 +561,12 @@ private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; Offer offer = openOffer.getOffer(); + + // validate passphrase + if (openOffer.getPassphrase() != null && !HavenoUtils.getPassphraseHash(openOffer.getPassphrase()).equals(HavenoUtils.getPassphraseHash(request.getPassphrase()))) { + log.warn("Ignoring InitTradeRequest to maker because passphrase is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } // ensure trade does not already exist Optional tradeOptional = getOpenTrade(request.getOfferId()); @@ -583,7 +589,8 @@ private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getPassphrase()); else trade = new SellerAsMakerTrade(offer, BigInteger.valueOf(request.getTradeAmount()), @@ -593,7 +600,8 @@ private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getPassphrase()); trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId()); trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); @@ -646,6 +654,12 @@ else if (request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().g return; } + // validate passphrase hash + if (offer.getPassphraseHash() != null && !offer.getPassphraseHash().equals(HavenoUtils.getPassphraseHash(request.getPassphrase()))) { + log.warn("Ignoring InitTradeRequest to arbitrator because passphrase hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + // handle trade Trade trade; Optional tradeOptional = getOpenTrade(offer.getId()); @@ -679,7 +693,8 @@ else if (request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().g UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + request.getPassphrase()); // set reserve tx hash if available Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); @@ -873,7 +888,8 @@ public void onTakeOffer(BigInteger amount, UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), - null); + null, + offer.getPassphrase()); } else { trade = new BuyerAsTakerTrade(offer, amount, @@ -883,7 +899,8 @@ public void onTakeOffer(BigInteger amount, UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), - null); + null, + offer.getPassphrase()); } trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); diff --git a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java index c743595ed40..b0ac60af850 100644 --- a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java @@ -33,7 +33,9 @@ public final class DepositRequest extends TradeMessage implements DirectMessage { private final long currentDate; private final byte[] contractSignature; + @Nullable private final String depositTxHex; + @Nullable private final String depositTxKey; @Nullable private final byte[] paymentAccountKey; @@ -43,8 +45,8 @@ public DepositRequest(String tradeId, String messageVersion, long currentDate, byte[] contractSignature, - String depositTxHex, - String depositTxKey, + @Nullable String depositTxHex, + @Nullable String depositTxKey, @Nullable byte[] paymentAccountKey) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -63,13 +65,12 @@ public DepositRequest(String tradeId, public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder() .setTradeId(offerId) - .setUid(uid) - .setDepositTxHex(depositTxHex) - .setDepositTxKey(depositTxKey); + .setUid(uid); builder.setCurrentDate(currentDate); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex); + Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); - return getNetworkEnvelopeBuilder().setDepositRequest(builder).build(); } @@ -81,8 +82,8 @@ public static DepositRequest fromProto(protobuf.DepositRequest proto, messageVersion, proto.getCurrentDate(), ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()), - proto.getDepositTxHex(), - proto.getDepositTxKey(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())); } diff --git a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java index 4846487598b..ec547f74dee 100644 --- a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java @@ -58,6 +58,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag private final String reserveTxKey; @Nullable private final String payoutAddress; + @Nullable + private final String passphrase; public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, String offerId, @@ -79,7 +81,8 @@ public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, - @Nullable String payoutAddress) { + @Nullable String payoutAddress, + @Nullable String passphrase) { super(messageVersion, offerId, uid); this.tradeProtocolVersion = tradeProtocolVersion; this.tradeAmount = tradeAmount; @@ -99,6 +102,7 @@ public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.payoutAddress = payoutAddress; + this.passphrase = passphrase; } @@ -129,6 +133,7 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); + Optional.ofNullable(passphrase).ifPresent(e -> builder.setPassphrase(passphrase)); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); @@ -158,7 +163,8 @@ public static InitTradeRequest fromProto(protobuf.InitTradeRequest proto, ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), - ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress())); + ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), + ProtoUtil.stringOrNullFromProto(proto.getPassphrase())); } @Override @@ -183,6 +189,7 @@ public String toString() { ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + + ",\n passphrase=" + passphrase + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java index 8945904421d..ab82e13dec0 100644 --- a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java @@ -35,7 +35,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes private final String accountId; private final byte[] paymentAccountPayloadHash; private final String payoutAddress; + @Nullable private final String depositTxHash; + @Nullable private final byte[] accountAgeWitnessSignatureOfDepositHash; public SignContractRequest(String tradeId, @@ -45,7 +47,7 @@ public SignContractRequest(String tradeId, String accountId, byte[] paymentAccountPayloadHash, String payoutAddress, - String depositTxHash, + @Nullable String depositTxHash, @Nullable byte[] accountAgeWitnessSignatureOfDepositHash) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -68,10 +70,9 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { .setUid(uid) .setAccountId(accountId) .setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash)) - .setPayoutAddress(payoutAddress) - .setDepositTxHash(depositTxHash); - + .setPayoutAddress(payoutAddress); Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build(); @@ -87,7 +88,7 @@ public static SignContractRequest fromProto(protobuf.SignContractRequest proto, proto.getAccountId(), proto.getPaymentAccountPayloadHash().toByteArray(), proto.getPayoutAddress(), - proto.getDepositTxHash(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash())); } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index b076826b950..eeef2d4daf1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -158,7 +158,6 @@ public void setDepositTxFee(BigInteger depositTxFee) { } public BigInteger getSecurityDeposit() { - if (depositTxHash == null) return null; return BigInteger.valueOf(securityDeposit); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 54ced825104..5bd7ab9d807 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -36,8 +36,9 @@ import monero.daemon.model.MoneroTx; import java.math.BigInteger; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; @Slf4j @@ -83,72 +84,86 @@ private void processDepositRequest() { byte[] signature = request.getContractSignature(); // get trader info - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - if (trader == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); - PubKeyRing peerPubKeyRing = trader.getPubKeyRing(); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); + PubKeyRing senderPubKeyRing = sender.getPubKeyRing(); // verify signature - if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) { + if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) { throw new RuntimeException("Peer's contract signature is invalid"); } // set peer's signature - trader.setContractSignature(signature); + sender.setContractSignature(signature); // collect expected values Offer offer = trade.getOffer(); - boolean isFromTaker = trader == trade.getTaker(); - boolean isFromBuyer = trader == trade.getBuyer(); + boolean isFromTaker = sender == trade.getTaker(); + boolean isFromBuyer = sender == trade.getBuyer(); BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee(); BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount(); BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); String depositAddress = processModel.getMultisigAddress(); + sender.setSecurityDeposit(securityDeposit); // verify deposit tx - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyDepositTx( - offer.getId(), - tradeFee, - trade.getProcessModel().getTradeFeeAddress(), - sendTradeAmount, - securityDeposit, - depositAddress, - trader.getDepositTxHash(), - request.getDepositTxHex(), - request.getDepositTxKey(), - null); - } catch (Exception e) { - throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { + MoneroTx verifiedTx; + try { + verifiedTx = trade.getXmrWalletService().verifyDepositTx( + offer.getId(), + tradeFee, + trade.getProcessModel().getTradeFeeAddress(), + sendTradeAmount, + securityDeposit, + depositAddress, + sender.getDepositTxHash(), + request.getDepositTxHex(), + request.getDepositTxKey(), + null); + } catch (Exception e) { + throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } + + // update trade state + sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setDepositTxFee(verifiedTx.getFee()); + sender.setDepositTxHex(request.getDepositTxHex()); + sender.setDepositTxKey(request.getDepositTxKey()); } // update trade state - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setDepositTxFee(verifiedTx.getFee()); - trader.setDepositTxHex(request.getDepositTxHex()); - trader.setDepositTxKey(request.getDepositTxKey()); - if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); + if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey()); processModel.getTradeManager().requestPersistence(); - // relay deposit txs when both available + // relay deposit txs when both requests received MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) { + if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { // check timeout and extend just before relaying if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); + // relay deposit txs boolean depositTxsRelayed = false; + List txHashes = new ArrayList<>(); try { - // submit txs to pool but do not relay + // submit maker tx to pool but do not relay MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true); - MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult)); - if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + txHashes.add(processModel.getMaker().getDepositTxHash()); + + // submit taker tx to pool but do not relay + if (!trade.hasBuyerAsTakerWithoutDeposit()) { + MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); + if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + txHashes.add(processModel.getTaker().getDepositTxHash()); + } // relay txs - daemon.relayTxsByHash(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())); + daemon.relayTxsByHash(txHashes); depositTxsRelayed = true; // update trade state @@ -160,7 +175,7 @@ private void processDepositRequest() { // flush txs from pool try { - daemon.flushTxPool(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()); + daemon.flushTxPool(txHashes); } catch (Exception e2) { log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2); } @@ -180,7 +195,7 @@ private void processDepositRequest() { }); if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); - if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); + if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index 10baac85674..18e97dd4662 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -53,38 +53,44 @@ protected void run() { TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); boolean isFromMaker = sender == trade.getMaker(); boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL; + sender = isFromMaker ? processModel.getMaker() : processModel.getTaker(); + BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + sender.setSecurityDeposit(securityDeposit); // TODO (woodser): if signer online, should never be called by maker? - // process reserve tx with expected values - BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); - BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); - BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount - BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyReserveTx( - offer.getId(), - penaltyFee, - tradeFee, - sendAmount, - securityDeposit, - request.getPayoutAddress(), - request.getReserveTxHash(), - request.getReserveTxHex(), - request.getReserveTxKey(), - null); - } catch (Exception e) { - log.error(ExceptionUtils.getStackTrace(e)); - throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); - } + // process reserve tx unless from buyer as taker without deposit + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { - // save reserve tx to model - TradePeer trader = isFromMaker ? processModel.getMaker() : processModel.getTaker(); - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setReserveTxHash(request.getReserveTxHash()); - trader.setReserveTxHex(request.getReserveTxHex()); - trader.setReserveTxKey(request.getReserveTxKey()); + // process reserve tx with expected values + BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); + BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); + BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount + MoneroTx verifiedTx; + try { + verifiedTx = trade.getXmrWalletService().verifyReserveTx( + offer.getId(), + penaltyFee, + tradeFee, + sendAmount, + securityDeposit, + request.getPayoutAddress(), + request.getReserveTxHash(), + request.getReserveTxHex(), + request.getReserveTxKey(), + null); + } catch (Exception e) { + log.error(ExceptionUtils.getStackTrace(e)); + throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } + + // save reserve tx to model + sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setReserveTxHash(request.getReserveTxHash()); + sender.setReserveTxHex(request.getReserveTxHex()); + sender.setReserveTxKey(request.getReserveTxKey()); + } // persist trade processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index 9ec7aebe280..a846077db4e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -78,6 +78,7 @@ protected void run() { null, null, null, + null, null); // send request to taker @@ -118,7 +119,7 @@ private void sendInitMultisigRequests() { // ensure arbitrator has reserve txs if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); - if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); + if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); // create wallet for multisig MoneroWallet multisigWallet = trade.createWallet(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index 8b739011d59..05fee1374ad 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -74,7 +74,7 @@ protected void run() { Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null"); Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); - Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); + if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); // create payout tx if we have seller's updated multisig hex diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java index 91398e82e1c..b640f9d9c48 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java @@ -138,7 +138,8 @@ private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectM trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); + model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(), + trade.getPassphrase()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 69d1620aeaf..e1c4cce5cc3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -83,7 +83,7 @@ protected void run() { if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); - // collect relevant info + // collect info Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { @@ -97,53 +97,60 @@ protected void run() { } // attempt creating deposit tx - try { - synchronized (HavenoUtils.getWalletFunctionLock()) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); - try { - depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); - } catch (Exception e) { - log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (!trade.isBuyerAsTakerWithoutDeposit()) { + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); + } catch (Exception e) { + log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + if (depositTx != null) break; } - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); - if (depositTx != null) break; } + } catch (Exception e) { + + // thaw deposit inputs + if (depositTx != null) { + trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + + // re-freeze maker offer inputs + if (trade instanceof MakerTrade) { + trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); + } + + throw e; } - } catch (Exception e) { - - // thaw deposit inputs - if (depositTx != null) { - trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); - trade.getSelf().setReserveTxKeyImages(null); - } - - // re-freeze maker offer inputs - if (trade instanceof MakerTrade) { - trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); - } - - throw e; } // reset protocol timeout trade.addInitProgressStep(); // update trade state - BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); - trade.getSelf().setDepositTx(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - trade.getSelf().setDepositTxFee(depositTx.getFee()); - trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); + trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash()); + BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + if (depositTx == null) { + trade.getSelf().setSecurityDeposit(securityDeposit); + } else { + trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); + trade.getSelf().setDepositTx(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); + } } // maker signs deposit hash nonce to avoid challenge protocol @@ -161,7 +168,7 @@ protected void run() { trade.getProcessModel().getAccountId(), trade.getSelf().getPaymentAccountPayload().getHash(), trade.getSelf().getPayoutAddressString(), - depositTx.getHash(), + depositTx == null ? null : depositTx.getHash(), sig); // send request to trading peer diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java index 8fc93df9a94..1ce805aca66 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -63,20 +63,20 @@ protected void run() { // extract fields from request // TODO (woodser): verify request and from maker or taker SignContractRequest request = (SignContractRequest) processModel.getTradeMessage(); - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - trader.setDepositTxHash(request.getDepositTxHash()); - trader.setAccountId(request.getAccountId()); - trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); - trader.setPayoutAddressString(request.getPayoutAddress()); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + sender.setDepositTxHash(request.getDepositTxHash()); + sender.setAccountId(request.getAccountId()); + sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); + sender.setPayoutAddressString(request.getPayoutAddress()); // maker sends witness signature of deposit tx hash - if (trader == trade.getMaker()) { - trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); - trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); + if (sender == trade.getMaker()) { + sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); + sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); } - // sign contract only when both deposit txs hashes known - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) { + // sign contract only when received from both peers + if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) { complete(); return; } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java index dd43d6944fd..7101c488a51 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java @@ -82,8 +82,8 @@ protected void run() { Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getContractSignature(), - trade.getSelf().getDepositTx().getFullHex(), - trade.getSelf().getDepositTx().getKey(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(), trade.getSelf().getPaymentAccountKey()); // update trade state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 5ab91343aa5..e6c71032f42 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -47,62 +47,63 @@ protected void run() { throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); } - // create reserve tx + // create reserve tx unless deposit not required from buyer as taker MoneroTxWallet reserveTx = null; - synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); - trade.startProtocolTimeout(); - - // collect relevant info - BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); - BigInteger takerFee = trade.getTakerFee(); - BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; - BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); - String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - - // attempt creating reserve tx - try { - synchronized (HavenoUtils.getWalletFunctionLock()) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); - try { - reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); - } catch (Exception e) { - log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (!trade.isBuyerAsTakerWithoutDeposit()) { + synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); + + // collect relevant info + BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); + BigInteger takerFee = trade.getTakerFee(); + BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; + BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee(); + String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + + // attempt creating reserve tx + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (Exception e) { + log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + if (reserveTx != null) break; } - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); - if (reserveTx != null) break; } - } - } catch (Exception e) { - - // reset state with wallet lock - model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); - if (reserveTx != null) { - model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); - trade.getSelf().setReserveTxKeyImages(null); - } + } catch (Exception e) { - throw e; - } + // reset state with wallet lock + model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); + if (reserveTx != null) { + model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + throw e; + } - // reset protocol timeout - trade.startProtocolTimeout(); + // reset protocol timeout + trade.startProtocolTimeout(); - // update trade state - trade.getTaker().setReserveTxHash(reserveTx.getHash()); - trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); - trade.getTaker().setReserveTxKey(reserveTx.getKey()); - trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + // update trade state + trade.getTaker().setReserveTxHash(reserveTx.getHash()); + trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); + trade.getTaker().setReserveTxKey(reserveTx.getKey()); + trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + } } // save process state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java index b5a6e3624c2..188e73e0248 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java @@ -48,7 +48,9 @@ protected void run() { InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker checkNotNull(sourceRequest); checkTradeId(processModel.getOfferId(), sourceRequest); - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // create request to arbitrator Offer offer = processModel.getOffer(); @@ -73,7 +75,8 @@ protected void run() { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), + trade.getPassphrase()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java index c6315eb1741..a65c951cba8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java @@ -47,7 +47,9 @@ protected void run() { runInterceptHook(); // verify trade state - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // collect fields Offer offer = model.getOffer(); @@ -55,6 +57,7 @@ protected void run() { P2PService p2PService = processModel.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String passphrase = model.getPassphrase(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId()); @@ -81,7 +84,8 @@ protected void run() { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + passphrase); // send request to maker log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 3c09126c56a..a3756041bf3 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -616,14 +616,14 @@ public void setWithdrawalTxFeeInVbytes(long withdrawalTxFeeInVbytes) { requestPersistence(); } - public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) { - double max = Restrictions.getMaxBuyerSecurityDepositAsPercent(); - double min = Restrictions.getMinBuyerSecurityDepositAsPercent(); + public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) { + double max = Restrictions.getMaxSecurityDepositAsPercent(); + double min = Restrictions.getMinSecurityDepositAsPercent(); if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) - prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent))); else - prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent))); requestPersistence(); } @@ -755,6 +755,11 @@ public void setShowOffersMatchingMyAccounts(boolean value) { requestPersistence(); } + public void setShowPrivateOffers(boolean value) { + prefPayload.setShowPrivateOffers(value); + requestPersistence(); + } + public void setDenyApiTaker(boolean value) { prefPayload.setDenyApiTaker(value); requestPersistence(); @@ -838,16 +843,16 @@ public boolean getSplitOfferOutput() { return prefPayload.isSplitOfferOutput(); } - public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) { + public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) { double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? - prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent(); + prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent(); - if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) { - value = Restrictions.getMinBuyerSecurityDepositAsPercent(); - setBuyerSecurityDepositAsPercent(value, paymentAccount); + if (value < Restrictions.getMinSecurityDepositAsPercent()) { + value = Restrictions.getMinSecurityDepositAsPercent(); + setSecurityDepositAsPercent(value, paymentAccount); } - return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value; + return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : value; } @Override diff --git a/core/src/main/java/haveno/core/user/PreferencesPayload.java b/core/src/main/java/haveno/core/user/PreferencesPayload.java index 6d3d41f30f5..44e6aef5095 100644 --- a/core/src/main/java/haveno/core/user/PreferencesPayload.java +++ b/core/src/main/java/haveno/core/user/PreferencesPayload.java @@ -41,7 +41,7 @@ import java.util.Optional; import java.util.stream.Collectors; -import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; @Slf4j @Data @@ -120,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String rpcPw; @Nullable private String takeOfferSelectedPaymentAccountId; - private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent(); private int ignoreDustThreshold = 600; private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; - private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; private double bsqAverageTrimThreshold = 0.05; @@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope { // Added in 1.5.5 private boolean hideNonAccountPaymentMethods; private boolean showOffersMatchingMyAccounts; + private boolean showPrivateOffers; private boolean denyApiTaker; private boolean notifyOnPreRelease; @@ -193,10 +194,10 @@ public Message toProtoMessage() { .setUseStandbyMode(useStandbyMode) .setUseSoundForNotifications(useSoundForNotifications) .setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized) - .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) + .setSecurityDepositAsPercent(securityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setClearDataAfterDays(clearDataAfterDays) - .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) + .setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) @@ -205,6 +206,7 @@ public Message toProtoMessage() { .collect(Collectors.toList())) .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) + .setShowPrivateOffers(showPrivateOffers) .setDenyApiTaker(denyApiTaker) .setNotifyOnPreRelease(notifyOnPreRelease); @@ -297,10 +299,10 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), - proto.getBuyerSecurityDepositAsPercent(), + proto.getSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), proto.getClearDataAfterDays(), - proto.getBuyerSecurityDepositAsPercentForCrypto(), + proto.getSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), proto.getBsqAverageTrimThreshold(), @@ -310,6 +312,7 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co .collect(Collectors.toList())), proto.getHideNonAccountPaymentMethods(), proto.getShowOffersMatchingMyAccounts(), + proto.getShowPrivateOffers(), proto.getDenyApiTaker(), proto.getNotifyOnPreRelease(), XmrNodeSettings.fromProto(proto.getXmrNodeSettings()) diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index ec7ff113e09..bf194b3ea0b 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -47,35 +47,35 @@ public static Coin maxCoin(Coin a, Coin b) { } /** - * @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC) + * @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR) * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger value) { - return getAsPercentPerBtc(value, HavenoUtils.xmrToAtomicUnits(1.0)); + public static double getAsPercentPerXmr(BigInteger value) { + return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0)); } /** - * @param part Btc amount to be converted to percent value, based on total value passed. - * E.g. 0.1 BTC is 25% (of 0.4 BTC) - * @param total Total Btc amount the percentage part is calculated from + * @param part Xmr amount to be converted to percent value, based on total value passed. + * E.g. 0.1 XMR is 25% (of 0.4 XMR) + * @param total Total Xmr amount the percentage part is calculated from * * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger part, BigInteger total) { + public static double getAsPercentPerXmr(BigInteger part, BigInteger total) { return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4); } /** * @param percent The percentage value as double (e.g. 1% is 0.01) * @param amount The amount as atomic units for the percentage calculation - * @return The percentage as atomic units (e.g. 1% of 1 BTC is 0.01 BTC) + * @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR) */ public static BigInteger getPercentOfAmount(double percent, BigInteger amount) { if (amount == null) amount = BigInteger.ZERO; return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger(); } - public static BigInteger getRoundedAmount(BigInteger amount, Price price, long maxTradeLimit, String currencyCode, String paymentMethodId) { + public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { return getRoundedAtmCashAmount(amount, price, maxTradeLimit); } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { @@ -86,7 +86,7 @@ public static BigInteger getRoundedAmount(BigInteger amount, Price price, long m return amount; } - public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 10); } @@ -99,11 +99,11 @@ public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, * @param maxTradeLimit The max. trade limit of the users account, in atomic units. * @return The adjusted amount */ - public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, Long maxTradeLimit) { DecimalFormat decimalFormat = new DecimalFormat("#.####"); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); @@ -121,7 +121,7 @@ public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price pric * @return The adjusted amount */ @VisibleForTesting - static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTradeLimit, int factor) { + static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" @@ -163,11 +163,13 @@ static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTrad // If we are above our trade limit we reduce the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); - while (adjustedAmount > maxTradeLimit) { - adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + if (maxTradeLimit != null) { + while (adjustedAmount > maxTradeLimit) { + adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + } } adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); return BigInteger.valueOf(adjustedAmount); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index a70d2cc1f3a..b270762d3bc 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -24,11 +24,13 @@ import java.math.BigInteger; public class Restrictions { + + // configure restrictions + public static final double MIN_SECURITY_DEPOSIT_PCT = 0.15; + public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); - public static BigInteger MIN_BUYER_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); - // For the seller we use a fixed one as there is no way the seller can cancel the trade - // To make it editable would just increase complexity. - public static BigInteger MIN_SELLER_SECURITY_DEPOSIT = MIN_BUYER_SECURITY_DEPOSIT; + public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); + // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE; @@ -53,31 +55,20 @@ public static BigInteger getMinTradeAmount() { return MIN_TRADE_AMOUNT; } - public static double getDefaultBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. - } - - public static double getMinBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getDefaultSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMaxBuyerSecurityDepositAsPercent() { - return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC + public static double getMinSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - // We use MIN_BUYER_SECURITY_DEPOSIT as well as lower bound in case of small trade amounts. - // So 0.0005 BTC is the min. buyer security deposit even with amount of 0.0001 BTC and 0.05% percentage value. - public static BigInteger getMinBuyerSecurityDeposit() { - return MIN_BUYER_SECURITY_DEPOSIT; - } - - - public static double getSellerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getMaxSecurityDepositAsPercent() { + return MAX_SECURITY_DEPOSIT_PCT; } - public static BigInteger getMinSellerSecurityDeposit() { - return MIN_SELLER_SECURITY_DEPOSIT; + public static BigInteger getMinSecurityDeposit() { + return MIN_SECURITY_DEPOSIT; } // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT diff --git a/core/src/main/resources/bip39_english.txt b/core/src/main/resources/bip39_english.txt new file mode 100644 index 00000000000..942040ed50f --- /dev/null +++ b/core/src/main/resources/bip39_english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c1f98ce4202..4b89acc9a7b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -43,6 +43,8 @@ shared.buyMonero=Buy Monero shared.sellMonero=Sell Monero shared.buyCurrency=Buy {0} shared.sellCurrency=Sell {0} +shared.buyCurrencyLocked=Buy {0} 🔒 +shared.sellCurrencyLocked=Sell {0} 🔒 shared.buyingXMRWith=buying XMR with {0} shared.sellingXMRFor=selling XMR for {0} shared.buyingCurrency=buying {0} (selling XMR) @@ -55,6 +57,7 @@ shared.P2P=P2P shared.oneOffer=offer shared.multipleOffers=offers shared.Offer=Offer +shared.LockedOffer=Offer 🔒 shared.offerVolumeCode={0} Offer Volume shared.openOffers=open offers shared.trade=trade @@ -236,6 +239,7 @@ shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker +shared.passphrase=Passphrase #################################################################### @@ -349,6 +353,7 @@ market.trades.showVolumeInUSD=Show volume in USD offerbook.createOffer=Create offer offerbook.takeOffer=Take offer offerbook.takeOffer.createAccount=Create account and take offer +offerbook.takeOffer.enterPassphrase=Enter the offer passphrase offerbook.trader=Trader offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} offerbook.offerersBankName=Maker''s bank name: {0} @@ -360,6 +365,8 @@ offerbook.availableOffersToSell=Sell {0} for {1} offerbook.filterByCurrency=Choose currency offerbook.filterByPaymentMethod=Choose payment method offerbook.matchingOffers=Offers matching my accounts +offerbook.filterNoDeposit=No deposit +offerbook.noDepositOffers=Offers with no deposit or fee (requires passphrase) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted @@ -527,7 +534,10 @@ createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Minimum security deposit is used +createOffer.buyerAsTakerWithoutDeposit=No deposit from buyer (passphrase-protected) +createOffer.myDeposit=My security deposit (%) +createOffer.myDepositInfo=Your security deposit will be {0} #################################################################### @@ -2207,6 +2217,9 @@ popup.info.firewallSetupInfo=It appears this machine blocks incoming Tor connect Please set up your environment to accept incoming Tor connections, otherwise no-one will be able to take your offers. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. +popup.info.buyerAsTakerWithoutDeposit=Your offer will be protected by a passphrase and will not require a buyer deposit or fee.\ + \n\nYou must share the passphrase with your trade peer outside of Haveno so they can accept the offer.\ + \n\nThe passphrase is generated automatically and will appear in the offer details once the offer is created.\ popup.privateNotification.headline=Important private notification! diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 081a0949f7b..7768522b23f 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -150,10 +150,12 @@ public void postOffer(PostOfferRequest req, req.getMarketPriceMarginPct(), req.getAmount(), req.getMinAmount(), - req.getBuyerSecurityDepositPct(), + req.getSecurityDepositPct(), req.getTriggerPrice(), req.getReserveExactAmount(), req.getPaymentAccountId(), + req.getIsPrivateOffer(), + req.getBuyerAsTakerWithoutDeposit(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 123078b2465..d78316f95ee 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -138,6 +138,7 @@ public void takeOffer(TakeOfferRequest req, coreApi.takeOffer(req.getOfferId(), req.getPaymentAccountId(), req.getAmount(), + req.getPassphrase(), trade -> { TradeInfo tradeInfo = toTradeInfo(trade); var reply = TakeOfferReply.newBuilder() diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java index 64d0066d5ec..5abdee0d614 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java @@ -184,14 +184,14 @@ else if (!paymentAccount.getTradeCurrencies().isEmpty() && paymentAccount.getTra Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true)) + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true)) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL, false), true), DisplayUtils.formatAccountAge(accountAge)); return limitationsText; } diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 28887daaab2..39f0c7e8061 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -59,6 +59,10 @@ -fx-image: url("../../images/sell_red.png"); } +#image-lock2x { + -fx-image: url("../../images/lock@2x.png"); +} + #image-expand { -fx-image: url("../../images/expand.png"); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 325e7f2930a..bad2ee75f8c 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -107,7 +107,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount - protected final DoubleProperty buyerSecurityDepositPct = new SimpleDoubleProperty(); + protected final DoubleProperty securityDepositPct = new SimpleDoubleProperty(); + protected final BooleanProperty buyerAsTakerWithoutDeposit = new SimpleBooleanProperty(); protected final ObservableList paymentAccounts = FXCollections.observableArrayList(); @@ -166,7 +167,7 @@ public MutableOfferDataModel(CreateOfferService createOfferService, reserveExactAmount = preferences.getSplitOfferOutput(); useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice()); - buyerSecurityDepositPct.set(Restrictions.getMinBuyerSecurityDepositAsPercent()); + securityDepositPct.set(Restrictions.getMinSecurityDepositAsPercent()); paymentAccountsChangeListener = change -> fillPaymentAccounts(); } @@ -301,8 +302,10 @@ protected Offer createAndGetOffer() { useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMargin : 0, - buyerSecurityDepositPct.get(), - paymentAccount); + securityDepositPct.get(), + paymentAccount, + buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit + buyerAsTakerWithoutDeposit.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -329,10 +332,10 @@ void onPaymentAccountSelected(PaymentAccount paymentAccount) { } private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { - var minSecurityDeposit = Restrictions.getMinBuyerSecurityDepositAsPercent(); + var minSecurityDeposit = Restrictions.getMinSecurityDepositAsPercent(); try { if (getTradeCurrency() == null) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // Get average historic prices over for the prior trade period equaling the lock time @@ -355,16 +358,16 @@ private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { var min = extremes[0]; var max = extremes[1]; if (min == 0d || max == 0d) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit var suggestedSecurityDeposit = - Math.min(2 * (max - min) / max, Restrictions.getMaxBuyerSecurityDepositAsPercent()); - buyerSecurityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); + Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositAsPercent()); + securityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); } catch (Throwable t) { log.error(t.toString()); - buyerSecurityDepositPct.set(minSecurityDeposit); + securityDepositPct.set(minSecurityDeposit); } } @@ -455,6 +458,10 @@ protected void setUseMarketBasedPrice(boolean useMarketBasedPrice) { preferences.setUsePercentageBasedPrice(useMarketBasedPrice); } + protected void setBuyerAsTakerWithoutDeposit(boolean buyerAsTakerWithoutDeposit) { + this.buyerAsTakerWithoutDeposit.set(buyerAsTakerWithoutDeposit); + } + public ObservableList getPaymentAccounts() { return paymentAccounts; } @@ -467,11 +474,11 @@ long getMaxTradeLimit() { // disallow offers which no buyer can take due to trade limits on release if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get()); } if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } else { return 0; } @@ -560,10 +567,6 @@ void calculateTotalToPay() { } } - BigInteger getSecurityDeposit() { - return isBuyOffer() ? getBuyerSecurityDeposit() : getSellerSecurityDeposit(); - } - void swapTradeToSavings() { xmrWalletService.resetAddressEntriesForOpenOffer(offerId); } @@ -588,8 +591,8 @@ protected void setVolume(Volume volume) { this.volume.set(volume); } - protected void setBuyerSecurityDeposit(double value) { - this.buyerSecurityDepositPct.set(value); + protected void setSecurityDepositPct(double value) { + this.securityDepositPct.set(value); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -620,6 +623,10 @@ ReadOnlyObjectProperty getMinVolume() { return minVolume; } + public ReadOnlyBooleanProperty getBuyerAsTakerWithoutDeposit() { + return buyerAsTakerWithoutDeposit; + } + protected void setMinAmount(BigInteger minAmount) { this.minAmount.set(minAmount); } @@ -644,35 +651,19 @@ ReadOnlyBooleanProperty getUseMarketBasedPrice() { return useMarketBasedPrice; } - ReadOnlyDoubleProperty getBuyerSecurityDepositPct() { - return buyerSecurityDepositPct; + ReadOnlyDoubleProperty getSecurityDepositPct() { + return securityDepositPct; } - protected BigInteger getBuyerSecurityDeposit() { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDepositPct.get(), amount.get()); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit() { + protected BigInteger getSecurityDeposit() { BigInteger amount = this.amount.get(); - if (amount == null) - amount = BigInteger.ZERO; - - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount( - createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDepositPct.get()), amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); - } - - protected BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); + if (amount == null) amount = BigInteger.ZERO; + BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(securityDepositPct.get(), amount); + return getBoundedSecurityDeposit(percentOfAmount); } - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); + protected BigInteger getBoundedSecurityDeposit(BigInteger value) { + return Restrictions.getMinSecurityDeposit().max(value); } ReadOnlyObjectProperty totalToPayAsProperty() { @@ -684,7 +675,7 @@ public void setMarketPriceAvailable(boolean marketPriceAvailable) { } public BigInteger getMaxMakerFee() { - return HavenoUtils.multiply(amount.get(), HavenoUtils.MAKER_FEE_PCT); + return HavenoUtils.multiply(amount.get(), buyerAsTakerWithoutDeposit.get() ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); } boolean canPlaceOffer() { @@ -692,8 +683,8 @@ boolean canPlaceOffer() { GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } - public boolean isMinBuyerSecurityDeposit() { - return getBuyerSecurityDeposit().compareTo(Restrictions.getMinBuyerSecurityDeposit()) <= 0; + public boolean isMinSecurityDeposit() { + return getSecurityDeposit().compareTo(Restrictions.getMinSecurityDeposit()) <= 0; } public void setTriggerPrice(long triggerPrice) { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 3c6ed097889..c46f1f904a5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -77,6 +77,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -132,16 +133,17 @@ public abstract class MutableOfferView> exten private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; private Button priceTypeToggleButton; private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; - protected InputTextField amountTextField, minAmountTextField, volumeTextField, buyerSecurityDepositInputTextField; + protected InputTextField amountTextField, minAmountTextField, volumeTextField, securityDepositInputTextField; private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; private CheckBox reserveExactAmountCheckbox; + private ToggleButton buyerAsTakerWithoutDepositSlider; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, - resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, buyerSecurityDepositLabel, - buyerSecurityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; + resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, securityDepositLabel, + securityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; @@ -149,16 +151,16 @@ public abstract class MutableOfferView> exten private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, - minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox; + minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; private Subscription isWaitingForFundsSubscription, balanceSubscription; private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, - buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, + securityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, - isMinBuyerSecurityDepositListener, triggerPriceFocusedListener; + isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, - marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener; + marketPriceMarginListener, volumeListener, securityDepositInXMRListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; @@ -168,7 +170,7 @@ public abstract class MutableOfferView> exten private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean zelleWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, - buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField; + securityDepositInfoInputTextField, triggerPriceInfoInputTextField; private Text xIcon, fakeXIcon; @Setter @@ -252,6 +254,8 @@ protected void doActivate() { Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip")); triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); + + buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); } } @@ -323,6 +327,9 @@ public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, fundFromSavingsWalletButton.setId("sell-button"); } + buyerAsTakerWithoutDepositSlider.setVisible(model.isSellOffer()); + buyerAsTakerWithoutDepositSlider.setManaged(model.isSellOffer()); + placeOfferButton.updateText(placeOfferButtonLabel); updatePriceToggle(); } @@ -375,8 +382,11 @@ private void onShowPayFundsScreen() { setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); updateQrCode(); @@ -556,8 +566,8 @@ private void addBindings() { volumeTextField.promptTextProperty().bind(model.volumePromptLabel); totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsProperty().bind(model.getDataModel().getMissingCoin()); - buyerSecurityDepositInputTextField.textProperty().bindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().bind(model.buyerSecurityDepositLabel); + securityDepositInputTextField.textProperty().bindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().bind(model.securityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); @@ -567,7 +577,7 @@ private void addBindings() { fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult); triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); - buyerSecurityDepositInputTextField.validationResultProperty().bind(model.buyerSecurityDepositValidationResult); + securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); @@ -604,8 +614,8 @@ private void removeBindings() { volumeTextField.promptTextProperty().unbindBidirectional(model.volume); totalToPayTextField.textProperty().unbind(); addressTextField.amountAsProperty().unbind(); - buyerSecurityDepositInputTextField.textProperty().unbindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().unbind(); + securityDepositInputTextField.textProperty().unbindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().unbind(); tradeFeeInXmrLabel.textProperty().unbind(); tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); @@ -617,7 +627,7 @@ private void removeBindings() { fixedPriceTextField.validationResultProperty().unbind(); triggerPriceInputTextField.validationResultProperty().unbind(); volumeTextField.validationResultProperty().unbind(); - buyerSecurityDepositInputTextField.validationResultProperty().unbind(); + securityDepositInputTextField.validationResultProperty().unbind(); // funding fundingHBox.visibleProperty().unbind(); @@ -679,9 +689,9 @@ private void createListeners() { model.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(model.volume.get()); }; - buyerSecurityDepositFocusedListener = (o, oldValue, newValue) -> { - model.onFocusOutBuyerSecurityDepositTextField(oldValue, newValue); - buyerSecurityDepositInputTextField.setText(model.buyerSecurityDeposit.get()); + securityDepositFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutSecurityDepositTextField(oldValue, newValue); + securityDepositInputTextField.setText(model.securityDeposit.get()); }; triggerPriceFocusedListener = (o, oldValue, newValue) -> { @@ -750,12 +760,11 @@ private void createListeners() { } }; - buyerSecurityDepositInBTCListener = (observable, oldValue, newValue) -> { + securityDepositInXMRListener = (observable, oldValue, newValue) -> { if (!newValue.equals("")) { - Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(newValue)); - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + updateSecurityDepositLabels(); } else { - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(null); + securityDepositInfoInputTextField.setContentForInfoPopOver(null); } }; @@ -805,19 +814,31 @@ private void createListeners() { } }; - isMinBuyerSecurityDepositListener = ((observable, oldValue, newValue) -> { - if (newValue) { - // show BTC - buyerSecurityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); - buyerSecurityDepositInputTextField.setDisable(true); - } else { - // show % - buyerSecurityDepositPercentageLabel.setText("%"); - buyerSecurityDepositInputTextField.setDisable(false); - } + isMinSecurityDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); + }); + + buyerAsTakerWithoutDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); }); } + private void updateSecurityDepositLabels() { + if (model.isMinSecurityDeposit.get()) { + // show XMR + securityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); + securityDepositInputTextField.setDisable(true); + } else { + // show % + securityDepositPercentageLabel.setText("%"); + securityDepositInputTextField.setDisable(model.getDataModel().buyerAsTakerWithoutDeposit.get()); + } + if (model.securityDepositInXMR.get() != null && !model.securityDepositInXMR.get().equals("")) { + Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(model.securityDepositInXMR.get())); + securityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + } + } + private void updateQrCode() { final byte[] imageBytes = QRCode .from(getMoneroURI()) @@ -856,8 +877,9 @@ private void addListeners() { model.marketPriceMargin.addListener(marketPriceMarginListener); model.volume.addListener(volumeListener); model.getDataModel().missingCoin.addListener(missingCoinListener); - model.buyerSecurityDepositInBTC.addListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.addListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.addListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.addListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.addListener(buyerAsTakerWithoutDepositListener); // focus out amountTextField.focusedProperty().addListener(amountFocusedListener); @@ -866,7 +888,7 @@ private void addListeners() { triggerPriceInputTextField.focusedProperty().addListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().addListener(securityDepositFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); @@ -888,8 +910,9 @@ private void removeListeners() { model.marketPriceMargin.removeListener(marketPriceMarginListener); model.volume.removeListener(volumeListener); model.getDataModel().missingCoin.removeListener(missingCoinListener); - model.buyerSecurityDepositInBTC.removeListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.removeListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.removeListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.removeListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.removeListener(buyerAsTakerWithoutDepositListener); // focus out amountTextField.focusedProperty().removeListener(amountFocusedListener); @@ -898,7 +921,7 @@ private void removeListeners() { triggerPriceInputTextField.focusedProperty().removeListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().removeListener(securityDepositFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); @@ -997,22 +1020,45 @@ private void addAmountPriceGroup() { } private void addOptionsGroup() { - setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, + setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("shared.advancedOptions"), Layout.COMPACT_GROUP_DISTANCE); - advancedOptionsBox = new HBox(); - advancedOptionsBox.setSpacing(40); + securityDepositAndFeeBox = new HBox(); + securityDepositAndFeeBox.setSpacing(40); - GridPane.setRowIndex(advancedOptionsBox, gridRow); - GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING); - GridPane.setColumnIndex(advancedOptionsBox, 0); - GridPane.setHalignment(advancedOptionsBox, HPos.LEFT); - GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); - gridPane.getChildren().add(advancedOptionsBox); + GridPane.setRowIndex(securityDepositAndFeeBox, gridRow); + GridPane.setColumnSpan(securityDepositAndFeeBox, GridPane.REMAINING); + GridPane.setColumnIndex(securityDepositAndFeeBox, 0); + GridPane.setHalignment(securityDepositAndFeeBox, HPos.LEFT); + GridPane.setMargin(securityDepositAndFeeBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(securityDepositAndFeeBox); VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); tradeFeeFieldsBox.setMinWidth(240); - advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), tradeFeeFieldsBox); + securityDepositAndFeeBox.getChildren().addAll(getSecurityDepositBox(), tradeFeeFieldsBox); + + buyerAsTakerWithoutDepositSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("createOffer.buyerAsTakerWithoutDeposit")); + buyerAsTakerWithoutDepositSlider.setOnAction(event -> { + + // popup info box + String key = "popup.info.buyerAsTakerWithoutDeposit"; + if (buyerAsTakerWithoutDepositSlider.isSelected() && DontShowAgainLookup.showAgain(key)) { + new Popup().information(Res.get(key)) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.ok")) + .onAction(() -> model.dataModel.setBuyerAsTakerWithoutDeposit(true)) + .onClose(() -> { + buyerAsTakerWithoutDepositSlider.setSelected(false); + model.dataModel.setBuyerAsTakerWithoutDeposit(false); + }) + .dontShowAgainId(key) + .show(); + } else { + model.dataModel.setBuyerAsTakerWithoutDeposit(buyerAsTakerWithoutDepositSlider.isSelected()); + } + }); + GridPane.setHalignment(buyerAsTakerWithoutDepositSlider, HPos.LEFT); + GridPane.setMargin(buyerAsTakerWithoutDepositSlider, new Insets(0, 0, 0, 0)); Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); @@ -1060,26 +1106,28 @@ protected void hideOptionsGroup() { nextButton.setManaged(false); cancelButton1.setVisible(false); cancelButton1.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); } - private VBox getBuyerSecurityDepositBox() { + private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); - buyerSecurityDepositInfoInputTextField = tuple.second; - buyerSecurityDepositInputTextField = buyerSecurityDepositInfoInputTextField.getInputTextField(); - buyerSecurityDepositPercentageLabel = tuple.third; + securityDepositInfoInputTextField = tuple.second; + securityDepositInputTextField = securityDepositInfoInputTextField.getInputTextField(); + securityDepositPercentageLabel = tuple.third; // getEditableValueBox delivers BTC, so we overwrite it with % - buyerSecurityDepositPercentageLabel.setText("%"); + securityDepositPercentageLabel.setText("%"); Tuple2 tradeInputBoxTuple = getTradeInputBox(tuple.first, model.getSecurityDepositLabel()); VBox depositBox = tradeInputBoxTuple.second; - buyerSecurityDepositLabel = tradeInputBoxTuple.first; + securityDepositLabel = tradeInputBoxTuple.first; depositBox.setMaxWidth(310); - editOfferElements.add(buyerSecurityDepositInputTextField); - editOfferElements.add(buyerSecurityDepositPercentageLabel); + editOfferElements.add(securityDepositInputTextField); + editOfferElements.add(securityDepositPercentageLabel); return depositBox; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fabad8570f3..4441befeb7f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -113,9 +113,9 @@ public abstract class MutableOfferViewModel ext public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); - protected final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositInBTC = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositLabel = new SimpleStringProperty(); + protected final StringProperty securityDeposit = new SimpleStringProperty(); + final StringProperty securityDepositInXMR = new SimpleStringProperty(); + final StringProperty securityDepositLabel = new SimpleStringProperty(); // Price in the viewModel is always dependent on fiat/crypto: Fiat Fiat/BTC, for cryptos we use inverted price. // The domain (dataModel) uses always the same price model (otherCurrencyBTC) @@ -151,14 +151,14 @@ public abstract class MutableOfferViewModel ext final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - final BooleanProperty isMinBuyerSecurityDeposit = new SimpleBooleanProperty(); + final BooleanProperty isMinSecurityDeposit = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; @@ -171,6 +171,7 @@ public abstract class MutableOfferViewModel ext private ChangeListener priceListener; private ChangeListener volumeListener; private ChangeListener securityDepositAsDoubleListener; + private ChangeListener buyerAsTakerWithoutDepositListener; private ChangeListener isWalletFundedListener; private ChangeListener errorMessageListener; @@ -303,7 +304,7 @@ private void createListeners() { dataModel.calculateVolume(); dataModel.calculateTotalToPay(); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); updateButtonDisableState(); } }; @@ -419,34 +420,36 @@ private void createListeners() { updateButtonDisableState(); } }; + securityDepositStringListener = (ov, oldValue, newValue) -> { if (!ignoreSecurityDepositStringListener) { if (securityDepositValidator.validate(newValue).isValid) { - setBuyerSecurityDepositToModel(); + setSecurityDepositToModel(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; - amountListener = (ov, oldValue, newValue) -> { if (newValue != null) { amount.set(HavenoUtils.formatXmr(newValue)); - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } else { amount.set(""); - buyerSecurityDepositInBTC.set(""); + securityDepositInXMR.set(""); } applyMakerFee(); }; + minAmountListener = (ov, oldValue, newValue) -> { if (newValue != null) minAmount.set(HavenoUtils.formatXmr(newValue)); else minAmount.set(""); }; + priceListener = (ov, oldValue, newValue) -> { ignorePriceStringListener = true; if (newValue != null) @@ -457,6 +460,7 @@ private void createListeners() { ignorePriceStringListener = false; applyMakerFee(); }; + volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) @@ -470,17 +474,25 @@ private void createListeners() { securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { - buyerSecurityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); + securityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); if (dataModel.getAmount().get() != null) { - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); } else { - buyerSecurityDeposit.set(""); - buyerSecurityDepositInBTC.set(""); + securityDeposit.set(""); + securityDepositInXMR.set(""); } }; + buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { + if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); + updateSecurityDeposit(); + applyMakerFee(); + dataModel.calculateTotalToPay(); + }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { @@ -525,14 +537,15 @@ private void addListeners() { marketPriceMargin.addListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); - buyerSecurityDeposit.addListener(securityDepositStringListener); + securityDeposit.addListener(securityDepositStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); dataModel.getMinAmount().addListener(minAmountListener); dataModel.getPrice().addListener(priceListener); dataModel.getVolume().addListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().addListener(buyerAsTakerWithoutDepositListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); @@ -547,14 +560,15 @@ private void removeListeners() { marketPriceMargin.removeListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); - buyerSecurityDeposit.removeListener(securityDepositStringListener); + securityDeposit.removeListener(securityDepositStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); dataModel.getMinAmount().removeListener(minAmountListener); dataModel.getPrice().removeListener(priceListener); dataModel.getVolume().removeListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -593,9 +607,9 @@ boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { } securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); - validateAndSetBuyerSecurityDepositToModel(); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); + validateAndSetSecurityDepositToModel(); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); + securityDepositLabel.set(getSecurityDepositLabel()); applyMakerFee(); return result; @@ -932,14 +946,14 @@ void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { } } - void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) { + void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { - InputValidator.ValidationResult result = securityDepositValidator.validate(buyerSecurityDeposit.get()); - buyerSecurityDepositValidationResult.set(result); + InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); + securityDepositValidationResult.set(result); if (result.isValid) { - double defaultSecurityDeposit = Restrictions.getDefaultBuyerSecurityDepositAsPercent(); + double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositAsPercent(); String key = "buyerSecurityDepositIsLowerAsDefault"; - double depositAsDouble = ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()); + double depositAsDouble = ParsingUtils.parsePercentStringToDouble(securityDeposit.get()); if (preferences.showAgain(key) && depositAsDouble < defaultSecurityDeposit) { String postfix = dataModel.isBuyOffer() ? Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : @@ -950,26 +964,26 @@ void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) .width(800) .actionButtonText(Res.get("createOffer.resetToDefault")) .onAction(() -> { - dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit); + dataModel.setSecurityDepositPct(defaultSecurityDeposit); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; }) .closeButtonText(Res.get("createOffer.useLowerValue")) - .onClose(this::applyBuyerSecurityDepositOnFocusOut) + .onClose(this::applySecurityDepositOnFocusOut) .dontShowAgainId(key) .show(); } else { - applyBuyerSecurityDepositOnFocusOut(); + applySecurityDepositOnFocusOut(); } } } } - private void applyBuyerSecurityDepositOnFocusOut() { - setBuyerSecurityDepositToModel(); + private void applySecurityDepositOnFocusOut() { + setSecurityDepositToModel(); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; } @@ -1024,13 +1038,15 @@ public String getTradeAmount() { } public String getSecurityDepositLabel() { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") : + Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit"); } - public String getSecurityDepositPopOverLabel(String depositInBTC) { - return dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInBTC) : - Res.get("createOffer.securityDepositInfo", depositInBTC); + public String getSecurityDepositPopOverLabel(String depositInXMR) { + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDepositInfo", depositInXMR) : + dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInXMR) : + Res.get("createOffer.securityDepositInfo", depositInXMR); } public String getSecurityDepositInfo() { @@ -1193,19 +1209,19 @@ private void setVolumeToModel() { } } - private void setBuyerSecurityDepositToModel() { - if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) { - dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get())); + private void setSecurityDepositToModel() { + if (!(dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer()) && securityDeposit.get() != null && !securityDeposit.get().isEmpty()) { + dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } - private void validateAndSetBuyerSecurityDepositToModel() { + private void validateAndSetSecurityDepositToModel() { // If the security deposit in the model is not valid percent - String value = FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get()); + String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); if (!securityDepositValidator.validate(value).isValid) { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } @@ -1263,15 +1279,17 @@ private void updateSpinnerInfo() { isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); } - private void updateBuyerSecurityDeposit() { - isMinBuyerSecurityDeposit.set(dataModel.isMinBuyerSecurityDeposit()); - - if (dataModel.isMinBuyerSecurityDeposit()) { - buyerSecurityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); - buyerSecurityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinBuyerSecurityDeposit())); + private void updateSecurityDeposit() { + isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit()); + if (dataModel.isMinSecurityDeposit()) { + securityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); + securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit())); } else { - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDepositLabel.set(getSecurityDepositLabel()); + boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); + securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? + Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer + dataModel.getSecurityDepositPct().get())); } } @@ -1293,8 +1311,8 @@ void updateButtonDisableState() { } // validating the percentage deposit value only makes sense if it is actually used - if (!dataModel.isMinBuyerSecurityDeposit()) { - inputDataValid = inputDataValid && securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + if (!dataModel.isMinSecurityDeposit()) { + inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } isNextButtonDisabled.set(!inputDataValid); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java index 8b92477e2e0..71a827ffa3f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java @@ -87,4 +87,8 @@ protected void updateBalances() { }); } + + public boolean hasTotalToPay() { + return totalToPay.get() != null && totalToPay.get().compareTo(BigInteger.ZERO) > 0; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 6d9575b0d3e..9b3571458f3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -17,6 +17,7 @@ package haveno.desktop.main.offer.offerbook; +import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; @@ -44,7 +45,6 @@ import haveno.desktop.components.AccountStatusTooltipLabel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; -import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; @@ -83,6 +83,7 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; @@ -120,7 +121,8 @@ abstract public class OfferBookView paymentMethodComboBox; private AutoTooltipButton createOfferButton; private AutoTooltipTextField filterInputField; - private AutoTooltipSlideToggleButton matchingOffersToggle; + private ToggleButton matchingOffersToggleButton; + private ToggleButton noDepositOffersToggleButton; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; private AutoTooltipTableColumn marketColumn; @@ -183,9 +185,17 @@ public void initialize() { paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); paymentMethodComboBox.setPrefWidth(250); - matchingOffersToggle = new AutoTooltipSlideToggleButton(); - matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); - HBox.setMargin(matchingOffersToggle, new Insets(7, 0, -9, -15)); + matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.3em", null); + matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); + matchingOffersToggleButton.setPrefHeight(27); + Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); + Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); + + noDepositOffersToggleButton = new ToggleButton(Res.get("offerbook.filterNoDeposit")); + noDepositOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); + noDepositOffersToggleButton.setPrefHeight(27); + Tooltip noDepositOffersTooltip = new Tooltip(Res.get("offerbook.noDepositOffers")); + Tooltip.install(noDepositOffersToggleButton, noDepositOffersTooltip); createOfferButton = new AutoTooltipButton(""); createOfferButton.setMinHeight(40); @@ -208,7 +218,7 @@ public void initialize() { filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, - filterBox, matchingOffersToggle, getSpacer(), createOfferButtonStack); + filterBox, matchingOffersToggleButton, noDepositOffersToggleButton, getSpacer(), createOfferButtonStack); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); @@ -346,9 +356,12 @@ protected void activate() { currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem())); - matchingOffersToggle.setSelected(model.useOffersMatchingMyAccountsFilter); - matchingOffersToggle.disableProperty().bind(model.disableMatchToggle); - matchingOffersToggle.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected())); + matchingOffersToggleButton.setSelected(model.useOffersMatchingMyAccountsFilter); + matchingOffersToggleButton.disableProperty().bind(model.disableMatchToggle); + matchingOffersToggleButton.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggleButton.isSelected())); + + noDepositOffersToggleButton.setSelected(model.showPrivateOffers); + noDepositOffersToggleButton.setOnAction(e -> model.onShowPrivateOffers(noDepositOffersToggleButton.isSelected())); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); @@ -452,8 +465,10 @@ private void updateSigningStateColumn() { @Override protected void deactivate() { createOfferButton.setOnAction(null); - matchingOffersToggle.setOnAction(null); - matchingOffersToggle.disableProperty().unbind(); + matchingOffersToggleButton.setOnAction(null); + matchingOffersToggleButton.disableProperty().unbind(); + noDepositOffersToggleButton.setOnAction(null); + noDepositOffersToggleButton.disableProperty().unbind(); model.getOfferList().comparatorProperty().unbind(); volumeColumn.sortableProperty().unbind(); @@ -574,6 +589,10 @@ public void setDirection(OfferDirection direction) { iconView.setId(direction == OfferDirection.SELL ? "image-sell-white" : "image-buy-white"); createOfferButton.setId(direction == OfferDirection.SELL ? "sell-button-big" : "buy-button-big"); avatarColumn.setTitle(direction == OfferDirection.SELL ? Res.get("shared.buyerUpperCase") : Res.get("shared.sellerUpperCase")); + if (direction == OfferDirection.SELL) { + noDepositOffersToggleButton.setVisible(false); + noDepositOffersToggleButton.setManaged(false); + } } public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) { @@ -658,7 +677,7 @@ private void onShowInfo(Offer offer, OfferFilterService.Result result) { Optional account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection()); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); new Popup() .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(tradeLimit, true), @@ -1123,7 +1142,10 @@ public void updateItem(final OfferBookListItem item, boolean empty) { button2.setVisible(true); } else { boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); - iconView.setId(isSellOffer ? "image-buy-white" : "image-sell-white"); + boolean isPrivateOffer = offer.isPrivateOffer(); + iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); + iconView.setFitHeight(16); + iconView.setFitWidth(16); button.setId(isSellOffer ? "buy-button" : "sell-button"); button.setStyle("-fx-text-fill: white"); title = Res.get("offerbook.takeOffer"); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index b0a0f7c1df2..55fb0946c8b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -130,6 +130,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; boolean useOffersMatchingMyAccountsFilter; + boolean showPrivateOffers; /////////////////////////////////////////////////////////////////////////////////////////// @@ -213,6 +214,7 @@ protected void activate() { disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); } useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); + showPrivateOffers = preferences.isShowPrivateOffers(); fillCurrencies(); updateSelectedTradeCurrency(); @@ -307,6 +309,12 @@ void onShowOffersMatchingMyAccounts(boolean isSelected) { filterOffers(); } + void onShowPrivateOffers(boolean isSelected) { + showPrivateOffers = isSelected; + preferences.setShowPrivateOffers(isSelected); + filterOffers(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -571,6 +579,8 @@ private void filterOffers() { getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); + predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); + if (!filterText.isEmpty()) { // filter node address diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index beae7c11e36..0f6c5db8fb6 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -341,7 +341,7 @@ public PaymentAccount getLastSelectedPaymentAccount() { long getMaxTradeLimit() { if (paymentAccount != null) { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); } else { return 0; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 6f9991331d2..1e5711ff152 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -452,7 +452,7 @@ private void onShowPayFundsScreen() { balanceTextField.setTargetAmount(model.dataModel.getTotalToPay().get()); - if (!DevEnv.isDevMode()) { + if (!DevEnv.isDevMode() && model.dataModel.hasTotalToPay()) { String tradeAmountText = model.isSeller() ? Res.get("takeOffer.takeOfferFundWalletInfo.tradeAmount", model.getTradeAmount()) : ""; String message = Res.get("takeOffer.takeOfferFundWalletInfo.msg", model.getTotalToPayInfo(), @@ -482,7 +482,7 @@ private void onShowPayFundsScreen() { model.getSecurityDepositWithCode(), model.getTakerFeePercentage())); totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); - if (model.dataModel.getIsXmrWalletFunded().get()) { + if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay()) { if (walletFundedNotification == null) { walletFundedNotification = new Notification() .headLine(Res.get("notification.walletUpdate.headline")) @@ -937,7 +937,7 @@ private void addFundingGroup() { cancelButton2.setOnAction(e -> { String key = "CreateOfferCancelAndFunded"; - if (model.dataModel.getIsXmrWalletFunded().get() && + if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay() && model.dataModel.preferences.showAgain(key)) { new Popup().backgroundInfo(Res.get("takeOffer.alreadyFunded.askCancel")) .closeButtonText(Res.get("shared.no")) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java new file mode 100644 index 00000000000..bd169b3a4b1 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java @@ -0,0 +1,245 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.desktop.main.overlays.editor; + +import haveno.common.util.Utilities; +import haveno.core.locale.GlobalSettings; +import haveno.desktop.components.InputTextField; +import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.FormBuilder; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.value.ChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.scene.Camera; +import javafx.scene.PerspectiveCamera; +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.transform.Rotate; +import javafx.stage.Modality; +import javafx.util.Duration; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Consumer; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import static haveno.desktop.util.FormBuilder.addInputTextField; + +@Slf4j +public class PasswordPopup extends Overlay { + private InputTextField inputTextField; + private static PasswordPopup INSTANCE; + private Consumer actionHandler; + private ChangeListener focusListener; + private EventHandler keyEventEventHandler; + + public PasswordPopup() { + width = 600; + type = Type.Confirmation; + if (INSTANCE != null) + INSTANCE.hide(); + INSTANCE = this; + } + + public PasswordPopup onAction(Consumer confirmHandler) { + this.actionHandler = confirmHandler; + return this; + } + + @Override + public void show() { + actionButtonText("CONFIRM"); + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + onShow(); + } + + @Override + protected void onShow() { + super.display(); + + if (stage != null) { + focusListener = (observable, oldValue, newValue) -> { + if (!newValue) + hide(); + }; + stage.focusedProperty().addListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + @Override + public void hide() { + animateHide(); + } + + @Override + protected void onHidden() { + INSTANCE = null; + + if (stage != null) { + if (focusListener != null) + stage.focusedProperty().removeListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + private void addContent() { + gridPane.setPadding(new Insets(64)); + + inputTextField = addInputTextField(gridPane, ++rowIndex, null, -10d); + GridPane.setColumnSpan(inputTextField, 2); + inputTextField.requestFocus(); + + keyEventEventHandler = event -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { + doClose(); + } + }; + } + + @Override + protected void addHeadLine() { + super.addHeadLine(); + GridPane.setHalignment(headLineLabel, HPos.CENTER); + } + + protected void setupKeyHandler(Scene scene) { + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + e.consume(); + doClose(); + } + if (e.getCode() == KeyCode.ENTER) { + e.consume(); + apply(); + } + }); + } + + @Override + protected void animateHide(Runnable onFinishedHandler) { + if (GlobalSettings.getUseAnimations()) { + double duration = getDuration(300); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + + gridPane.setRotationAxis(Rotate.X_AXIS); + Camera camera = gridPane.getScene().getCamera(); + gridPane.getScene().setCamera(new PerspectiveCamera()); + + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.rotateProperty(), 0, interpolator), + new KeyValue(gridPane.opacityProperty(), 1, interpolator) + )); + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.rotateProperty(), -90, interpolator), + new KeyValue(gridPane.opacityProperty(), 0, interpolator) + )); + timeline.setOnFinished(event -> { + gridPane.setRotate(0); + gridPane.setRotationAxis(Rotate.Z_AXIS); + gridPane.getScene().setCamera(camera); + onFinishedHandler.run(); + }); + timeline.play(); + } else { + onFinishedHandler.run(); + } + } + + @Override + protected void animateDisplay() { + if (GlobalSettings.getUseAnimations()) { + double startY = -160; + double duration = getDuration(400); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.opacityProperty(), 0, interpolator), + new KeyValue(gridPane.translateYProperty(), startY, interpolator) + )); + + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.opacityProperty(), 1, interpolator), + new KeyValue(gridPane.translateYProperty(), 0, interpolator) + )); + + timeline.play(); + } + } + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(15, 15, 30, 30)); + } + + @Override + protected void addButtons() { + buttonDistance = 10; + super.addButtons(); + + actionButton.setOnAction(event -> apply()); + } + + private void apply() { + hide(); + if (actionHandler != null && inputTextField != null) + actionHandler.accept(inputTextField.getText()); + } + + @Override + protected void applyStyles() { + super.applyStyles(); + FormBuilder.getIconForLabel(AwesomeIcon.LOCK, headlineIcon, "1.5em"); + } + + @Override + protected void setModality() { + stage.initOwner(owner.getScene().getWindow()); + stage.initModality(Modality.NONE); + } + + @Override + protected void addEffectToBackground() { + } + + @Override + protected void removeEffectFromBackground() { + } +} diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index e93dc647a35..4f9da3879d1 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -29,6 +29,7 @@ import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; @@ -42,6 +43,8 @@ import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.main.overlays.editor.PasswordPopup; +import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; @@ -195,7 +198,7 @@ private void addContent() { rows++; } - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get(offer.isPrivateOffer() ? "shared.LockedOffer" : "shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; @@ -342,6 +345,10 @@ private void addContent() { // get amount reserved for the offer BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; + // get offer passphrase + OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + String offerPassphrase = myOpenOffer == null ? null : myOpenOffer.getPassphrase(); + rows = 3; if (countryCode != null) rows++; @@ -349,6 +356,8 @@ private void addContent() { rows++; if (reservedAmount != null) rows++; + if (offerPassphrase != null) + rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), @@ -365,6 +374,7 @@ private void addContent() { " " + HavenoUtils.formatXmr(offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); + if (reservedAmount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } @@ -373,6 +383,9 @@ private void addContent() { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); + if (offerPassphrase != null) + addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.passphrase"), offerPassphrase); + if (placeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), @@ -416,13 +429,13 @@ private void addConfirmAndCancelButtons(boolean isPlaceOffer) { ++rowIndex, 1, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); - AutoTooltipButton button = (AutoTooltipButton) placeOfferTuple.first; - button.setMinHeight(40); - button.setPadding(new Insets(0, 20, 0, 20)); - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); - button.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); + AutoTooltipButton confirmButton = (AutoTooltipButton) placeOfferTuple.first; + confirmButton.setMinHeight(40); + confirmButton.setPadding(new Insets(0, 20, 0, 20)); + confirmButton.setGraphic(iconView); + confirmButton.setGraphicTextGap(10); + confirmButton.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); + confirmButton.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; @@ -436,29 +449,48 @@ private void addConfirmAndCancelButtons(boolean isPlaceOffer) { placeOfferTuple.fourth.getChildren().add(cancelButton); - button.setOnAction(e -> { + confirmButton.setOnAction(e -> { if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) { - button.setDisable(true); - cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent - // temporarily disabled due to high CPU usage (per issue #4649) - // busyAnimation.play(); - if (isPlaceOffer) { - spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); - placeOfferHandlerOptional.ifPresent(Runnable::run); + if (!isPlaceOffer && offer.isPrivateOffer()) { + new PasswordPopup() + .headLine(Res.get("offerbook.takeOffer.enterPassphrase")) + .onAction(password -> { + if (offer.getPassphraseHash().equals(HavenoUtils.getPassphraseHash(password))) { + offer.setPassphrase(password); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); + } else { + new Popup().warning(Res.get("password.wrongPw")).show(); + } + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); } else { - - // subscribe to trade progress - spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); - numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { - subscribeToProgress(spinnerInfoLabel); - }); - - takeOfferHandlerOptional.ifPresent(Runnable::run); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); } } }); } + private void confirmTakeOfferAux(Button button, Button cancelButton, Label spinnerInfoLabel, boolean isPlaceOffer) { + button.setDisable(true); + cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent + // temporarily disabled due to high CPU usage (per issue #4649) + // busyAnimation.play(); + if (isPlaceOffer) { + spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); + placeOfferHandlerOptional.ifPresent(Runnable::run); + } else { + + // subscribe to trade progress + spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); + numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { + subscribeToProgress(spinnerInfoLabel); + }); + + takeOfferHandlerOptional.ifPresent(Runnable::run); + } + } + private void subscribeToProgress(Label spinnerInfoLabel) { Trade trade = tradeManager.getTrade(offer.getId()); if (trade == null || initProgressSubscription != null) return; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 75514e43002..e3fd44c0ba0 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -52,7 +52,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { DuplicateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, - XmrWalletService btcWalletService, + XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, @@ -65,7 +65,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { super(createOfferService, openOfferManager, offerUtil, - btcWalletService, + xmrWalletService, preferences, user, p2PService, @@ -85,20 +85,21 @@ public void populateData(Offer offer) { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); - setBuyerSecurityDeposit(getBuyerSecurityAsPercent(offer)); + setSecurityDepositPct(getSecurityAsPercent(offer)); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } } - private double getBuyerSecurityAsPercent(Offer offer) { - BigInteger offerBuyerSecurityDeposit = getBoundedBuyerSecurityDeposit(offer.getMaxBuyerSecurityDeposit()); - double offerBuyerSecurityDepositAsPercent = CoinUtil.getAsPercentPerBtc(offerBuyerSecurityDeposit, + private double getSecurityAsPercent(Offer offer) { + BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); + double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, offer.getAmount()); - return Math.min(offerBuyerSecurityDepositAsPercent, - Restrictions.getMaxBuyerSecurityDepositAsPercent()); + return Math.min(offerSellerSecurityDepositAsPercent, + Restrictions.getMaxSecurityDepositAsPercent()); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index be2b811f07a..5633c2ecd5a 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -95,7 +95,7 @@ public void reset() { price.set(null); volume.set(null); minVolume.set(null); - buyerSecurityDepositPct.set(0); + securityDepositPct.set(0); paymentAccounts.clear(); paymentAccount = null; marketPriceMargin = 0; @@ -127,12 +127,12 @@ public void applyOpenOffer(OpenOffer openOffer) { // If the security deposit got bounded because it was below the coin amount limit, it can be bigger // by percentage than the restriction. We can't determine the percentage originally entered at offer // creation, so just use the default value as it doesn't matter anyway. - double buyerSecurityDepositPercent = CoinUtil.getAsPercentPerBtc(offer.getMaxBuyerSecurityDeposit(), offer.getAmount()); - if (buyerSecurityDepositPercent > Restrictions.getMaxBuyerSecurityDepositAsPercent() - && offer.getMaxBuyerSecurityDeposit().equals(Restrictions.getMinBuyerSecurityDeposit())) - buyerSecurityDepositPct.set(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + double securityDepositPercent = CoinUtil.getAsPercentPerXmr(offer.getMaxSellerSecurityDeposit(), offer.getAmount()); + if (securityDepositPercent > Restrictions.getMaxSecurityDepositAsPercent() + && offer.getMaxSellerSecurityDeposit().equals(Restrictions.getMinSecurityDeposit())) + securityDepositPct.set(Restrictions.getDefaultSecurityDepositAsPercent()); else - buyerSecurityDepositPct.set(buyerSecurityDepositPercent); + securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; } @@ -211,7 +211,7 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro offerPayload.getLowerClosePrice(), offerPayload.getUpperClosePrice(), offerPayload.isPrivateOffer(), - offerPayload.getHashOfChallenge(), + offerPayload.getPassphraseHash(), offerPayload.getExtraDataMap(), offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 721f21bbfc3..34b78be6836 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -111,7 +111,7 @@ public void onInvalidatePrice() { } public boolean isSecurityDepositValid() { - return securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + return securityDepositValidator.validate(securityDeposit.get()).isValid; } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 0fe030bc124..68ae7385a11 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -121,7 +121,7 @@ String getDirectionLabel(OpenOfferListItem item) { if ((item == null)) return ""; - return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode()); + return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode(), item.getOffer().isPrivateOffer()); } String getMarketLabel(OpenOfferListItem item) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 8119166996f..e4fc8b1bdc1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -380,14 +380,11 @@ private void doSelectItem(@Nullable PendingTradesListItem item) { tradeStateChangeListener = (observable, oldValue, newValue) -> { String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); + if (makerDepositTxHash != null || takerDepositTxHash != null) { notificationCenter.setSelectedTradeId(tradeId); selectedTrade.stateProperty().removeListener(tradeStateChangeListener); - } else { - makerTxId.set(""); - takerTxId.set(""); } }; selectedTrade.stateProperty().addListener(tradeStateChangeListener); @@ -401,13 +398,8 @@ private void doSelectItem(@Nullable PendingTradesListItem item) { isMaker = tradeManager.isMyOffer(offer); String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); - } else { - makerTxId.set(""); - takerTxId.set(""); - } + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); notificationCenter.setSelectedTradeId(tradeId); } else { selectedTrade = null; @@ -419,6 +411,10 @@ private void doSelectItem(@Nullable PendingTradesListItem item) { }); } + private String nullToEmptyString(String str) { + return str == null ? "" : str; + } + private void tryOpenDispute(boolean isSupportTicket) { Trade trade = getTrade(); if (trade == null) { @@ -446,7 +442,7 @@ private void doOpenDispute(boolean isSupportTicket, Trade trade) { } depositTxId = trade.getMaker().getDepositTxHash(); } else { - if (trade.getTaker().getDepositTxHash() == null) { + if (trade.getTaker().getDepositTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("Deposit tx must not be null"); new Popup().instruction(Res.get("portfolio.pending.error.depositTxNull")).show(); return; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index f06f615e781..263e4d0ef05 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -326,32 +326,38 @@ protected void addTradeInfoBlock() { GridPane.setColumnSpan(tradeInfoTitledGroupBg, 2); // self's deposit tx id - final Tuple3 labelSelfTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), - Layout.COMPACT_FIRST_ROW_DISTANCE); - - GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); - selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; - - String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); - if (!selfTxId.isEmpty()) - selfTxIdTextField.setup(selfTxId, trade); - else - selfTxIdTextField.cleanup(); + boolean showSelfTxId = model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showSelfTxId) { + final Tuple3 labelSelfTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), + Layout.COMPACT_FIRST_ROW_DISTANCE); + + GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); + selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; + + String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); + if (!selfTxId.isEmpty()) + selfTxIdTextField.setup(selfTxId, trade); + else + selfTxIdTextField.cleanup(); + } // peer's deposit tx id - final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, ++gridRow, Res.get("shared.peerDepositTransactionId"), - -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); - - GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); - peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; - - String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); - if (!peerTxId.isEmpty()) - peerTxIdTextField.setup(peerTxId, trade); - else - peerTxIdTextField.cleanup(); + boolean showPeerTxId = !model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showPeerTxId) { + final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, showSelfTxId ? ++gridRow : gridRow, Res.get("shared.peerDepositTransactionId"), + showSelfTxId ? -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR : Layout.COMPACT_FIRST_ROW_DISTANCE); + + GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); + peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; + + String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); + if (!peerTxId.isEmpty()) + peerTxIdTextField.setup(peerTxId, trade); + else + peerTxIdTextField.cleanup(); + } if (model.dataModel.getTrade() != null) { checkNotNull(model.dataModel.getTrade().getOffer(), "Offer must not be null in TradeStepView"); @@ -648,7 +654,7 @@ private void openMediationResultPopup(String headLine) { model.dataModel.onMoveInvalidTradeToFailedTrades(trade); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); // TODO (woodser): separate error messages for maker/taker return; - } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null) { + } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("trade.getTakerDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); //model.dataModel.addTradeToFailedTrades(); diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index 8e7345b3851..37903692ce6 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -557,3 +557,12 @@ -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +.toggle-button-no-slider:selected { + -fx-background-color: -bs-color-gray-ddd; +} diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index b4ab888f87d..7605eb18197 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -125,3 +125,8 @@ .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-3; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} diff --git a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java index 2a38895421c..f1bf25ddf25 100644 --- a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java @@ -117,10 +117,14 @@ public static String booleanToYesNo(boolean value) { /////////////////////////////////////////////////////////////////////////////////////////// public static String getDirectionWithCode(OfferDirection direction, String currencyCode) { + return getDirectionWithCode(direction, currencyCode, false); + } + + public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency", Res.getBaseCurrencyCode()); + return (direction == OfferDirection.BUY) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", Res.getBaseCurrencyCode()); else - return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency", currencyCode) : Res.get("shared.sellCurrency", currencyCode); + return (direction == OfferDirection.SELL) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", currencyCode) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", currencyCode); } public static String getDirectionBothSides(OfferDirection direction) { diff --git a/desktop/src/main/resources/images/lock.png b/desktop/src/main/resources/images/lock.png deleted file mode 100644 index 3a4bba5d91e1919cd73c6b5830508436da6a946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22292 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 D;@nZw diff --git a/desktop/src/main/resources/images/lock@2x.png b/desktop/src/main/resources/images/lock@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..371f6aeb5d8845ffaaef57e46036d90f05797826 GIT binary patch literal 721 zcmV;?0xtcDP)0EtA|W9mp&}N)5*Y$lmAxlAT8HZsXL znJ`mJ6uamTu!0LO)JzJcsC=Gtn8>J=If*k!z`(ni^S$r;e91}3p^1AbigL8AuFteI zeI6@gX`VbIXPy;Hz1}goW8B1LpIe#%2T9h99l4|5AhRxSy?#>G^qy4$)n_fpNi|q0 zi&R{-PM^s?VDwEzF;2Nu{^B?e+$O;_S%DN1;8Bh{P)Q`v84A%c-pGbA^;hKpa2Ri; zwydpVT%Hy!7!DYa4(H_0=6MrO#Y1)(b>V{Cx7YFs2iO{EKq`2sD^ z6wBM3D4zIrHeXqr#%FKw44I`K>u$`B4S=_QPC^Q`1}#qu#g7P~BY&DFXUjtA6P~4> z;kz(a`I@eWaKP6_TZJWS`&CIg>*V9=PiL%;35XXj@kP_L;K%@cy>TT|Dnrb=UeIQ_mZrU0b;q_C40!j9EVuFWY+E_ zSbguJRp;qKx+NM~=s zgTx&utX^XUAhX!BK)f{L<&)^}3^@aI{ky}U4KWIQ_9`^_bMQfpumaH8X5l>bZdA|M z&in%gfB<+YX8<(-CLe@BIs}81E23E@3PN9H0KnzM_ zAUJRYz(ayp0A@18I|7(-Uzk7;TKCFn7Bb_%|0VklVx(?C;s@SE00000NkvXXu0mjf Dtl2{# literal 0 HcmV?d00001 diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index 98c933ed4ac..33d43b7bc58 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -53,7 +53,7 @@ public void setUp() { when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(preferences.isUsePercentageBasedPrice()).thenReturn(true); - when(preferences.getBuyerSecurityDepositAsPercent(null)).thenReturn(0.01); + when(preferences.getSecurityDepositAsPercent(null)).thenReturn(0.01); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 4b33c8bab75..a8c6ede578f 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -55,6 +55,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -99,7 +100,7 @@ public void setUp() { when(paymentAccount.getPaymentMethod()).thenReturn(PaymentMethod.ZELLE); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); + when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any(), anyBoolean())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("ES", "Spain", null)); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index cb6ba818604..5a887d8fe4e 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -521,10 +521,12 @@ message PostOfferRequest { double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double buyer_security_deposit_pct = 8; + double security_deposit_pct = 8; string trigger_price = 9; bool reserve_exact_amount = 10; string payment_account_id = 11; + bool is_private_offer = 12; + bool buyer_as_taker_without_deposit = 13; } message PostOfferReply { @@ -570,6 +572,9 @@ message OfferInfo { string arbitrator_signer = 29; string split_output_tx_hash = 30; uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; + bool is_private_offer = 32; + string passphrase_hash = 33; + string passphrase = 34; } message AvailabilityResultWithDescription { @@ -785,6 +790,7 @@ message TakeOfferRequest { string offer_id = 1; string payment_account_id = 2; uint64 amount = 3 [jstype = JS_STRING]; + string passphrase = 4; } message TakeOfferReply { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index db5aa49d7d8..9ea2a8dbb18 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -250,6 +250,7 @@ message InitTradeRequest { string reserve_tx_hex = 18; string reserve_tx_key = 19; string payout_address = 20; + string passphrase = 21; } message InitMultisigRequest { @@ -650,7 +651,7 @@ message OfferPayload { int64 lower_close_price = 30; int64 upper_close_price = 31; bool is_private_offer = 32; - string hash_of_challenge = 33; + string passphrase_hash = 33; map extra_data = 34; int32 protocol_version = 35; NodeAddress arbitrator_signer = 36; @@ -1412,6 +1413,7 @@ message OpenOffer { string reserve_tx_hash = 9; string reserve_tx_hex = 10; string reserve_tx_key = 11; + string passphrase = 12; } message Tradable { @@ -1528,6 +1530,7 @@ message Trade { string counter_currency_extra_data = 26; string uid = 27; bool is_completed = 28; + string passphrase = 29; } message BuyerAsMakerTrade { @@ -1723,9 +1726,9 @@ message PreferencesPayload { string rpc_user = 43; string rpc_pw = 44; string take_offer_selected_payment_account_id = 45; - double buyer_security_deposit_as_percent = 46; + double security_deposit_as_percent = 46; int32 ignore_dust_threshold = 47; - double buyer_security_deposit_as_percent_for_crypto = 48; + double security_deposit_as_percent_for_crypto = 48; int32 block_notify_port = 49; int32 css_theme = 50; bool tac_accepted_v120 = 51; @@ -1744,6 +1747,7 @@ message PreferencesPayload { bool use_sound_for_notifications_initialized = 64; string buy_screen_other_currency_code = 65; string sell_screen_other_currency_code = 66; + bool show_private_offers = 67; } message AutoConfirmSettings {