diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh index 5ade2b55f41..72b29cdfbf3 100755 --- a/apitest/scripts/mainnet-test.sh +++ b/apitest/scripts/mainnet-test.sh @@ -14,7 +14,7 @@ # # This script must be run from the root of the project, e.g.: # -# ./cli/test.sh +# bats apitest/scripts/mainnet-test.sh @test "test unsupported method error" { run ./bisq-cli --password=xyz bogus diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 4687477e956..770df37ede2 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -70,10 +70,8 @@ public boolean hasShutdownExceptions() { @Override public void logExceptions(List exceptions, org.slf4j.Logger log) { - StringBuilder errorBuilder = new StringBuilder(); for (Throwable t : exceptions) { log.error("", t); - errorBuilder.append(t.getMessage()).append("\n"); } } diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 854ce4c59bc..7f84772f543 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -26,6 +26,8 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import org.junit.jupiter.api.TestInfo; + import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -117,4 +119,10 @@ protected static void sleep(long ms) { // empty } } + + protected final String testName(TestInfo testInfo) { + return testInfo.getTestMethod().isPresent() + ? testInfo.getTestMethod().get().getName() + : "unknown test name"; + } } diff --git a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java index 661205c74d3..9e8b0af878a 100644 --- a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -40,7 +41,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; - +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class CreatePaymentAccountTest extends MethodTest { diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java index abce9a78a66..1d44590837b 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -35,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; - +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class GetBalanceTest extends MethodTest { diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java index 82780340ee8..212eb5ca438 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -33,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; - +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class GetVersionTest extends MethodTest { diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 6a175d118ce..43073ba995b 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,6 +17,7 @@ package bisq.apitest.method; +import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; @@ -25,6 +26,7 @@ import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.OfferInfo; @@ -34,20 +36,27 @@ import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.UnlockWalletRequest; +import bisq.proto.grpc.WithdrawFundsRequest; import protobuf.PaymentAccount; import java.util.stream.Collectors; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; import static bisq.core.payment.payload.PaymentMethod.PERFECT_MONEY; +import static java.util.Arrays.stream; import static java.util.Comparator.comparing; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import bisq.apitest.ApiTestCase; import bisq.apitest.config.BisqAppConfig; +import bisq.cli.GrpcStubs; public class MethodTest extends ApiTestCase { @@ -55,6 +64,43 @@ public class MethodTest extends ApiTestCase { protected static final String MEDIATOR = "mediator"; protected static final String REFUND_AGENT = "refundagent"; + protected static GrpcStubs aliceStubs; + protected static GrpcStubs bobStubs; + + protected static PaymentAccount alicesDummyAcct; + protected static PaymentAccount bobsDummyAcct; + + public static void startSupportingApps(boolean registerDisputeAgents, + boolean generateBtcBlock, + Enum... supportingApps) { + try { + // To run Bisq apps in debug mode, use the other setUpScaffold method: + // setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon", + // "--enableBisqDebugging", "true"}); + setUpScaffold(supportingApps); + if (registerDisputeAgents) { + registerDisputeAgents(arbdaemon); + } + + if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(alicedaemon.name()))) { + aliceStubs = grpcStubs(alicedaemon); + alicesDummyAcct = getDefaultPerfectDummyPaymentAccount(alicedaemon); + } + + if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(bobdaemon.name()))) { + bobStubs = grpcStubs(bobdaemon); + bobsDummyAcct = getDefaultPerfectDummyPaymentAccount(bobdaemon); + } + + // Generate 1 regtest block for alice's and/or bob's wallet to + // show 10 BTC balance, and allow time for daemons parse the new block. + if (generateBtcBlock) + genBtcBlocksThenWait(1, 1500); + } catch (Exception ex) { + fail(ex); + } + } + // Convenience methods for building gRPC request objects protected final GetBalanceRequest createBalanceRequest() { @@ -93,6 +139,10 @@ protected final GetOfferRequest createGetOfferRequest(String offerId) { return GetOfferRequest.newBuilder().setId(offerId).build(); } + protected final CancelOfferRequest createCancelOfferRequest(String offerId) { + return CancelOfferRequest.newBuilder().setId(offerId).build(); + } + protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) { return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build(); } @@ -109,6 +159,19 @@ protected final ConfirmPaymentReceivedRequest createConfirmPaymentReceivedReques return ConfirmPaymentReceivedRequest.newBuilder().setTradeId(tradeId).build(); } + protected final KeepFundsRequest createKeepFundsRequest(String tradeId) { + return KeepFundsRequest.newBuilder() + .setTradeId(tradeId) + .build(); + } + + protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) { + return WithdrawFundsRequest.newBuilder() + .setTradeId(tradeId) + .setAddress(address) + .build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance(BisqAppConfig bisqAppConfig) { @@ -148,7 +211,7 @@ protected final CreatePaymentAccountRequest createCreatePerfectMoneyPaymentAccou .build(); } - protected final PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) { + protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) { var req = GetPaymentAccountsRequest.newBuilder().build(); var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService; PaymentAccount paymentAccount = paymentAccountsService.getPaymentAccounts(req) @@ -170,21 +233,40 @@ protected final OfferInfo getOffer(BisqAppConfig bisqAppConfig, String offerId) return grpcStubs(bisqAppConfig).offersService.getOffer(req).getOffer(); } + @SuppressWarnings("ResultOfMethodCallIgnored") + protected final void cancelOffer(BisqAppConfig bisqAppConfig, String offerId) { + var req = createCancelOfferRequest(offerId); + grpcStubs(bisqAppConfig).offersService.cancelOffer(req); + } + protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String tradeId) { var req = createGetTradeRequest(tradeId); return grpcStubs(bisqAppConfig).tradesService.getTrade(req).getTrade(); } + @SuppressWarnings("ResultOfMethodCallIgnored") protected final void confirmPaymentStarted(BisqAppConfig bisqAppConfig, String tradeId) { var req = createConfirmPaymentStartedRequest(tradeId); grpcStubs(bisqAppConfig).tradesService.confirmPaymentStarted(req); } + @SuppressWarnings("ResultOfMethodCallIgnored") protected final void confirmPaymentReceived(BisqAppConfig bisqAppConfig, String tradeId) { var req = createConfirmPaymentReceivedRequest(tradeId); grpcStubs(bisqAppConfig).tradesService.confirmPaymentReceived(req); } + @SuppressWarnings("ResultOfMethodCallIgnored") + protected final void keepFunds(BisqAppConfig bisqAppConfig, String tradeId) { + var req = createKeepFundsRequest(tradeId); + grpcStubs(bisqAppConfig).tradesService.keepFunds(req); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) { + var req = createWithdrawFundsRequest(tradeId, address); + grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req); + } // Static conveniences for test methods and test case fixture setups. protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) { diff --git a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java index 9c875012080..746c851b4d5 100644 --- a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java +++ b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -40,6 +41,7 @@ @SuppressWarnings("ResultOfMethodCallIgnored") +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class RegisterDisputeAgentsTest extends MethodTest { @@ -56,8 +58,7 @@ public static void setUp() { @Test @Order(1) public void testRegisterArbitratorShouldThrowException() { - var req = - createRegisterDisputeAgentRequest(ARBITRATOR); + var req = createRegisterDisputeAgentRequest(ARBITRATOR); Throwable exception = assertThrows(StatusRuntimeException.class, () -> grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req)); assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI", @@ -67,8 +68,7 @@ public void testRegisterArbitratorShouldThrowException() { @Test @Order(2) public void testInvalidDisputeAgentTypeArgShouldThrowException() { - var req = - createRegisterDisputeAgentRequest("badagent"); + var req = createRegisterDisputeAgentRequest("badagent"); Throwable exception = assertThrows(StatusRuntimeException.class, () -> grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req)); assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'", @@ -90,16 +90,14 @@ public void testInvalidRegistrationKeyArgShouldThrowException() { @Test @Order(4) public void testRegisterMediator() { - var req = - createRegisterDisputeAgentRequest(MEDIATOR); + var req = createRegisterDisputeAgentRequest(MEDIATOR); grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req); } @Test @Order(5) public void testRegisterRefundAgent() { - var req = - createRegisterDisputeAgentRequest(REFUND_AGENT); + var req = createRegisterDisputeAgentRequest(REFUND_AGENT); grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req); } diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java index 61f3c27e4a1..08547e9ebb9 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -18,6 +19,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @SuppressWarnings("ResultOfMethodCallIgnored") +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class WalletProtectionTest extends MethodTest { diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index 979d7a33e6a..fe9a98aaaae 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -59,28 +59,15 @@ @Slf4j public abstract class AbstractOfferTest extends MethodTest { - protected static GrpcStubs aliceStubs; - protected static GrpcStubs bobStubs; - @BeforeAll public static void setUp() { - startSupportingApps(); - } - - static void startSupportingApps() { - try { - // setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon", "--enableBisqDebugging", "true"}); - setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon); - registerDisputeAgents(arbdaemon); - aliceStubs = grpcStubs(alicedaemon); - bobStubs = grpcStubs(bobdaemon); - - // Generate 1 regtest block for alice's wallet to show 10 BTC balance, - // and give alicedaemon time to parse the new block. - genBtcBlocksThenWait(1, 1500); - } catch (Exception ex) { - fail(ex); - } + startSupportingApps(true, + true, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); } protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount, @@ -120,6 +107,11 @@ protected final OfferInfo getOffer(String offerId) { return aliceStubs.offersService.getOffer(createGetOfferRequest(offerId)).getOffer(); } + @SuppressWarnings("ResultOfMethodCallIgnored") + protected final void cancelOffer(GrpcStubs grpcStubs, String offerId) { + grpcStubs.offersService.cancelOffer(createCancelOfferRequest(offerId)); + } + protected final OfferInfo getMostRecentOffer(GrpcStubs grpcStubs, String direction, String currencyCode) { List offerInfoList = getOffersSortedByDate(grpcStubs, direction, currencyCode); if (offerInfoList.isEmpty()) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java new file mode 100644 index 00000000000..334fb022bb3 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java @@ -0,0 +1,82 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method.offer; + +import bisq.core.btc.wallet.Restrictions; + +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.OfferInfo; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CancelOfferTest extends AbstractOfferTest { + + private static final int MAX_OFFERS = 3; + + @Test + @Order(1) + public void testCancelOffer() { + var req = CreateOfferRequest.newBuilder() + .setPaymentAccountId(alicesDummyAcct.getId()) + .setDirection("buy") + .setCurrencyCode("cad") + .setAmount(10000000) + .setMinAmount(10000000) + .setUseMarketBasedPrice(true) + .setMarketPriceMargin(0.00) + .setPrice("0") + .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .build(); + + // Create some offers. + for (int i = 1; i <= MAX_OFFERS; i++) { + //noinspection ResultOfMethodCallIgnored + aliceStubs.offersService.createOffer(req); + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay. + sleep(2500); + } + + List offers = getOffersSortedByDate(aliceStubs, "buy", "cad"); + assertEquals(MAX_OFFERS, offers.size()); + + // Cancel the offers, checking the open offer count after each offer removal. + for (int i = 1; i <= MAX_OFFERS; i++) { + cancelOffer(aliceStubs, offers.remove(0).getId()); + assertEquals(MAX_OFFERS - i, getOpenOffersCount(aliceStubs, "buy", "cad")); + } + + sleep(1000); // wait for offer removal + + offers = getOffersSortedByDate(aliceStubs, "buy", "cad"); + assertEquals(0, offers.size()); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 739faf71e96..72ff91f3115 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -23,16 +23,17 @@ import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { @@ -40,9 +41,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { @Test @Order(1) public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("buy") .setCurrencyCode("aud") .setAmount(10000000) @@ -61,7 +61,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); @@ -73,7 +73,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); } @@ -81,9 +81,8 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { @Test @Order(2) public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("buy") .setCurrencyCode("usd") .setAmount(10000000) @@ -102,7 +101,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); @@ -114,7 +113,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); } @@ -122,9 +121,8 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { @Test @Order(3) public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("sell") .setCurrencyCode("eur") .setAmount(10000000) @@ -143,7 +141,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("EUR", newOffer.getCounterCurrencyCode()); @@ -155,7 +153,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("EUR", newOffer.getCounterCurrencyCode()); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index f9d379131bb..345bd130d71 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -26,12 +26,12 @@ import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static java.lang.Math.abs; @@ -41,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferPayload.Direction.BUY; +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { @@ -52,10 +53,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { @Test @Order(1) public void testCreateUSDBTCBuyOffer5PctPriceMargin() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); double priceMarginPctInput = 5.00; var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("buy") .setCurrencyCode("usd") .setAmount(10000000) @@ -73,7 +73,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); @@ -84,7 +84,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); @@ -94,10 +94,9 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { @Test @Order(2) public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); double priceMarginPctInput = -2.00; var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("buy") .setCurrencyCode("nzd") .setAmount(10000000) @@ -115,7 +114,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); @@ -126,7 +125,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); @@ -136,10 +135,9 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { @Test @Order(3) public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); double priceMarginPctInput = -1.5; var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("sell") .setCurrencyCode("gbp") .setAmount(10000000) @@ -158,7 +156,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); @@ -169,7 +167,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); @@ -179,10 +177,9 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { @Test @Order(4) public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); double priceMarginPctInput = 6.55; var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("sell") .setCurrencyCode("brl") .setAmount(10000000) @@ -201,7 +198,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); @@ -212,7 +209,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertEquals(10000000, newOffer.getAmount()); assertEquals(10000000, newOffer.getMinAmount()); assertEquals(1500000, newOffer.getBuyerSecurityDeposit()); - assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId()); + assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); diff --git a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java index 785dc97fdcb..3ddd8cb3030 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java @@ -25,15 +25,16 @@ import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ValidateCreateOfferTest extends AbstractOfferTest { @@ -41,9 +42,8 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { @Test @Order(1) public void testAmtTooLargeShouldThrowException() { - var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); var req = CreateOfferRequest.newBuilder() - .setPaymentAccountId(paymentAccount.getId()) + .setPaymentAccountId(alicesDummyAcct.getId()) .setDirection("buy") .setCurrencyCode("usd") .setAmount(100000000000L) diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index 094007abd40..e8537206dbf 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -1,9 +1,13 @@ package bisq.apitest.method.trade; -import bisq.core.trade.Trade; - import bisq.proto.grpc.TradeInfo; +import org.slf4j.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInfo; + +import static bisq.cli.TradeFormat.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -13,19 +17,44 @@ public class AbstractTradeTest extends AbstractOfferTest { + public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus(); + + // A Trade ID cache for use in @Test sequences. + protected static String tradeId; + + @BeforeAll + public static void initStaticFixtures() { + EXPECTED_PROTOCOL_STATUS.init(); + } + protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) { return bobStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); } + @SuppressWarnings("unused") protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId) { return aliceStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); } - protected final void verifyExpectedTradeStateAndPhase(TradeInfo trade, - Trade.State expectedState, - Trade.Phase expectedPhase) { + protected final void verifyExpectedProtocolStatus(TradeInfo trade) { assertNotNull(trade); - assertEquals(expectedState.name(), trade.getState()); - assertEquals(expectedPhase.name(), trade.getPhase()); + assertEquals(EXPECTED_PROTOCOL_STATUS.state.name(), trade.getState()); + assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositConfirmed()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatSent, trade.getIsFiatSent()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatReceived, trade.getIsFiatReceived()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished()); + assertEquals(EXPECTED_PROTOCOL_STATUS.isWithdrawn, trade.getIsWithdrawn()); + } + + protected final void logTrade(Logger log, + TestInfo testInfo, + String description, + TradeInfo trade) { + log.info(String.format("%s %s%n%s", + testName(testInfo), + description.toUpperCase(), + format(trade))); } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java new file mode 100644 index 00000000000..63655585947 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/ExpectedProtocolStatus.java @@ -0,0 +1,69 @@ +package bisq.apitest.method.trade; + +import bisq.core.trade.Trade; + +/** + * A test fixture encapsulating expected trade protocol status. + * Status flags should be cleared via init() before starting a new trade protocol. + */ +public class ExpectedProtocolStatus { + Trade.State state; + Trade.Phase phase; + boolean isDepositPublished; + boolean isDepositConfirmed; + boolean isFiatSent; + boolean isFiatReceived; + boolean isPayoutPublished; + boolean isWithdrawn; + + public ExpectedProtocolStatus setState(Trade.State state) { + this.state = state; + return this; + } + + public ExpectedProtocolStatus setPhase(Trade.Phase phase) { + this.phase = phase; + return this; + } + + public ExpectedProtocolStatus setDepositPublished(boolean depositPublished) { + isDepositPublished = depositPublished; + return this; + } + + public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) { + isDepositConfirmed = depositConfirmed; + return this; + } + + public ExpectedProtocolStatus setFiatSent(boolean fiatSent) { + isFiatSent = fiatSent; + return this; + } + + public ExpectedProtocolStatus setFiatReceived(boolean fiatReceived) { + isFiatReceived = fiatReceived; + return this; + } + + public ExpectedProtocolStatus setPayoutPublished(boolean payoutPublished) { + isPayoutPublished = payoutPublished; + return this; + } + + public ExpectedProtocolStatus setWithdrawn(boolean withdrawn) { + isWithdrawn = withdrawn; + return this; + } + + public void init() { + state = null; + phase = null; + isDepositPublished = false; + isDepositConfirmed = false; + isFiatSent = false; + isFiatReceived = false; + isPayoutPublished = false; + isWithdrawn = false; + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 4aba67fc715..3561787c454 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -17,58 +17,46 @@ package bisq.apitest.method.trade; -import protobuf.PaymentAccount; - import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; -import static bisq.cli.TradeFormat.format; +import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; import static bisq.core.trade.Trade.Phase.FIAT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; -import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; -import static bisq.core.trade.Trade.State.SELLER_PUBLISHED_DEPOSIT_TX; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; -import static java.lang.System.out; +import static bisq.core.trade.Trade.State.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OpenOffer.State.AVAILABLE; +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeBuyBTCOfferTest extends AbstractTradeTest { // Alice is buyer, Bob is seller. - private static String tradeId; - - private PaymentAccount alicesAccount; - private PaymentAccount bobsAccount; - - @BeforeEach - public void init() { - alicesAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); - bobsAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); - } - @Test @Order(1) - public void testTakeAlicesBuyOffer() { + public void testTakeAlicesBuyOffer(final TestInfo testInfo) { try { - var alicesOffer = createAliceOffer(alicesAccount, "buy", "usd", 12500000); + var alicesOffer = createAliceOffer(alicesDummyAcct, + "buy", + "usd", + 12500000); var offerId = alicesOffer.getId(); // Wait for Alice's AddToOfferBook task. @@ -76,7 +64,7 @@ public void testTakeAlicesBuyOffer() { sleep(3000); assertEquals(1, getOpenOffersCount(aliceStubs, "buy", "usd")); - var trade = takeAlicesOffer(offerId, bobsAccount.getId()); + var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId()); assertNotNull(trade); assertEquals(offerId, trade.getTradeId()); // Cache the trade id for the other tests. @@ -86,13 +74,19 @@ public void testTakeAlicesBuyOffer() { assertEquals(0, getOpenOffersCount(aliceStubs, "buy", "usd")); trade = getTrade(bobdaemon, trade.getTradeId()); - verifyExpectedTradeStateAndPhase(trade, SELLER_PUBLISHED_DEPOSIT_TX, DEPOSIT_PUBLISHED); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_PUBLISHED_DEPOSIT_TX) + .setPhase(DEPOSIT_PUBLISHED) + .setDepositPublished(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade); genBtcBlocksThenWait(1, 2250); trade = getTrade(bobdaemon, trade.getTradeId()); - verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade); } catch (StatusRuntimeException e) { fail(e); } @@ -100,18 +94,19 @@ public void testTakeAlicesBuyOffer() { @Test @Order(2) - public void testAlicesConfirmPaymentStarted() { + public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = getTrade(alicedaemon, tradeId); - assertNotNull(trade); - confirmPaymentStarted(alicedaemon, trade.getTradeId()); sleep(3000); trade = getTrade(alicedaemon, tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade); } catch (StatusRuntimeException e) { fail(e); } @@ -119,17 +114,41 @@ public void testAlicesConfirmPaymentStarted() { @Test @Order(3) - public void testBobsConfirmPaymentReceived() { + public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { var trade = getTrade(bobdaemon, tradeId); - assertNotNull(trade); - confirmPaymentReceived(bobdaemon, trade.getTradeId()); sleep(3000); trade = getTrade(bobdaemon, tradeId); - // TODO is this a bug? Why is offer.state == available? + // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); + } + + @Test + @Order(4) + public void testAlicesKeepFunds(final TestInfo testInfo) { + genBtcBlocksThenWait(1, 2250); + + var trade = getTrade(alicedaemon, tradeId); + logTrade(log, testInfo, "Alice's view before keeping funds", trade); + + keepFunds(alicedaemon, tradeId); + + genBtcBlocksThenWait(1, 2250); + + trade = getTrade(alicedaemon, tradeId); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after keeping funds", trade); + log.info("{} Alice's current available balance: {} BTC", + testName(testInfo), + formatSatoshis(getBalance(alicedaemon))); } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 658b4083231..35e13c002fa 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -17,58 +17,43 @@ package bisq.apitest.method.trade; -import protobuf.PaymentAccount; - import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; -import static bisq.cli.TradeFormat.format; -import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG; -import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; -import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; -import static java.lang.System.out; +import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.core.trade.Trade.Phase.*; +import static bisq.core.trade.Trade.State.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OpenOffer.State.AVAILABLE; +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeSellBTCOfferTest extends AbstractTradeTest { // Alice is seller, Bob is buyer. - private static String tradeId; - - private PaymentAccount alicesAccount; - private PaymentAccount bobsAccount; - - @BeforeEach - public void init() { - alicesAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); - bobsAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); - } - @Test @Order(1) - public void testTakeAlicesSellOffer() { + public void testTakeAlicesSellOffer(final TestInfo testInfo) { try { - var alicesOffer = createAliceOffer(alicesAccount, "sell", "usd", 12500000); + var alicesOffer = createAliceOffer(alicesDummyAcct, + "sell", + "usd", + 12500000); var offerId = alicesOffer.getId(); // Wait for Alice's AddToOfferBook task. @@ -77,7 +62,7 @@ public void testTakeAlicesSellOffer() { sleep(3000); assertEquals(1, getOpenOffersCount(bobStubs, "sell", "usd")); - var trade = takeAlicesOffer(offerId, bobsAccount.getId()); + var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId()); assertNotNull(trade); assertEquals(offerId, trade.getTradeId()); // Cache the trade id for the other tests. @@ -87,14 +72,20 @@ public void testTakeAlicesSellOffer() { assertEquals(0, getOpenOffersCount(bobStubs, "sell", "usd")); trade = getTrade(bobdaemon, trade.getTradeId()); - verifyExpectedTradeStateAndPhase(trade, BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG, DEPOSIT_PUBLISHED); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) + .setPhase(DEPOSIT_PUBLISHED) + .setDepositPublished(true); + verifyExpectedProtocolStatus(trade); + + logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade); genBtcBlocksThenWait(1, 2250); trade = getTrade(bobdaemon, trade.getTradeId()); - verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); - out.println(format(trade)); - + EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN) + .setPhase(DEPOSIT_CONFIRMED) + .setDepositConfirmed(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade); } catch (StatusRuntimeException e) { fail(e); } @@ -102,20 +93,20 @@ public void testTakeAlicesSellOffer() { @Test @Order(2) - public void testBobsConfirmPaymentStarted() { + public void testBobsConfirmPaymentStarted(final TestInfo testInfo) { try { var trade = getTrade(bobdaemon, tradeId); - assertNotNull(trade); - confirmPaymentStarted(bobdaemon, trade.getTradeId()); sleep(3000); trade = getTrade(bobdaemon, tradeId); - // TODO is this a bug? Why is offer.state == available? + // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); - - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) + .setPhase(FIAT_SENT) + .setFiatSent(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade); } catch (StatusRuntimeException e) { fail(e); } @@ -123,16 +114,42 @@ public void testBobsConfirmPaymentStarted() { @Test @Order(3) - public void testAlicesConfirmPaymentReceived() { + public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { var trade = getTrade(alicedaemon, tradeId); - assertNotNull(trade); - confirmPaymentReceived(alicedaemon, trade.getTradeId()); sleep(3000); trade = getTrade(alicedaemon, tradeId); assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); - out.println(format(trade)); + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) + .setPhase(PAYOUT_PUBLISHED) + .setPayoutPublished(true) + .setFiatReceived(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); + } + + @Test + @Order(4) + public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { + genBtcBlocksThenWait(1, 2250); + + var trade = getTrade(bobdaemon, tradeId); + logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade); + + String toAddress = bitcoinCli.getNewBtcAddress(); + withdrawFunds(bobdaemon, tradeId, toAddress); + + genBtcBlocksThenWait(1, 2250); + + trade = getTrade(bobdaemon, tradeId); + EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED) + .setPhase(WITHDRAWN) + .setWithdrawn(true); + verifyExpectedProtocolStatus(trade); + logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade); + log.info("{} Bob's current available balance: {} BTC", + testName(testInfo), + formatSatoshis(getBalance(bobdaemon))); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java index 75977949c33..4b7d40f516c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -33,9 +33,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; + + +import bisq.apitest.method.MethodTest; + @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class FundWalletScenarioTest extends ScenarioTest { +public class FundWalletScenarioTest extends MethodTest { @BeforeAll public static void setUp() { diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java new file mode 100644 index 00000000000..b01a9486ea5 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -0,0 +1,72 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + + + +import bisq.apitest.method.offer.AbstractOfferTest; +import bisq.apitest.method.offer.CancelOfferTest; +import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; +import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; +import bisq.apitest.method.offer.ValidateCreateOfferTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class OfferTest extends AbstractOfferTest { + + @Test + @Order(1) + public void testAmtTooLargeShouldThrowException() { + ValidateCreateOfferTest test = new ValidateCreateOfferTest(); + test.testAmtTooLargeShouldThrowException(); + } + + @Test + @Order(2) + public void testCancelOffer() { + CancelOfferTest test = new CancelOfferTest(); + test.testCancelOffer(); + } + + @Test + @Order(3) + public void testCreateOfferUsingFixedPrice() { + CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest(); + test.testCreateAUDBTCBuyOfferUsingFixedPrice16000(); + test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234(); + test.testCreateEURBTCSellOfferUsingFixedPrice95001234(); + } + + @Test + @Order(4) + public void testCreateOfferUsingMarketPriceMargin() { + CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest(); + test.testCreateUSDBTCBuyOffer5PctPriceMargin(); + test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin(); + test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin(); + test.testCreateBRLBTCSellOffer6Point55PctPriceMargin(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java deleted file mode 100644 index 9750b2ed9d6..00000000000 --- a/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.scenario; - -import lombok.extern.slf4j.Slf4j; - - - -import bisq.apitest.method.MethodTest; - -@Slf4j -public class ScenarioTest extends MethodTest { -} diff --git a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java new file mode 100644 index 00000000000..fa81ddff6b9 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.CreatePaymentAccountTest; +import bisq.apitest.method.GetVersionTest; +import bisq.apitest.method.MethodTest; +import bisq.apitest.method.RegisterDisputeAgentsTest; + + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class StartupTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetVersion() { + GetVersionTest test = new GetVersionTest(); + test.testGetVersion(); + } + + @Test + @Order(2) + public void testRegisterDisputeAgents() { + RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest(); + test.testRegisterArbitratorShouldThrowException(); + test.testInvalidDisputeAgentTypeArgShouldThrowException(); + test.testInvalidRegistrationKeyArgShouldThrowException(); + test.testRegisterMediator(); + test.testRegisterRefundAgent(); + } + + @Test + @Order(3) + public void testCreatePaymentAccount() { + CreatePaymentAccountTest test = new CreatePaymentAccountTest(); + test.testCreatePerfectMoneyUSDPaymentAccount(); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java new file mode 100644 index 00000000000..4c07452abc6 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/TradeTest.java @@ -0,0 +1,64 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + + + +import bisq.apitest.method.trade.AbstractTradeTest; +import bisq.apitest.method.trade.TakeBuyBTCOfferTest; +import bisq.apitest.method.trade.TakeSellBTCOfferTest; + + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TradeTest extends AbstractTradeTest { + + @BeforeEach + public void init() { + EXPECTED_PROTOCOL_STATUS.init(); + } + + @Test + @Order(1) + public void testTakeBuyBTCOffer(final TestInfo testInfo) { + TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); + test.testTakeAlicesBuyOffer(testInfo); + test.testAlicesConfirmPaymentStarted(testInfo); + test.testBobsConfirmPaymentReceived(testInfo); + test.testAlicesKeepFunds(testInfo); + } + + @Test + @Order(2) + public void testTakeSellBTCOffer(final TestInfo testInfo) { + TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); + test.testTakeAlicesSellOffer(testInfo); + test.testBobsConfirmPaymentStarted(testInfo); + test.testAlicesConfirmPaymentReceived(testInfo); + test.testBobsBtcWithdrawalToExternalAddress(testInfo); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java new file mode 100644 index 00000000000..ecd38dc2295 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -0,0 +1,99 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.MethodTest; +import bisq.apitest.method.WalletProtectionTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class WalletTest extends MethodTest { + + // All tests depend on the DAO / regtest environment, and Alice's wallet is + // initialized with 10 BTC during the scaffolding setup. + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, seednode, alicedaemon); + genBtcBlocksThenWait(1, 1500); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testFundWallet() { + // The regtest Bisq wallet was initialized with 10 BTC. + long balance = getBalance(alicedaemon); + assertEquals(1000000000, balance); + + String unusedAddress = getUnusedBtcAddress(alicedaemon); + bitcoinCli.sendToAddress(unusedAddress, "2.5"); + + bitcoinCli.generateBlocks(1); + sleep(1500); + + balance = getBalance(alicedaemon); + assertEquals(1250000000L, balance); // new balance is 12.5 btc + } + + @Test + @Order(2) + public void testWalletProtection() { + // Batching all wallet tests in this test case reduces scaffold setup + // time. Here, we create a method WalletProtectionTest instance and run each + // test in declared order. + + WalletProtectionTest walletProtectionTest = new WalletProtectionTest(); + + walletProtectionTest.testSetWalletPassword(); + walletProtectionTest.testGetBalanceOnEncryptedWalletShouldThrowException(); + walletProtectionTest.testUnlockWalletFor4Seconds(); + walletProtectionTest.testGetBalanceAfterUnlockTimeExpiryShouldThrowException(); + walletProtectionTest.testLockWalletBeforeUnlockTimeoutExpiry(); + walletProtectionTest.testLockWalletWhenWalletAlreadyLockedShouldThrowException(); + walletProtectionTest.testUnlockWalletTimeoutOverride(); + walletProtectionTest.testSetNewWalletPassword(); + walletProtectionTest.testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException(); + walletProtectionTest.testRemoveNewWalletPassword(); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/build.gradle b/build.gradle index 02551cc7193..1de42b984a8 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ configure(subprojects) { joptVersion = '5.0.4' jsonsimpleVersion = '1.1.1' junitVersion = '4.12' - jupiterVersion = '5.3.2' + jupiterVersion = '5.7.0' kotlinVersion = '1.3.41' knowmXchangeVersion = '4.4.2' langVersion = '3.8' @@ -643,8 +643,31 @@ configure(project(':apitest')) { test { useJUnitPlatform() + outputs.upToDateWhen { false } // Don't use previously cached test outputs. testLogging { - events "passed", "skipped", "failed" + showStackTraces = true // Show full stack traces in the console. + exceptionFormat = "full" + // Show passed & failed tests, and anything printed to stderr by the tests in the console. + // Do not show skipped tests in the console; they are shown in the html report. + events "passed", "failed", "standardError" + } + + afterSuite { desc, result -> + if (!desc.parent) { + println("${result.resultType} " + + "[${result.testCount} tests, " + + "${result.successfulTestCount} passed, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped] html report contains skipped test info") + + // Show report link if all tests passed in case you want to see more detail, stdout, skipped, etc. + if(result.resultType == TestResult.ResultType.SUCCESS) { + DirectoryReport htmlReport = getReports().getHtml() + String reportUrl = new org.gradle.internal.logging.ConsoleRenderer() + .asClickableFileUrl(htmlReport.getEntryPoint()) + println("REPORT " + reportUrl) + } + } } } @@ -674,12 +697,12 @@ configure(project(':apitest')) { compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" - testCompile "org.junit.jupiter:junit-jupiter-api:5.6.2" - testCompile "org.junit.jupiter:junit-jupiter-params:5.6.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" - testRuntime("org.junit.jupiter:junit-jupiter-engine:5.6.2") } } diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index cecaadf0d56..ec0e5e71bb6 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreateOfferRequest; @@ -74,6 +75,7 @@ public class CliMain { private enum Method { createoffer, + canceloffer, getoffer, getoffers, takeoffer, @@ -238,6 +240,18 @@ public static void run(String[] args) { out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode)); return; } + case canceloffer: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("incorrect parameter count, expecting offer id"); + + var offerId = nonOptionArgs.get(1); + var request = CancelOfferRequest.newBuilder() + .setId(offerId) + .build(); + offersService.cancelOffer(request); + out.println("offer canceled and removed from offer book"); + return; + } case getoffer: { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("incorrect parameter count, expecting offer id"); @@ -475,6 +489,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", ""); stream.format(rowFormat, "", "security deposit (%)", ""); + stream.format(rowFormat, "canceloffer", "offer id", "Cancel offer with id"); stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 798af169d40..e4d8f89c6c7 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -17,6 +17,8 @@ package bisq.cli; +import com.google.common.annotations.VisibleForTesting; + import java.text.DecimalFormat; import java.text.NumberFormat; @@ -27,15 +29,17 @@ import static java.lang.String.format; -class CurrencyFormat { +@VisibleForTesting +public class CurrencyFormat { private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); + @VisibleForTesting @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - static String formatSatoshis(long sats) { + public static String formatSatoshis(long sats) { return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); } diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index e9868f336e4..8336fff9ba1 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -63,7 +63,7 @@ static String formatAddressBalanceTbl(List addressBalanceInf static String formatOfferTable(List offerInfo, String fiatCurrency) { - // Some column values might be longer than header, so we need to calculated them. + // Some column values might be longer than header, so we need to calculate them. int paymentMethodColWidth = getLengthOfLongestColumn( COL_HEADER_PAYMENT_METHOD.length(), offerInfo.stream() @@ -100,7 +100,7 @@ static String formatOfferTable(List offerInfo, String fiatCurrency) { } static String formatPaymentAcctTbl(List paymentAccounts) { - // Some column values might be longer than header, so we need to calculated them. + // Some column values might be longer than header, so we need to calculate them. int nameColWidth = getLengthOfLongestColumn( COL_HEADER_NAME.length(), paymentAccounts.stream().map(PaymentAccount::getAccountName) diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java index ab9075fe341..2a28c1dccc4 100644 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -33,7 +33,7 @@ public class TradeFormat { @VisibleForTesting public static String format(TradeInfo tradeInfo) { - // Some column values might be longer than header, so we need to calculated them. + // Some column values might be longer than header, so we need to calculate them. int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length()); int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length()); diff --git a/common/src/main/java/bisq/common/persistence/PersistenceManager.java b/common/src/main/java/bisq/common/persistence/PersistenceManager.java index 6cdf98c9661..c377c69a478 100644 --- a/common/src/main/java/bisq/common/persistence/PersistenceManager.java +++ b/common/src/main/java/bisq/common/persistence/PersistenceManager.java @@ -45,6 +45,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -213,14 +214,42 @@ public void shutdown() { // Reading file /////////////////////////////////////////////////////////////////////////////////////////// + /** + * Read persisted file in a thread. + * + * @param resultHandler Consumer of persisted data once it was read from disk. + * @param orElse Called if no file exists or reading of file failed. + */ + public void readPersisted(Consumer resultHandler, Runnable orElse) { + readPersisted(checkNotNull(fileName), resultHandler, orElse); + } + + /** + * Read persisted file in a thread. + * We map result handler calls to UserThread, so clients don't need to worry about threading + * + * @param fileName File name of our persisted data. + * @param resultHandler Consumer of persisted data once it was read from disk. + * @param orElse Called if no file exists or reading of file failed. + */ + public void readPersisted(String fileName, Consumer resultHandler, Runnable orElse) { + new Thread(() -> { + T persisted = getPersisted(fileName); + if (persisted != null) { + UserThread.execute(() -> resultHandler.accept(persisted)); + } else { + UserThread.execute(orElse); + } + }, "PersistenceManager-read-" + fileName).start(); + } + + // API for synchronous reading of data. Not recommended to be used in application code. + // Currently used by tests and monitor. Should be converted to the threaded API as well. @Nullable public T getPersisted() { return getPersisted(checkNotNull(fileName)); } - //TODO use threading here instead in the clients - // We get called at startup either by readAllPersisted or readFromResources. Both are wrapped in a thread so we - // are not on the user thread. @Nullable public T getPersisted(String fileName) { File storageFile = new File(dir, fileName); diff --git a/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java b/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java index f2e53cf19f1..0aca05732d8 100644 --- a/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java +++ b/common/src/main/java/bisq/common/proto/persistable/PersistedDataHost.java @@ -17,12 +17,6 @@ package bisq.common.proto.persistable; -import java.util.List; - public interface PersistedDataHost { - void readPersisted(); - - static void apply(List persistedDataHosts) { - persistedDataHosts.forEach(PersistedDataHost::readPersisted); - } + void readPersisted(Runnable completeHandler); } diff --git a/common/src/main/java/bisq/common/setup/CommonSetup.java b/common/src/main/java/bisq/common/setup/CommonSetup.java index f2fac170fce..5562787ff78 100644 --- a/common/src/main/java/bisq/common/setup/CommonSetup.java +++ b/common/src/main/java/bisq/common/setup/CommonSetup.java @@ -102,11 +102,13 @@ protected static void setSystemProperties() { protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) { Signal.handle(new Signal("INT"), signal -> { + log.info("Received {}", signal); UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { })); }); Signal.handle(new Signal("TERM"), signal -> { + log.info("Received {}", signal); UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { })); }); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 001bf4773bc..7cfcc5ce152 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -142,6 +142,10 @@ public Offer editOffer(String offerId, paymentAccount); } + public void cancelOffer(String id) { + coreOffersService.cancelOffer(id); + } + /////////////////////////////////////////////////////////////////////////////////////////// // PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index da07677f1b2..6d8641c0579 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -160,6 +160,16 @@ Offer editOffer(String offerId, paymentAccount); } + void cancelOffer(String id) { + Offer offer = getOffer(id); + openOfferManager.removeOffer(offer, + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + }); + } + private void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 88ab0128cd2..dbc6927f452 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -17,36 +17,58 @@ package bisq.core.api; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.offer.takeoffer.TakeOfferModel; +import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.TradeUtil; +import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.protocol.BuyerProtocol; import bisq.core.trade.protocol.SellerProtocol; import bisq.core.user.User; +import bisq.core.util.validation.BtcAddressValidator; + +import org.bitcoinj.core.Coin; import javax.inject.Inject; +import java.util.Optional; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; +import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT; import static java.lang.String.format; @Slf4j class CoreTradesService { + // Dependencies on core api services in this package must be kept to an absolute + // minimum, but some trading functions require an unlocked wallet's key, so an + // exception is made in this case. + private final CoreWalletsService coreWalletsService; + + private final BtcWalletService btcWalletService; + private final ClosedTradableManager closedTradableManager; private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; private final TradeUtil tradeUtil; private final User user; @Inject - public CoreTradesService(TakeOfferModel takeOfferModel, + public CoreTradesService(CoreWalletsService coreWalletsService, + BtcWalletService btcWalletService, + ClosedTradableManager closedTradableManager, + TakeOfferModel takeOfferModel, TradeManager tradeManager, TradeUtil tradeUtil, User user) { + this.coreWalletsService = coreWalletsService; + this.btcWalletService = btcWalletService; + this.closedTradableManager = closedTradableManager; this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; this.tradeUtil = tradeUtil; @@ -116,14 +138,50 @@ void confirmPaymentReceived(String tradeId) { } } - @SuppressWarnings("unused") void keepFunds(String tradeId) { - log.info("TODO"); + verifyTradeIsNotClosed(tradeId); + var trade = getOpenTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + log.info("Keeping funds received from trade {}", tradeId); + tradeManager.onTradeCompleted(trade); } - @SuppressWarnings("unused") - void withdrawFunds(String tradeId, String address) { - log.info("TODO"); + void withdrawFunds(String tradeId, String toAddress) { + // An encrypted wallet must be unlocked for this operation. + verifyTradeIsNotClosed(tradeId); + var trade = getOpenTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + + verifyIsValidBTCAddress(toAddress); + + var fromAddressEntry = btcWalletService.getOrCreateAddressEntry(trade.getId(), TRADE_PAYOUT); + verifyFundsNotWithdrawn(fromAddressEntry); + + var amount = trade.getPayoutAmount(); + var fee = getEstimatedTxFee(fromAddressEntry.getAddressString(), toAddress, amount); + var receiverAmount = amount.subtract(fee); + + log.info(format("Withdrawing funds received from trade %s:" + + "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s", + tradeId, + fromAddressEntry.getAddressString(), + toAddress, + amount.toFriendlyString(), + fee.toFriendlyString(), + receiverAmount.toFriendlyString())); + + tradeManager.onWithdrawRequest( + toAddress, + amount, + fee, + coreWalletsService.getKey(), + trade, + () -> { + }, + (errorMessage, throwable) -> { + log.error(errorMessage, throwable); + throw new IllegalStateException(errorMessage, throwable); + }); } String getTradeRole(String tradeId) { @@ -131,11 +189,59 @@ String getTradeRole(String tradeId) { } Trade getTrade(String tradeId) { - return tradeManager.getTradeById(tradeId).orElseThrow(() -> - new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + return getOpenTrade(tradeId).orElseGet(() -> + getClosedTrade(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId)) + )); + } + + private Optional getOpenTrade(String tradeId) { + return tradeManager.getTradeById(tradeId); + } + + private Optional getClosedTrade(String tradeId) { + Optional tradable = closedTradableManager.getTradableById(tradeId); + return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value); } private boolean isFollowingBuyerProtocol(Trade trade) { return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; } + + private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) { + // TODO This and identical logic should be refactored into TradeUtil. + try { + return btcWalletService.getFeeEstimationTransaction(fromAddress, + toAddress, + amount, + TRADE_PAYOUT).getFee(); + } catch (Exception ex) { + log.error("", ex); + throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage())); + } + } + + // Throws a RuntimeException trade is already closed. + private void verifyTradeIsNotClosed(String tradeId) { + if (getClosedTrade(tradeId).isPresent()) + throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId)); + } + + // Throws a RuntimeException if address is not valid. + private void verifyIsValidBTCAddress(String address) { + try { + new BtcAddressValidator().validate(address); + } catch (Throwable t) { + log.error("", t); + throw new IllegalArgumentException(format("'%s' is not a valid btc address", address)); + } + } + + // Throws a RuntimeException if address has a zero balance. + private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) { + Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress()); + if (fromAddressBalance.isZero()) + throw new IllegalStateException(format("funds already withdrawn from address '%s'", + fromAddressEntry.getAddressString())); + } } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 0f1e1db0844..fc15ec5062a 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -23,6 +23,9 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; +import bisq.common.Timer; +import bisq.common.UserThread; + import org.bitcoinj.core.Address; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.crypto.KeyCrypterScrypt; @@ -37,8 +40,6 @@ import java.util.List; import java.util.Optional; -import java.util.Timer; -import java.util.TimerTask; import java.util.function.Function; import java.util.stream.Collectors; @@ -57,7 +58,7 @@ class CoreWalletsService { private final BtcWalletService btcWalletService; @Nullable - private TimerTask lockTask; + private Timer lockTimer; @Nullable private KeyParameter tempAesKey; @@ -71,6 +72,12 @@ public CoreWalletsService(Balances balances, this.btcWalletService = btcWalletService; } + @Nullable + KeyParameter getKey() { + verifyEncryptedWalletIsUnlocked(); + return tempAesKey; + } + long getAvailableBalance() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); @@ -184,29 +191,22 @@ void unlockWallet(String password, long timeout) { if (!walletsManager.checkAESKey(tempAesKey)) throw new IllegalStateException("incorrect password"); - if (lockTask != null) { - // The user is overriding a prior unlock timeout. Cancel the existing - // lock TimerTask to prevent it from calling lockWallet() before or after the - // new timer task does. - lockTask.cancel(); - // Avoid the synchronized(lock) overhead of an unnecessary lockTask.cancel() - // call the next time 'unlockwallet' is called. - lockTask = null; + if (lockTimer != null) { + // The user has called unlockwallet again, before the prior unlockwallet + // timeout has expired. He's overriding it with a new timeout value. + // Remove the existing lock timer to prevent it from calling lockwallet + // before or after the new one does. + lockTimer.stop(); + lockTimer = null; } - lockTask = new TimerTask() { - @Override - public void run() { - if (tempAesKey != null) { - // Do not try to lock wallet after timeout if the user has already - // done so via 'lockwallet' - log.info("Locking wallet after {} second timeout expired.", timeout); - tempAesKey = null; - } + lockTimer = UserThread.runAfter(() -> { + if (tempAesKey != null) { + // The unlockwallet timeout has expired; re-lock the wallet. + log.info("Locking wallet after {} second timeout expired.", timeout); + tempAesKey = null; } - }; - Timer timer = new Timer("Lock Wallet Timer"); - timer.schedule(lockTask, SECONDS.toMillis(timeout)); + }, timeout, SECONDS); } // Provided for automated wallet protection method testing, despite the diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 66c03dede43..b96ca3c865c 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -168,15 +168,12 @@ protected void readAllPersisted(@Nullable List additionalHost } AtomicInteger remaining = new AtomicInteger(hosts.size()); - hosts.forEach(e -> { - new Thread(() -> { - e.readPersisted(); - remaining.decrementAndGet(); - if (remaining.get() == 0) { + hosts.forEach(host -> { + host.readPersisted(() -> { + if (remaining.decrementAndGet() == 0) { UserThread.execute(completeHandler); } - - }, "BisqExecutable-read-" + e.getClass().getSimpleName()).start(); + }); }); } diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 0f608b1a99e..3325e37453c 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -316,11 +316,9 @@ private void maybeShowTac(Runnable nextStep) { } } - private void readMapsFromResources(Runnable nextStep) { - SetupUtils.readFromResources(p2PService.getP2PDataStorage(), config).addListener((observable, oldValue, newValue) -> { - if (newValue) - nextStep.run(); - }); + private void readMapsFromResources(Runnable completeHandler) { + String postFix = "_" + config.baseCurrencyNetwork.name(); + p2PService.getP2PDataStorage().readFromResources(postFix, completeHandler); } private void startP2pNetworkAndWallet(Runnable nextStep) { diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index 614ba083c76..dcbd8491021 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -45,6 +45,8 @@ import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.core.user.User; @@ -76,6 +78,8 @@ public class DomainInitialisation { private final RefundManager refundManager; private final TraderChatManager traderChatManager; private final TradeManager tradeManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; private final XmrTxProofService xmrTxProofService; private final OpenOfferManager openOfferManager; private final Balances balances; @@ -109,6 +113,8 @@ public DomainInitialisation(ClockWatcher clockWatcher, RefundManager refundManager, TraderChatManager traderChatManager, TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager, XmrTxProofService xmrTxProofService, OpenOfferManager openOfferManager, Balances balances, @@ -140,6 +146,8 @@ public DomainInitialisation(ClockWatcher clockWatcher, this.refundManager = refundManager; this.traderChatManager = traderChatManager; this.tradeManager = tradeManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; this.xmrTxProofService = xmrTxProofService; this.openOfferManager = openOfferManager; this.balances = balances; @@ -183,6 +191,8 @@ public void initDomainServices(Consumer rejectedTxErrorMessageHandler, traderChatManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); + closedTradableManager.onAllServicesInitialized(); + failedTradesManager.onAllServicesInitialized(); xmrTxProofService.onAllServicesInitialized(); openOfferManager.onAllServicesInitialized(); diff --git a/core/src/main/java/bisq/core/app/SetupUtils.java b/core/src/main/java/bisq/core/app/SetupUtils.java deleted file mode 100644 index eb210da5fea..00000000000 --- a/core/src/main/java/bisq/core/app/SetupUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.core.app; - -import bisq.network.p2p.storage.P2PDataStorage; - -import bisq.common.UserThread; -import bisq.common.config.BaseCurrencyNetwork; -import bisq.common.config.Config; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; - -import java.util.Date; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class SetupUtils { - - public static BooleanProperty readFromResources(P2PDataStorage p2PDataStorage, Config config) { - BooleanProperty result = new SimpleBooleanProperty(); - new Thread(() -> { - // Used to load different files per base currency (EntryMap_BTC_MAINNET, EntryMap_LTC,...) - final BaseCurrencyNetwork baseCurrencyNetwork = config.baseCurrencyNetwork; - final String postFix = "_" + baseCurrencyNetwork.name(); - long ts = new Date().getTime(); - p2PDataStorage.readFromResources(postFix); - log.info("readFromResources took {} ms", (new Date().getTime() - ts)); - UserThread.execute(() -> result.set(true)); - }, "BisqSetup-readFromResources").start(); - return result; - } -} diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java index bad57eba85a..952d63e3b8d 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java @@ -19,8 +19,6 @@ import bisq.core.account.sign.SignedWitnessService; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.app.SetupUtils; -import bisq.core.app.TorSetup; import bisq.core.filter.FilterManager; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -29,6 +27,8 @@ import bisq.network.p2p.network.CloseConnectionReason; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.ConnectionListener; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; import bisq.common.config.Config; import bisq.common.proto.persistable.PersistedDataHost; @@ -48,37 +48,42 @@ public class AppSetupWithP2P extends AppSetup { protected final AccountAgeWitnessService accountAgeWitnessService; private final SignedWitnessService signedWitnessService; protected final FilterManager filterManager; - private final TorSetup torSetup; - protected BooleanProperty p2pNetWorkReady; + private final P2PDataStorage p2PDataStorage; + private final PeerManager peerManager; protected final TradeStatisticsManager tradeStatisticsManager; protected ArrayList persistedDataHosts; + protected BooleanProperty p2pNetWorkReady; @Inject public AppSetupWithP2P(P2PService p2PService, + P2PDataStorage p2PDataStorage, + PeerManager peerManager, TradeStatisticsManager tradeStatisticsManager, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService, FilterManager filterManager, - TorSetup torSetup, Config config) { super(config); this.p2PService = p2PService; + this.p2PDataStorage = p2PDataStorage; + this.peerManager = peerManager; this.tradeStatisticsManager = tradeStatisticsManager; this.accountAgeWitnessService = accountAgeWitnessService; this.signedWitnessService = signedWitnessService; this.filterManager = filterManager; - this.torSetup = torSetup; this.persistedDataHosts = new ArrayList<>(); } @Override public void initPersistedDataHosts() { - persistedDataHosts.add(p2PService); + persistedDataHosts.add(p2PDataStorage); + persistedDataHosts.add(peerManager); // we apply at startup the reading of persisted data but don't want to get it triggered in the constructor persistedDataHosts.forEach(e -> { try { - e.readPersisted(); + e.readPersisted(() -> { + }); } catch (Throwable e1) { log.error("readPersisted error", e1); } @@ -87,10 +92,8 @@ public void initPersistedDataHosts() { @Override protected void initBasicServices() { - SetupUtils.readFromResources(p2PService.getP2PDataStorage(), config).addListener((observable, oldValue, newValue) -> { - if (newValue) - startInitP2PNetwork(); - }); + String postFix = "_" + config.baseCurrencyNetwork.name(); + p2PDataStorage.readFromResources(postFix, this::startInitP2PNetwork); } private void startInitP2PNetwork() { diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java index 3331906c8dc..95751832c9d 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -19,7 +19,6 @@ import bisq.core.account.sign.SignedWitnessService; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.app.TorSetup; import bisq.core.dao.DaoSetup; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; @@ -31,6 +30,8 @@ import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.network.p2p.P2PService; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; import bisq.common.config.Config; @@ -44,6 +45,8 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P { @Inject public AppSetupWithP2PAndDAO(P2PService p2PService, + P2PDataStorage p2PDataStorage, + PeerManager peerManager, TradeStatisticsManager tradeStatisticsManager, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService, @@ -55,14 +58,14 @@ public AppSetupWithP2PAndDAO(P2PService p2PService, MyProposalListService myProposalListService, MyReputationListService myReputationListService, MyProofOfBurnListService myProofOfBurnListService, - TorSetup torSetup, Config config) { super(p2PService, + p2PDataStorage, + peerManager, tradeStatisticsManager, accountAgeWitnessService, signedWitnessService, filterManager, - torSetup, config); this.daoSetup = daoSetup; diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index c88ebc40fbd..ea25be37756 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -63,12 +63,13 @@ public AddressEntryList(PersistenceManager persistenceManager) } @Override - public void readPersisted() { - AddressEntryList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - entrySet.clear(); - entrySet.addAll(persisted.entrySet); - } + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + entrySet.clear(); + entrySet.addAll(persisted.entrySet); + completeHandler.run(); + }, + completeHandler); } @@ -110,12 +111,12 @@ public void onWalletReady(Wallet wallet) { Set toBeRemoved = new HashSet<>(); entrySet.forEach(addressEntry -> { Script.ScriptType scriptType = addressEntry.isSegwit() ? Script.ScriptType.P2WPKH - : Script.ScriptType.P2PKH; + : Script.ScriptType.P2PKH; DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash( - addressEntry.getPubKeyHash(), scriptType); + addressEntry.getPubKeyHash(), scriptType); if (keyFromPubHash != null) { Address addressFromKey = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash, - scriptType); + scriptType); // We want to ensure key and address matches in case we have address in entry available already if (addressEntry.getAddress() == null || addressFromKey.equals(addressEntry.getAddress())) { addressEntry.setDeterministicKey(keyFromPubHash); @@ -197,8 +198,8 @@ public void addAddressEntry(AddressEntry addressEntry) { public void swapToAvailable(AddressEntry addressEntry) { boolean setChangedByRemove = entrySet.remove(addressEntry); boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), - AddressEntry.Context.AVAILABLE, - addressEntry.isSegwit())); + AddressEntry.Context.AVAILABLE, + addressEntry.isSegwit())); if (setChangedByRemove || setChangedByAdd) { requestPersistence(); } @@ -234,7 +235,7 @@ private void maybeAddNewAddressEntry(Transaction tx) { .map(address -> Pair.of(address, (DeterministicKey) wallet.findKeyFromAddress(address))) .filter(pair -> pair.getRight() != null) .map(pair -> new AddressEntry(pair.getRight(), AddressEntry.Context.AVAILABLE, - pair.getLeft() instanceof SegwitAddress)) + pair.getLeft() instanceof SegwitAddress)) .forEach(this::addAddressEntry); } diff --git a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java index cb01694fdf6..9662bdd9b17 100644 --- a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java +++ b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListService.java @@ -108,7 +108,7 @@ private boolean isNewProposal(Proposal proposal) { private void registerProposalAsBallot(Proposal proposal) { Ballot ballot = new Ballot(proposal); // vote is null if (log.isInfoEnabled()) { - log.info("We create a new ballot with a proposal and add it to our list. " + + log.debug("We create a new ballot with a proposal and add it to our list. " + "Vote is null at that moment. proposalTxId={}", proposal.getTxId()); } if (ballotList.contains(ballot)) { @@ -129,13 +129,16 @@ public void start() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - BallotList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - ballotList.setAll(persisted.getList()); - listeners.forEach(l -> l.onListChanged(ballotList.getList())); - } + persistenceManager.readPersisted(persisted -> { + ballotList.setAll(persisted.getList()); + listeners.forEach(l -> l.onListChanged(ballotList.getList())); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java index cc70c4a002e..cee5d1b9174 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -162,12 +162,15 @@ public void start() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - MyBlindVoteList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - myBlindVoteList.setAll(persisted.getList()); - } + persistenceManager.readPersisted(persisted -> { + myBlindVoteList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java index c417b9c61ce..5995a3be356 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyReputationListService.java @@ -55,12 +55,15 @@ public MyReputationListService(PersistenceManager persistenceM /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - MyReputationList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - myReputationList.setAll(persisted.getList()); - } + persistenceManager.readPersisted(persisted -> { + myReputationList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java index 390ee4ee608..ecd3faf1628 100644 --- a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteListService.java @@ -69,12 +69,15 @@ public MyVoteListService(DaoStateService daoStateService, /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - MyVoteList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - this.myVoteList.setAll(persisted.getList()); - } + persistenceManager.readPersisted(persisted -> { + myVoteList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } @@ -97,7 +100,8 @@ public void applyRevealTxId(MyVote myVote, String voteRevealTxId) { requestPersistence(); } - public Tuple2 getMeritAndStakeForProposal(String proposalTxId, MyBlindVoteListService myBlindVoteListService) { + public Tuple2 getMeritAndStakeForProposal(String proposalTxId, + MyBlindVoteListService myBlindVoteListService) { long merit = 0; long stake = 0; List list = new ArrayList<>(myVoteList.getList()); diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java index 39d8ed5a525..d61c6a93be3 100644 --- a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java @@ -55,12 +55,15 @@ public MyProofOfBurnListService(PersistenceManager persistenc /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - MyProofOfBurnList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - myProofOfBurnList.setAll(persisted.getList()); - } + persistenceManager.readPersisted(persisted -> { + myProofOfBurnList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java index 19284a0c042..96ab94ed66a 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java @@ -107,13 +107,16 @@ public MyProposalListService(P2PService p2PService, /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { if (DevEnv.isDaoActivated()) { - MyProposalList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - myProposalList.setAll(persisted.getList()); - listeners.forEach(l -> l.onListChanged(getList())); - } + persistenceManager.readPersisted(persisted -> { + myProposalList.setAll(persisted.getList()); + listeners.forEach(l -> l.onListChanged(getList())); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java index d39b6f5470d..b8af27c6726 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalStorageService.java @@ -73,9 +73,9 @@ public boolean canHandle(ProtectedStorageEntry entry) { } @Override - protected void readFromResources(String postFix) { + protected void readFromResources(String postFix, Runnable completeHandler) { // We do not have a resource file for that store, so we just call the readStore method instead. - readStore(); + readStore(persisted -> completeHandler.run()); } diff --git a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java index 467800748ff..4436c2bcb83 100644 --- a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java +++ b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java @@ -55,12 +55,15 @@ public UnconfirmedBsqChangeOutputListService(PersistenceManager { + unconfirmedBsqChangeOutputList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); + } else { + completeHandler.run(); } } diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index a22ecda78af..41bc9521ae8 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -48,6 +48,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final List bannedOfferIds; private final List bannedNodeAddress; + private final List bannedAutoConfExplorers; private final List bannedPaymentAccounts; private final List bannedCurrencies; private final List bannedPaymentMethods; @@ -115,7 +116,8 @@ static Filter cloneWithSig(Filter filter, String signatureAsBase64) { signatureAsBase64, filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), - filter.isDisableAutoConf()); + filter.isDisableAutoConf(), + filter.getBannedAutoConfExplorers()); } // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again @@ -143,7 +145,8 @@ static Filter cloneWithoutSig(Filter filter) { null, filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), - filter.isDisableAutoConf()); + filter.isDisableAutoConf(), + filter.getBannedAutoConfExplorers()); } public Filter(List bannedOfferIds, @@ -166,7 +169,8 @@ public Filter(List bannedOfferIds, PublicKey ownerPubKey, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, - boolean disableAutoConf) { + boolean disableAutoConf, + List bannedAutoConfExplorers) { this(bannedOfferIds, bannedNodeAddress, bannedPaymentAccounts, @@ -190,7 +194,8 @@ public Filter(List bannedOfferIds, null, signerPubKeyAsHex, bannedPrivilegedDevPubKeys, - disableAutoConf); + disableAutoConf, + bannedAutoConfExplorers); } @@ -222,7 +227,8 @@ public Filter(List bannedOfferIds, @Nullable String signatureAsBase64, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, - boolean disableAutoConf) { + boolean disableAutoConf, + List bannedAutoConfExplorers) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -247,6 +253,7 @@ public Filter(List bannedOfferIds, this.signerPubKeyAsHex = signerPubKeyAsHex; this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; this.disableAutoConf = disableAutoConf; + this.bannedAutoConfExplorers = bannedAutoConfExplorers; // ownerPubKeyBytes can be null when called from tests if (ownerPubKeyBytes != null) { @@ -283,7 +290,8 @@ public protobuf.StoragePayload toProtoMessage() { .setSignerPubKeyAsHex(signerPubKeyAsHex) .setCreationDate(creationDate) .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys) - .setDisableAutoConf(disableAutoConf); + .setDisableAutoConf(disableAutoConf) + .addAllBannedAutoConfExplorers(bannedAutoConfExplorers); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); @@ -320,7 +328,8 @@ public static Filter fromProto(protobuf.Filter proto) { proto.getSignatureAsBase64(), proto.getSignerPubKeyAsHex(), ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()), - proto.getDisableAutoConf() + proto.getDisableAutoConf(), + ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()) ); } @@ -361,6 +370,7 @@ public String toString() { ",\n creationDate=" + creationDate + ",\n extraDataMap=" + extraDataMap + ",\n disableAutoConf=" + disableAutoConf + + ",\n bannedAutoConfExplorers=" + bannedAutoConfExplorers + "\n}"; } } diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 1c8edc478f4..19cfded31fd 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -398,6 +398,12 @@ public boolean isNodeAddressBanned(NodeAddress nodeAddress) { .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); } + public boolean isAutoConfExplorerBanned(String address) { + return getFilter() != null && + getFilter().getBannedAutoConfExplorers().stream() + .anyMatch(e -> e.equals(address)); + } + public boolean requireUpdateToNewVersionForTrading() { if (getFilter() == null) { return false; @@ -460,7 +466,7 @@ private void onFilterAddedFromNetwork(Filter newFilter) { Filter currentFilter = getFilter(); if (!isFilterPublicKeyInList(newFilter)) { - log.warn("isFilterPublicKeyInList failed. Filter={}", newFilter); + log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex()); return; } if (!isSignatureValid(newFilter)) { diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 1e72532a612..c06b6d11fd2 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -166,16 +166,13 @@ public OpenOfferManager(CreateOfferService createOfferService, } @Override - public void readPersisted() { - TradableList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - openOffers.setAll(persisted.getList()); - } - - openOffers.forEach(e -> { - Offer offer = e.getOffer(); - offer.setPriceFeedService(priceFeedService); - }); + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + openOffers.setAll(persisted.getList()); + openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); + completeHandler.run(); + }, + completeHandler); } public void onAllServicesInitialized() { diff --git a/core/src/main/java/bisq/core/payment/AustraliaPayid.java b/core/src/main/java/bisq/core/payment/AustraliaPayid.java new file mode 100644 index 00000000000..a64c62b42ac --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AustraliaPayid.java @@ -0,0 +1,53 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.payment; + +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +public final class AustraliaPayid extends PaymentAccount { + public AustraliaPayid() { + super(PaymentMethod.AUSTRALIA_PAYID); + setSingleTradeCurrency(new FiatCurrency("AUD")); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AustraliaPayidPayload(paymentMethod.getId(), id); + } + + public String getPayid() { + return ((AustraliaPayidPayload) paymentAccountPayload).getPayid(); + } + + public void setPayid(String payid) { + if (payid == null) payid = ""; + ((AustraliaPayidPayload) paymentAccountPayload).setPayid(payid); + } + + public String getBankAccountName() { + return ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName(); + } + + public void setBankAccountName(String bankAccountName) { + if (bankAccountName == null) bankAccountName = ""; + ((AustraliaPayidPayload) paymentAccountPayload).setBankAccountName(bankAccountName); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java index 9cf7972f8a1..71c52a82711 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java @@ -46,6 +46,8 @@ public static PaymentAccount getPaymentAccount(PaymentMethod paymentMethod) { return new SpecificBanksAccount(); case PaymentMethod.JAPAN_BANK_ID: return new JapanBankAccount(); + case PaymentMethod.AUSTRALIA_PAYID_ID: + return new AustraliaPayid(); case PaymentMethod.ALI_PAY_ID: return new AliPayAccount(); case PaymentMethod.WECHAT_PAY_ID: diff --git a/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java new file mode 100644 index 00000000000..50539cd4c0a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AustraliaPayidPayload.java @@ -0,0 +1,113 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.payment.payload; + +import bisq.core.locale.Res; + +import bisq.common.util.CollectionUtils; + +import com.google.protobuf.Message; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class AustraliaPayidPayload extends PaymentAccountPayload { + private String payid = ""; + private String bankAccountName = ""; + + public AustraliaPayidPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private AustraliaPayidPayload(String paymentMethod, + String id, + String payid, + String bankAccountName, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.payid = payid; + this.bankAccountName = bankAccountName; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setAustraliaPayidPayload( + protobuf.AustraliaPayidPayload.newBuilder() + .setPayid(payid) + .setBankAccountName(bankAccountName) + ).build(); + } + + public static AustraliaPayidPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.AustraliaPayidPayload AustraliaPayidPayload = proto.getAustraliaPayidPayload(); + return new AustraliaPayidPayload(proto.getPaymentMethodId(), + proto.getId(), + AustraliaPayidPayload.getPayid(), + AustraliaPayidPayload.getBankAccountName(), + proto.getMaxTradePeriod(), + CollectionUtils.isEmpty(proto.getExcludeFromJsonDataMap()) ? null : new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); + } + + @Override + public String getPaymentDetailsForTradePopup() { + return + Res.get("payment.australia.payid") + ": " + payid + "\n" + + Res.get("payment.account.owner") + ": " + bankAccountName; + } + + + @Override + public byte[] getAgeWitnessInputData() { + String all = this.payid + this.bankAccountName; + return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java index a590b8f8557..f7520aa1a9b 100644 --- a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java @@ -74,6 +74,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable getPersistedDataHosts(Injector injector) { persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(RefundDisputeListService.class)); - persistedDataHosts.add(injector.getInstance(P2PService.class)); + persistedDataHosts.add(injector.getInstance(P2PDataStorage.class)); + persistedDataHosts.add(injector.getInstance(PeerManager.class)); if (injector.getInstance(Config.class).daoActivated) { persistedDataHosts.add(injector.getInstance(BallotListService.class)); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java index ffbec7dc985..d54b5e8b858 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java @@ -79,11 +79,12 @@ public DisputeListService(PersistenceManager persistenceManager) { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { - T persisted = persistenceManager.getPersisted(getFileName()); - if (persisted != null) { - disputeList.setAll(persisted.getList()); - } + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(getFileName(), persisted -> { + disputeList.setAll(persisted.getList()); + completeHandler.run(); + }, + completeHandler); } protected String getFileName() { diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index d8278018d56..fa1af34b555 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -200,19 +200,16 @@ public TradeManager(User user, /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { - TradableList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - tradableList.setAll(persisted.getList()); - } - - tradableList.forEach(trade -> { - Offer offer = trade.getOffer(); - if (offer != null) - offer.setPriceFeedService(priceFeedService); - }); - - dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(tradableList, "delayed_payout_txs_pending"); + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + tradableList.setAll(persisted.getList()); + tradableList.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(tradableList, "delayed_payout_txs_pending"); + completeHandler.run(); + }, + completeHandler); } diff --git a/core/src/main/java/bisq/core/trade/TradeUtil.java b/core/src/main/java/bisq/core/trade/TradeUtil.java index cd4fd074aad..a026f6ab982 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtil.java +++ b/core/src/main/java/bisq/core/trade/TradeUtil.java @@ -164,14 +164,13 @@ public String getMarketDescription(Trade trade) { } public String getPaymentMethodNameWithCountryCode(Trade trade) { - String paymentMethodDescription = ""; - if (trade != null) { - Offer offer = trade.getOffer(); - checkNotNull(offer); - checkNotNull(offer.getPaymentMethod()); - paymentMethodDescription = offer.getPaymentMethodNameWithCountryCode(); - } - return paymentMethodDescription; + if (trade == null) + return ""; + + Offer offer = trade.getOffer(); + checkNotNull(offer); + checkNotNull(offer.getPaymentMethod()); + return offer.getPaymentMethodNameWithCountryCode(); } /** diff --git a/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java b/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java new file mode 100644 index 00000000000..388b6701724 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java @@ -0,0 +1,129 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.trade.closed; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * Util for removing pending mailbox messages in case the trade has been closed by the seller after confirming receipt + * and a AckMessage as mailbox message will be sent by the buyer once they go online. In that case the seller's trade + * is closed already and the TradeProtocol is not executing the message processing, thus the mailbox message would not + * be removed. To ensure that in such cases (as well other potential cases in failure scenarios) the mailbox message + * gets removed from the network we use that util. + * + * This class must not be injected as a singleton! + */ +@Slf4j +public class CleanupMailboxMessages { + private final P2PService p2PService; + + @Inject + public CleanupMailboxMessages(P2PService p2PService) { + this.p2PService = p2PService; + } + + public void handleTrades(List trades) { + // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get + // a NullPointer and do not want that this escalate to the user. + try { + if (p2PService.isBootstrapped()) { + cleanupMailboxMessages(trades); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + cleanupMailboxMessages(trades); + } + }); + } + } catch (Throwable t) { + log.error("Cleanup mailbox messages failed. {}", t.toString()); + } + } + + private void cleanupMailboxMessages(List trades) { + p2PService.getMailboxItemsByUid().values() + .stream().map(P2PService.MailboxItem::getDecryptedMessageWithPubKey) + .forEach(message -> handleDecryptedMessageWithPubKey(message, trades)); + } + + private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, + List trades) { + trades.forEach(trade -> handleDecryptedMessageWithPubKey(decryptedMessageWithPubKey, trade)); + } + + private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, + Trade trade) { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (!isPubKeyValid(decryptedMessageWithPubKey, trade)) { + return; + } + + if (networkEnvelope instanceof TradeMessage && + isMyMessage((TradeMessage) networkEnvelope, trade)) { + removeEntryFromMailbox(decryptedMessageWithPubKey, trade); + } else if (networkEnvelope instanceof AckMessage && + isMyMessage((AckMessage) networkEnvelope, trade)) { + removeEntryFromMailbox(decryptedMessageWithPubKey, trade); + } + } + + private void removeEntryFromMailbox(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { + log.info("We found a pending mailbox message ({}) for trade {}. As the trade is closed we remove the mailbox message.", + decryptedMessageWithPubKey.getNetworkEnvelope().getClass().getSimpleName(), trade.getId()); + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } + + private boolean isMyMessage(TradeMessage message, Trade trade) { + return message.getTradeId().equals(trade.getId()); + } + + private boolean isMyMessage(AckMessage ackMessage, Trade trade) { + return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && + ackMessage.getSourceId().equals(trade.getId()); + } + + private boolean isPubKeyValid(DecryptedMessageWithPubKey message, Trade trade) { + // We can only validate the peers pubKey if we have it already. If we are the taker we get it from the offer + // Otherwise it depends on the state of the trade protocol if we have received the peers pubKeyRing already. + PubKeyRing peersPubKeyRing = trade.getProcessModel().getTradingPeer().getPubKeyRing(); + boolean isValid = true; + if (peersPubKeyRing != null && + !message.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) { + isValid = false; + log.error("SignaturePubKey in message does not match the SignaturePubKey we have set for our trading peer."); + } + return isValid; + } +} diff --git a/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java index 69567270cbf..11fa2714147 100644 --- a/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java +++ b/core/src/main/java/bisq/core/trade/closed/ClosedTradableManager.java @@ -39,20 +39,26 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class ClosedTradableManager implements PersistedDataHost { private final PersistenceManager> persistenceManager; private final TradableList closedTradables = new TradableList<>(); private final KeyRing keyRing; private final PriceFeedService priceFeedService; + private final CleanupMailboxMessages cleanupMailboxMessages; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; @Inject public ClosedTradableManager(KeyRing keyRing, PriceFeedService priceFeedService, PersistenceManager> persistenceManager, + CleanupMailboxMessages cleanupMailboxMessages, DumpDelayedPayoutTx dumpDelayedPayoutTx) { this.keyRing = keyRing; this.priceFeedService = priceFeedService; + this.cleanupMailboxMessages = cleanupMailboxMessages; this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; this.persistenceManager = persistenceManager; @@ -60,15 +66,20 @@ public ClosedTradableManager(KeyRing keyRing, } @Override - public void readPersisted() { - TradableList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - closedTradables.setAll(persisted.getList()); - } - - closedTradables.forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + closedTradables.setAll(persisted.getList()); + closedTradables.stream() + .filter(tradable -> tradable.getOffer() != null) + .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(closedTradables, "delayed_payout_txs_closed"); + completeHandler.run(); + }, + completeHandler); + } - dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(closedTradables, "delayed_payout_txs_closed"); + public void onAllServicesInitialized() { + cleanupMailboxMessages.handleTrades(getClosedTrades()); } public void add(Tradable tradable) { diff --git a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java index 4782ef080d2..69b2da21530 100644 --- a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java @@ -25,6 +25,7 @@ import bisq.core.trade.TradableList; import bisq.core.trade.Trade; import bisq.core.trade.TradeUtil; +import bisq.core.trade.closed.CleanupMailboxMessages; import bisq.common.crypto.KeyRing; import bisq.common.persistence.PersistenceManager; @@ -49,6 +50,7 @@ public class FailedTradesManager implements PersistedDataHost { private final KeyRing keyRing; private final PriceFeedService priceFeedService; private final BtcWalletService btcWalletService; + private final CleanupMailboxMessages cleanupMailboxMessages; private final PersistenceManager> persistenceManager; private final TradeUtil tradeUtil; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; @@ -61,10 +63,12 @@ public FailedTradesManager(KeyRing keyRing, BtcWalletService btcWalletService, PersistenceManager> persistenceManager, TradeUtil tradeUtil, + CleanupMailboxMessages cleanupMailboxMessages, DumpDelayedPayoutTx dumpDelayedPayoutTx) { this.keyRing = keyRing; this.priceFeedService = priceFeedService; this.btcWalletService = btcWalletService; + this.cleanupMailboxMessages = cleanupMailboxMessages; this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; this.persistenceManager = persistenceManager; this.tradeUtil = tradeUtil; @@ -73,19 +77,20 @@ public FailedTradesManager(KeyRing keyRing, } @Override - public void readPersisted() { - TradableList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - failedTrades.setAll(persisted.getList()); - } - - failedTrades.forEach(trade -> { - if (trade.getOffer() != null) { - trade.getOffer().setPriceFeedService(priceFeedService); - } - }); + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + failedTrades.setAll(persisted.getList()); + failedTrades.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(failedTrades, "delayed_payout_txs_failed"); + completeHandler.run(); + }, + completeHandler); + } - dumpDelayedPayoutTx.maybeDumpDelayedPayoutTxs(failedTrades, "delayed_payout_txs_failed"); + public void onAllServicesInitialized() { + cleanupMailboxMessages.handleTrades(failedTrades.getList()); } public void add(Trade trade) { diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java index 27024943903..29855fefe89 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java @@ -77,14 +77,13 @@ public Map getMap() { // seems to be not justified as it is needed only temporarily. @Override protected PersistableNetworkPayload putIfAbsent(P2PDataStorage.ByteArray hash, PersistableNetworkPayload payload) { - PersistableNetworkPayload previous = getMapOfAllData().putIfAbsent(hash, payload); - return previous; + return getMapOfAllData().putIfAbsent(hash, payload); } @Override - protected void readFromResources(String postFix) { + protected void readFromResources(String postFix, Runnable completeHandler) { // We do not attempt to read from resources as that file is not provided anymore - readStore(); + readStore(persisted -> completeHandler.run()); } public Map getMapOfAllData() { diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java index d7a1e84bc1f..2d0efe887d4 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java @@ -17,6 +17,7 @@ package bisq.core.trade.txproof.xmr; +import bisq.core.filter.FilterManager; import bisq.core.locale.Res; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.mediation.MediationManager; @@ -54,6 +55,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { private final Trade trade; private final AutoConfirmSettings autoConfirmSettings; private final MediationManager mediationManager; + private final FilterManager filterManager; private final RefundManager refundManager; private final Socks5ProxyProvider socks5ProxyProvider; @@ -74,11 +76,13 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { Trade trade, AutoConfirmSettings autoConfirmSettings, MediationManager mediationManager, + FilterManager filterManager, RefundManager refundManager) { this.socks5ProxyProvider = socks5ProxyProvider; this.trade = trade; this.autoConfirmSettings = autoConfirmSettings; this.mediationManager = mediationManager; + this.filterManager = filterManager; this.refundManager = refundManager; } @@ -140,6 +144,10 @@ public void requestFromAllServices(Consumer resultHandler, F numRequiredSuccessResults = serviceAddresses.size(); for (String serviceAddress : serviceAddresses) { + if (filterManager.isAutoConfExplorerBanned(serviceAddress)) { + log.warn("Filtered out auto-confirmation address: {}", serviceAddress); + continue; // #4683: filter for auto-confirm explorers + } XmrTxProofModel model = new XmrTxProofModel(trade, serviceAddress, autoConfirmSettings); XmrTxProofRequest request = new XmrTxProofRequest(socks5ProxyProvider, model); diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index 30349d3f784..9e9ca4c9087 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -257,6 +257,7 @@ private void startRequests(SellerTrade trade) { trade, autoConfirmSettings, mediationManager, + filterManager, refundManager); servicesByTradeId.put(trade.getId(), service); service.requestFromAllServices( diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 656f056522a..3845116acf6 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -168,8 +168,6 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings("WeakerAccess") @Inject public Preferences(PersistenceManager persistenceManager, Config config, @@ -225,54 +223,67 @@ public Preferences(PersistenceManager persistenceManager, } @Override - public void readPersisted() { - BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); - TradeCurrency preferredTradeCurrency; + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted("PreferencesPayload", + persisted -> { + initFromPersistedPreferences(persisted); + completeHandler.run(); + }, + () -> { + initNewPreferences(); + completeHandler.run(); + }); + } + + private void initFromPersistedPreferences(PreferencesPayload persisted) { + prefPayload = persisted; + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); + GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); + TradeCurrency preferredTradeCurrency = checkNotNull(prefPayload.getPreferredTradeCurrency(), "preferredTradeCurrency must not be null"); + setPreferredTradeCurrency(preferredTradeCurrency); + setFiatCurrencies(prefPayload.getFiatCurrencies()); + setCryptoCurrencies(prefPayload.getCryptoCurrencies()); + setBsqBlockChainExplorer(prefPayload.getBsqBlockChainExplorer()); + GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + setupPreferences(); + } - PreferencesPayload persisted = persistenceManager.getPersisted("PreferencesPayload"); - if (persisted != null) { - prefPayload = persisted; - GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); - GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); - preferredTradeCurrency = checkNotNull(prefPayload.getPreferredTradeCurrency(), "preferredTradeCurrency must not be null"); - setPreferredTradeCurrency(preferredTradeCurrency); - setFiatCurrencies(prefPayload.getFiatCurrencies()); - setCryptoCurrencies(prefPayload.getCryptoCurrencies()); - setBsqBlockChainExplorer(prefPayload.getBsqBlockChainExplorer()); - } else { - prefPayload = new PreferencesPayload(); - prefPayload.setUserLanguage(GlobalSettings.getLocale().getLanguage()); - prefPayload.setUserCountry(CountryUtil.getDefaultCountry()); - GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); - preferredTradeCurrency = checkNotNull(CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code), - "preferredTradeCurrency must not be null"); - prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); - setFiatCurrencies(CurrencyUtil.getMainFiatCurrencies()); - setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); + private void initNewPreferences() { + prefPayload = new PreferencesPayload(); + prefPayload.setUserLanguage(GlobalSettings.getLocale().getLanguage()); + prefPayload.setUserCountry(CountryUtil.getDefaultCountry()); + GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); + TradeCurrency preferredTradeCurrency = checkNotNull(CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code), + "preferredTradeCurrency must not be null"); + prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); + setFiatCurrencies(CurrencyUtil.getMainFiatCurrencies()); + setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); - if ("BTC".equals(baseCurrencyNetwork.getCurrencyCode())) { - setBlockChainExplorerMainNet(BTC_MAIN_NET_EXPLORERS.get(0)); - setBlockChainExplorerTestNet(BTC_TEST_NET_EXPLORERS.get(0)); - } else { - throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); - } + BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); + if ("BTC".equals(baseCurrencyNetwork.getCurrencyCode())) { + setBlockChainExplorerMainNet(BTC_MAIN_NET_EXPLORERS.get(0)); + setBlockChainExplorerTestNet(BTC_TEST_NET_EXPLORERS.get(0)); + } else { + throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); + } - prefPayload.setDirectoryChooserPath(Utilities.getSystemHomeDirectory()); + prefPayload.setDirectoryChooserPath(Utilities.getSystemHomeDirectory()); - prefPayload.setOfferBookChartScreenCurrencyCode(preferredTradeCurrency.getCode()); - prefPayload.setTradeChartsScreenCurrencyCode(preferredTradeCurrency.getCode()); - prefPayload.setBuyScreenCurrencyCode(preferredTradeCurrency.getCode()); - prefPayload.setSellScreenCurrencyCode(preferredTradeCurrency.getCode()); - } + prefPayload.setOfferBookChartScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setTradeChartsScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setBuyScreenCurrencyCode(preferredTradeCurrency.getCode()); + prefPayload.setSellScreenCurrencyCode(preferredTradeCurrency.getCode()); + GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); + setupPreferences(); + } + private void setupPreferences() { persistenceManager.initialize(prefPayload, PersistenceManager.Source.PRIVATE); // We don't want to pass Preferences to all popups where the don't show again checkbox is used, so we use // that static lookup class to avoid static access to the Preferences directly. DontShowAgainLookup.setPreferences(this); - GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); - // set all properties useAnimationsProperty.set(prefPayload.isUseAnimations()); useStandbyModeProperty.set(prefPayload.isUseStandbyMode()); @@ -280,12 +291,12 @@ public void readPersisted() { // if no valid Bitcoin block explorer is set, select the 1st valid Bitcoin block explorer ArrayList btcExplorers = getBlockChainExplorers(); - if (!blockExplorerExists(btcExplorers, getBlockChainExplorer())) + if (getBlockChainExplorer() == null || getBlockChainExplorer().name.length() == 0) setBlockChainExplorer(btcExplorers.get(0)); // if no valid BSQ block explorer is set, randomly select a valid BSQ block explorer ArrayList bsqExplorers = getBsqBlockChainExplorers(); - if (!blockExplorerExists(bsqExplorers, getBsqBlockChainExplorer())) + if (getBsqBlockChainExplorer() == null || getBsqBlockChainExplorer().name.length() == 0) setBsqBlockChainExplorer(bsqExplorers.get((new Random()).nextInt(bsqExplorers.size()))); tradeCurrenciesAsObservable.addAll(prefPayload.getFiatCurrencies()); @@ -336,6 +347,7 @@ public void readPersisted() { requestPersistence(); } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -416,6 +428,11 @@ public void setTacAcceptedV120(boolean tacAccepted) { requestPersistence(); } + public void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold) { + prefPayload.setBsqAverageTrimThreshold(bsqAverageTrimThreshold); + requestPersistence(); + } + public Optional findAutoConfirmSettings(String currencyCode) { return prefPayload.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals(currencyCode)) @@ -902,15 +919,6 @@ else if (change.wasRemoved() && change.getRemovedSize() == 1 && initialReadDone) requestPersistence(); } - private boolean blockExplorerExists(ArrayList explorers, - BlockChainExplorer explorer) { - if (explorer != null && explorers != null && explorers.size() > 0) - for (int i = 0; i < explorers.size(); i++) - if (explorers.get(i).name.equals(explorer.name)) - return true; - return false; - } - private interface ExcludesDelegateMethods { void setTacAccepted(boolean tacAccepted); @@ -1034,6 +1042,8 @@ private interface ExcludesDelegateMethods { void setTacAcceptedV120(boolean tacAccepted); + void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); + void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); } } diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index d84862e9132..9c919185df7 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -124,6 +124,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; + private double bsqAverageTrimThreshold = 0.05; // Added at 1.3.8 private List autoConfirmSettingsList = new ArrayList<>(); @@ -188,9 +189,10 @@ public Message toProtoMessage() { .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) + .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() - .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) - .collect(Collectors.toList())); + .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) + .collect(Collectors.toList())); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); @@ -280,6 +282,7 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co proto.getBuyerSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), + proto.getBsqAverageTrimThreshold(), proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index f101dc5f41a..2f8480e162d 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -89,13 +89,21 @@ public User() { } @Override - public void readPersisted() { - UserPayload persisted = checkNotNull(persistenceManager).getPersisted("UserPayload"); - if (persisted != null) { - userPayload = persisted; - } - - persistenceManager.initialize(userPayload, PersistenceManager.Source.PRIVATE); + public void readPersisted(Runnable completeHandler) { + checkNotNull(persistenceManager).readPersisted("UserPayload", + persisted -> { + userPayload = persisted; + init(); + completeHandler.run(); + }, + () -> { + init(); + completeHandler.run(); + }); + } + + private void init() { + checkNotNull(persistenceManager).initialize(userPayload, PersistenceManager.Source.PRIVATE); checkNotNull(userPayload.getPaymentAccounts(), "userPayload.getPaymentAccounts() must not be null"); checkNotNull(userPayload.getAcceptedLanguageLocaleCodes(), "userPayload.getAcceptedLanguageLocaleCodes() must not be null"); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ba26e7f6a30..d4d354cb9d3 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1184,6 +1184,7 @@ setting.preferences.general=General preferences setting.preferences.explorer=Bitcoin Explorer setting.preferences.explorer.bsq=Bisq Explorer setting.preferences.deviation=Max. deviation from market price +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate setting.preferences.avoidStandbyMode=Avoid standby mode setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled @@ -1237,6 +1238,14 @@ setting.preferences.dao.fullNodeInfo=For running Bisq as DAO full node you need After changing the mode you need to restart. setting.preferences.dao.fullNodeInfo.ok=Open docs page setting.preferences.dao.fullNodeInfo.cancel=No, I stick with lite node mode +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ + customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen = Chosen explorer settings +settings.preferences.editCustomExplorer.name=Name +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL settings.net.btcHeader=Bitcoin network settings.net.p2pHeader=Bisq network @@ -2594,6 +2603,7 @@ filterWindow.btcNode=Filtered Bitcoin nodes (comma sep. addresses + port) filterWindow.preventPublicBtcNetwork=Prevent usage of public Bitcoin network filterWindow.disableDao=Disable DAO filterWindow.disableAutoConf=Disable auto-confirm +filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableDaoBelowVersion=Min. version required for DAO filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=Add filter @@ -3321,6 +3331,11 @@ payment.japan.bank=Bank payment.japan.branch=Branch payment.japan.account=Account payment.japan.recipient=Name +payment.australia.payid=PayID +payment.payid=PayID linked to financial institution. Like email address or mobile phone. +payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your \ + bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. \ + Both sending and receiving financial institutions must support PayID. For more information please check https://payid.com.au/faqs/ # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -3335,6 +3350,7 @@ MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face to face (in person) JAPAN_BANK=Japan Bank Furikomi +AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=National banks @@ -3354,6 +3370,8 @@ WESTERN_UNION_SHORT=Western Union F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi +# suppress inspection "UnusedProperty" +AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" diff --git a/core/src/test/java/bisq/core/user/PreferencesTest.java b/core/src/test/java/bisq/core/user/PreferencesTest.java index 87d83bc3470..4a7acab39e2 100644 --- a/core/src/test/java/bisq/core/user/PreferencesTest.java +++ b/core/src/test/java/bisq/core/user/PreferencesTest.java @@ -94,11 +94,10 @@ public void testGetUniqueListOfFiatCurrencies() { when(payload.getPreferredTradeCurrency()).thenReturn(usd); when(payload.getFiatCurrencies()).thenReturn(fiatCurrencies); - preferences.readPersisted(); - - assertEquals(7, preferences.getFiatCurrenciesAsObservable().size()); - assertTrue(preferences.getFiatCurrenciesAsObservable().contains(usd)); - + preferences.readPersisted(() -> { + assertEquals(7, preferences.getFiatCurrenciesAsObservable().size()); + assertTrue(preferences.getFiatCurrenciesAsObservable().contains(usd)); + }); } @Test @@ -115,9 +114,9 @@ public void testGetUniqueListOfCryptoCurrencies() { when(payload.getPreferredTradeCurrency()).thenReturn(new FiatCurrency("USD")); when(payload.getCryptoCurrencies()).thenReturn(cryptoCurrencies); - preferences.readPersisted(); - - assertTrue(preferences.getCryptoCurrenciesAsObservable().contains(dash)); + preferences.readPersisted(() -> { + assertTrue(preferences.getCryptoCurrenciesAsObservable().contains(dash)); + }); } @Test @@ -136,9 +135,8 @@ public void testUpdateOfPersistedFiatCurrenciesAfterLocaleChanged() { when(payload.getPreferredTradeCurrency()).thenReturn(usd); when(payload.getFiatCurrencies()).thenReturn(fiatCurrencies); - preferences.readPersisted(); - - assertEquals("US Dollar (USD)", preferences.getFiatCurrenciesAsObservable().get(0).getNameAndCode()); + preferences.readPersisted(() -> { + assertEquals("US Dollar (USD)", preferences.getFiatCurrenciesAsObservable().get(0).getNameAndCode()); + }); } - } diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 6fc717b263c..144e299181c 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -63,7 +63,8 @@ public void testRoundtripFull() { null, null, null, - false)); + false, + Lists.newArrayList())); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index ce2f3f6f838..ca7b77bcffb 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -122,6 +122,7 @@ private static Filter filterWithReceivers(List btcFeeReceiverAddresses) null, null, null, - false); + false, + Lists.newArrayList()); } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index d7785935563..f03155d2f8d 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -21,6 +21,8 @@ import bisq.core.api.model.OfferInfo; import bisq.core.offer.Offer; +import bisq.proto.grpc.CancelOfferReply; +import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.CreateOfferReply; import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.GetOfferReply; @@ -114,4 +116,19 @@ public void createOffer(CreateOfferRequest req, throw ex; } } + + @Override + public void cancelOffer(CancelOfferRequest req, + StreamObserver responseObserver) { + try { + coreApi.cancelOffer(req.getId()); + var reply = CancelOfferReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } } diff --git a/desktop/src/main/java/bisq/desktop/Navigation.java b/desktop/src/main/java/bisq/desktop/Navigation.java index adb73000231..cb9da123d79 100644 --- a/desktop/src/main/java/bisq/desktop/Navigation.java +++ b/desktop/src/main/java/bisq/desktop/Navigation.java @@ -79,24 +79,26 @@ public Navigation(PersistenceManager persistenceManager) { } @Override - public void readPersisted() { - NavigationPath persisted = persistenceManager.getPersisted(); - if (persisted != null) { - List> viewClasses = persisted.getPath().stream() - .map(className -> { - try { - return (Class) Class.forName(className).asSubclass(View.class); - } catch (ClassNotFoundException e) { - log.warn("Could not find the viewPath class {}; exception: {}", className, e); - } - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - if (!viewClasses.isEmpty()) - previousPath = new ViewPath(viewClasses); - } + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + List> viewClasses = persisted.getPath().stream() + .map(className -> { + try { + return (Class) Class.forName(className).asSubclass(View.class); + } catch (ClassNotFoundException e) { + log.warn("Could not find the viewPath class {}; exception: {}", className, e); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!viewClasses.isEmpty()) { + previousPath = new ViewPath(viewClasses); + } + completeHandler.run(); + }, + completeHandler); } @SafeVarargs diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 1b213ab9008..a14d27712a0 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -1700,6 +1700,10 @@ textfield */ -fx-font-size: 0.880em; } +#price-chart .axis-tick-mark-text-node, #volume-chart .axis-tick-mark-text-node { + -fx-text-alignment: center; +} + #charts .chart-plot-background, #charts-dao .chart-plot-background { -fx-background-color: -bs-background-color; } @@ -2169,4 +2173,3 @@ textfield */ -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-transparent-dark) !important; -fx-fill: -bs-background-color !important; } - diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java new file mode 100644 index 00000000000..a1d6337ea84 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -0,0 +1,122 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.AustraliaPayidValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.AustraliaPayidPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +public class AustraliaPayidForm extends PaymentMethodForm { + private final AustraliaPayid australiaPayid; + private final AustraliaPayidValidator australiaPayidValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + ((AustraliaPayidPayload) paymentAccountPayload).getBankAccountName()); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + ((AustraliaPayidPayload) paymentAccountPayload).getPayid()); + return gridRow; + } + + public AustraliaPayidForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + AustraliaPayidValidator australiaPayidValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.australiaPayid = (AustraliaPayid) paymentAccount; + this.australiaPayidValidator = australiaPayidValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, + Res.get("payment.account.owner")); + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + australiaPayid.setBankAccountName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid")); + mobileNrInputTextField.setValidator(australiaPayidValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + australiaPayid.setPayid(newValue); + updateFromInputs(); + }); + + TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(mobileNrInputTextField.getText()); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + australiaPayid.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(australiaPayid.getPaymentMethod().getId())); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + australiaPayid.getPayid()); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), + australiaPayid.getBankAccountName()).second; + field.setMouseTransparent(false); + TradeCurrency singleTradeCurrency = australiaPayid.getSingleTradeCurrency(); + String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && australiaPayidValidator.validate(australiaPayid.getPayid()).isValid + && inputValidator.validate(australiaPayid.getBankAccountName()).isValid + && australiaPayid.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java index e111fb2dc73..7574a28371f 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java @@ -21,6 +21,7 @@ import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.paymentmethods.AdvancedCashForm; import bisq.desktop.components.paymentmethods.AliPayForm; +import bisq.desktop.components.paymentmethods.AustraliaPayidForm; import bisq.desktop.components.paymentmethods.CashDepositForm; import bisq.desktop.components.paymentmethods.ChaseQuickPayForm; import bisq.desktop.components.paymentmethods.ClearXchangeForm; @@ -54,6 +55,7 @@ import bisq.desktop.util.Layout; import bisq.desktop.util.validation.AdvancedCashValidator; import bisq.desktop.util.validation.AliPayValidator; +import bisq.desktop.util.validation.AustraliaPayidValidator; import bisq.desktop.util.validation.BICValidator; import bisq.desktop.util.validation.ChaseQuickPayValidator; import bisq.desktop.util.validation.ClearXchangeValidator; @@ -76,6 +78,7 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.offer.OfferRestrictions; +import bisq.core.payment.AustraliaPayid; import bisq.core.payment.CashDepositAccount; import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.F2FAccount; @@ -139,6 +142,7 @@ public class FiatAccountsView extends PaymentAccountsView doSaveNewAccount(paymentAccount)) .show(); + } else if (paymentAccount instanceof AustraliaPayid) { + new Popup().information(Res.get("payment.payid.info", currencyName, currencyName)) + .width(900) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iConfirm")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); } else { doSaveNewAccount(paymentAccount); } @@ -457,6 +470,8 @@ private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod, Paym return new SpecificBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.JAPAN_BANK_ID: return new JapanBankTransferForm(paymentAccount, accountAgeWitnessService, japanBankTransferValidator, inputValidator, root, gridRow, formatter); + case PaymentMethod.AUSTRALIA_PAYID_ID: + return new AustraliaPayidForm(paymentAccount, accountAgeWitnessService, australiapayidValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.ALI_PAY_ID: return new AliPayForm(paymentAccount, accountAgeWitnessService, aliPayValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.WECHAT_PAY_ID: diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 4e5bea571ca..a8f9677ad2b 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -20,6 +20,7 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TextFieldWithIcon; +import bisq.desktop.util.AxisInlierUtils; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateListener; @@ -112,8 +113,9 @@ public class BsqDashboardView extends ActivatableView implements private Label marketPriceLabel; private Coin availableAmount; - private int gridRow = 0; + double howManyStdDevsConstituteOutlier = 10; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -121,10 +123,10 @@ public class BsqDashboardView extends ActivatableView implements @Inject public BsqDashboardView(DaoFacade daoFacade, - TradeStatisticsManager tradeStatisticsManager, - PriceFeedService priceFeedService, - Preferences preferences, - BsqFormatter bsqFormatter) { + TradeStatisticsManager tradeStatisticsManager, + PriceFeedService priceFeedService, + Preferences preferences, + BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; this.tradeStatisticsManager = tradeStatisticsManager; this.priceFeedService = priceFeedService; @@ -134,7 +136,6 @@ public BsqDashboardView(DaoFacade daoFacade, @Override public void initialize() { - ADJUSTERS.put(DAY, TemporalAdjusters.ofDateAdjuster(d -> d)); createKPIs(); @@ -368,15 +369,24 @@ private void updateAveragePriceFields(TextField field90, TextFieldWithIcon field } private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) { + double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); Date pastXDays = getPastDate(days); - List bsqTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + List bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrency().equals("BSQ")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); - List usdTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + List bsqTradePastXDays = percentToTrim > 0 ? + removeOutliers(bsqAllTradePastXDays, percentToTrim) : + bsqAllTradePastXDays; + + List usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrency().equals("USD")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); + List usdTradePastXDays = percentToTrim > 0 ? + removeOutliers(usdAllTradePastXDays, percentToTrim) : + usdAllTradePastXDays; + long average = isUSDField ? getUSDAverage(bsqTradePastXDays, usdTradePastXDays) : getBTCAverage(bsqTradePastXDays); Price avgPrice = isUSDField ? Price.valueOf("USD", average) : @@ -390,11 +400,26 @@ private long updateAveragePriceField(TextField textField, int days, boolean isUS return average; } - private long getBTCAverage(List bsqList) { + private List removeOutliers(List list, double percentToTrim) { + List yValues = list.stream() + .filter(TradeStatistics3::isValid) + .map(e -> (double) e.getPrice()) + .collect(Collectors.toList()); + + Tuple2 tuple = AxisInlierUtils.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier); + double lowerBound = tuple.first; + double upperBound = tuple.second; + return list.stream() + .filter(e -> e.getPrice() > lowerBound) + .filter(e -> e.getPrice() < upperBound) + .collect(Collectors.toList()); + } + + private long getBTCAverage(List list) { long accumulatedVolume = 0; long accumulatedAmount = 0; - for (TradeStatistics3 item : bsqList) { + for (TradeStatistics3 item : list) { accumulatedVolume += item.getTradeVolume().getValue(); accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java index 9fd0b1bf072..c0862770124 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java @@ -49,7 +49,7 @@ //TODO merge with vote result ProposalListItem public class ProposalsListItem { - enum IconButtonTypes { + enum IconButtonType { REMOVE_PROPOSAL(Res.get("dao.proposal.table.icon.tooltip.removeProposal")), ACCEPT(Res.get("dao.proposal.display.myVote.accepted")), REJECT(Res.get("dao.proposal.display.myVote.rejected")), @@ -57,7 +57,7 @@ enum IconButtonTypes { @Getter private String title; - IconButtonTypes(String title) { + IconButtonType(String title) { this.title = title; } } @@ -127,8 +127,9 @@ public void onPhaseChanged(DaoPhase.Phase phase) { icon.getStyleClass().addAll("icon", "dao-remove-proposal-icon"); iconButton = new JFXButton("", icon); boolean isMyProposal = daoFacade.isMyProposal(proposal); - if (isMyProposal) - iconButton.setUserData(IconButtonTypes.REMOVE_PROPOSAL); + if (isMyProposal) { + iconButton.setUserData(IconButtonType.REMOVE_PROPOSAL); + } iconButton.setVisible(isMyProposal); iconButton.setManaged(isMyProposal); iconButton.getStyleClass().add("hidden-icon-button"); @@ -147,36 +148,36 @@ public void onPhaseChanged(DaoPhase.Phase phase) { icon = FormBuilder.getIcon(AwesomeIcon.THUMBS_UP); icon.getStyleClass().addAll("icon", "dao-accepted-icon"); iconButton = new JFXButton("", icon); - iconButton.setUserData(IconButtonTypes.ACCEPT); + iconButton.setUserData(IconButtonType.ACCEPT); } else { icon = FormBuilder.getIcon(AwesomeIcon.THUMBS_DOWN); icon.getStyleClass().addAll("icon", "dao-rejected-icon"); iconButton = new JFXButton("", icon); - iconButton.setUserData(IconButtonTypes.REJECT); + iconButton.setUserData(IconButtonType.REJECT); } } else { icon = FormBuilder.getIcon(AwesomeIcon.MINUS); icon.getStyleClass().addAll("icon", "dao-ignored-icon"); iconButton = new JFXButton("", icon); - iconButton.setUserData(IconButtonTypes.IGNORE); + iconButton.setUserData(IconButtonType.IGNORE); } iconButton.setTooltip(new Tooltip(Res.get("dao.proposal.table.icon.tooltip.changeVote", - ((IconButtonTypes) iconButton.getUserData()).getTitle(), - getNext(((IconButtonTypes) iconButton.getUserData())) + ((IconButtonType) iconButton.getUserData()).getTitle(), + getNext(((IconButtonType) iconButton.getUserData())) ))); iconButton.getStyleClass().add("hidden-icon-button"); iconButton.layout(); } } - private String getNext(IconButtonTypes iconButtonTypes) { - switch (iconButtonTypes) { + private String getNext(IconButtonType iconButtonType) { + switch (iconButtonType) { case ACCEPT: - return IconButtonTypes.REJECT.getTitle(); + return IconButtonType.REJECT.getTitle(); case REJECT: - return IconButtonTypes.IGNORE.getTitle(); + return IconButtonType.IGNORE.getTitle(); default: - return IconButtonTypes.ACCEPT.getTitle(); + return IconButtonType.ACCEPT.getTitle(); } } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java index 6a403d62977..362a5c041c9 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -590,7 +590,11 @@ private void updateViews() { switch (daoFacade.phaseProperty().get()) { case PROPOSAL: - lastColumn.setText(Res.get("dao.proposal.table.header.remove")); + // We have a bug in removing a proposal which is not trivial to fix (p2p network layer). Until that bug is fixed + // it is better to not show the remove button as it confused users and a removed proposal will reappear with a + // high probability at the vote phase. + //lastColumn.setText(Res.get("dao.proposal.table.header.remove")); + lastColumn.setText(""); break; case BLIND_VOTE: lastColumn.setText(Res.get("dao.proposal.table.header.myVote")); @@ -841,25 +845,36 @@ public void updateItem(final ProposalsListItem item, boolean empty) { item.onPhaseChanged(currentPhase); JFXButton iconButton = item.getIconButton(); if (iconButton != null) { + ProposalsListItem.IconButtonType iconButtonType = (ProposalsListItem.IconButtonType) iconButton.getUserData(); iconButton.setOnAction(e -> { selectedItem = item; if (areVoteButtonsVisible) { - if (iconButton.getUserData() == ProposalsListItem.IconButtonTypes.ACCEPT) + if (iconButtonType == ProposalsListItem.IconButtonType.ACCEPT) onReject(); - else if (iconButton.getUserData() == ProposalsListItem.IconButtonTypes.REJECT) + else if (iconButtonType == ProposalsListItem.IconButtonType.REJECT) onIgnore(); - else if (iconButton.getUserData() == ProposalsListItem.IconButtonTypes.IGNORE) + else if (iconButtonType == ProposalsListItem.IconButtonType.IGNORE) onAccept(); } else { - if (iconButton.getUserData() == ProposalsListItem.IconButtonTypes.REMOVE_PROPOSAL) + if (iconButtonType == ProposalsListItem.IconButtonType.REMOVE_PROPOSAL) onRemoveProposal(); } }); - if (!areVoteButtonsVisible && iconButton.getUserData() != ProposalsListItem.IconButtonTypes.REMOVE_PROPOSAL) { + if (!areVoteButtonsVisible && iconButtonType != ProposalsListItem.IconButtonType.REMOVE_PROPOSAL) { iconButton.setMouseTransparent(true); iconButton.setStyle("-fx-cursor: default;"); } + + // We have a bug in removing a proposal which is not trivial to fix (p2p network layer). + // Until that bug is fixed + // it is better to not show the remove button as it confused users and a removed proposal will reappear with a + // high probability at the vote phase. The following lines can be removed once the bug is fixed. + boolean showIcon = iconButtonType != null && + iconButtonType != ProposalsListItem.IconButtonType.REMOVE_PROPOSAL; + iconButton.setVisible(showIcon); + iconButton.setManaged(showIcon); + setGraphic(iconButton); } else { setGraphic(null); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 5a0c50537cc..2013f9fa08e 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -72,6 +72,8 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.Node; +import javafx.scene.text.Text; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -425,10 +427,12 @@ private void createCharts() { priceSeries = new XYChart.Series<>(); priceAxisX = new NumberAxis(0, model.maxTicks + 1, 1); - priceAxisX.setTickUnit(1); - priceAxisX.setMinorTickCount(0); + priceAxisX.setTickUnit(4); + priceAxisX.setMinorTickCount(4); + priceAxisX.setMinorTickVisible(true); priceAxisX.setForceZeroInRange(false); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + addTickMarkLabelCssClass(priceAxisX, "axis-tick-mark-text-node"); priceAxisY = new NumberAxis(); priceAxisY.setForceZeroInRange(false); @@ -469,8 +473,8 @@ public Number fromString(String string) { } }); priceChart.setId("price-chart"); - priceChart.setMinHeight(178); - priceChart.setPrefHeight(178); + priceChart.setMinHeight(188); + priceChart.setPrefHeight(188); priceChart.setMaxHeight(300); priceChart.setLegendVisible(false); priceChart.setPadding(new Insets(0)); @@ -489,10 +493,12 @@ public Number fromString(String string) { volumeSeries = new XYChart.Series<>(); volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1); - volumeAxisX.setTickUnit(1); - volumeAxisX.setMinorTickCount(0); + volumeAxisX.setTickUnit(4); + volumeAxisX.setMinorTickCount(4); + volumeAxisX.setMinorTickVisible(true); volumeAxisX.setForceZeroInRange(false); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + addTickMarkLabelCssClass(volumeAxisX, "axis-tick-mark-text-node"); volumeAxisY = new NumberAxis(); volumeAxisY.setForceZeroInRange(true); @@ -523,8 +529,8 @@ public Number fromString(String string) { }); volumeChart.setId("volume-chart"); volumeChart.setData(FXCollections.observableArrayList(List.of(volumeSeries))); - volumeChart.setMinHeight(128); - volumeChart.setPrefHeight(128); + volumeChart.setMinHeight(138); + volumeChart.setPrefHeight(138); volumeChart.setMaxHeight(200); volumeChart.setLegendVisible(false); volumeChart.setPadding(new Insets(0)); @@ -570,11 +576,31 @@ private StringConverter getTimeAxisStringConverter() { @Override public String toString(Number object) { long index = MathUtils.doubleToLong((double) object); + // The last tick is on the chart edge, it is not well spaced with + // the previous tick and interferes with its label. + if (model.maxTicks + 1 == index) return ""; + long time = model.getTimeFromTickIndex(index); - if (model.tickUnit.ordinal() <= TradesChartsViewModel.TickUnit.DAY.ordinal()) - return index % 4 == 0 ? DisplayUtils.formatDate(new Date(time)) : ""; - else - return index % 3 == 0 ? DisplayUtils.formatTime(new Date(time)) : ""; + String fmt = ""; + switch (model.tickUnit) { + case YEAR: + fmt = "yyyy"; + break; + case MONTH: + fmt = "MMMyy"; + break; + case WEEK: + case DAY: + fmt = "dd/MMM\nyyyy"; + break; + case HOUR : + case MINUTE_10: + fmt = "HH:mm\ndd/MMM"; + break; + default: // nothing here + } + + return DisplayUtils.formatDateAxis(new Date(time), fmt); } @Override @@ -584,6 +610,20 @@ public Number fromString(String string) { }; } + private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { + // grab the axis tick mark label (text object) and add a CSS class. + axis.getChildrenUnmodifiable().addListener((ListChangeListener) c -> { + while (c.next()) { + if (c.wasAdded()) { + for (Node mark : c.getAddedSubList()) { + if (mark instanceof Text) { + mark.getStyleClass().add(cssClass); + } + } + } + } + }); + } /////////////////////////////////////////////////////////////////////////////////////////// // CurrencyComboBox diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java index 673f501faaf..2403ac4fc7f 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java @@ -130,7 +130,7 @@ protected void layoutPlotChildren() { double candleWidth = -1; if (getXAxis() instanceof NumberAxis) { NumberAxis xa = (NumberAxis) getXAxis(); - candleWidth = xa.getDisplayPosition(xa.getTickUnit()) * 0.60; // use 90% width between ticks + candleWidth = xa.getDisplayPosition(1) * 0.60; // use 60% width between units } // update candle candle.update(close - y, high - y, low - y, candleWidth); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java index 12e61790b07..d5a0f456da4 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java @@ -65,7 +65,7 @@ protected void layoutPlotChildren() { double candleWidth = -1; if (getXAxis() instanceof NumberAxis) { NumberAxis xa = (NumberAxis) getXAxis(); - candleWidth = xa.getDisplayPosition(xa.getTickUnit()) * 0.60; // use 90% width between ticks + candleWidth = xa.getDisplayPosition(1) * 0.60; // use 60% width between units } // 97 is visible chart data height if chart height is 140. diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java new file mode 100644 index 00000000000..44a53361543 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java @@ -0,0 +1,199 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.locale.Res; +import bisq.core.util.validation.UrlInputValidator; + +import bisq.common.util.Tuple2; + +import bisq.core.user.BlockChainExplorer; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; + +import javafx.event.EventHandler; + +import javafx.collections.FXCollections; + +import javafx.util.Callback; + +import java.util.ArrayList; + +import static bisq.desktop.util.FormBuilder.*; +import static javafx.beans.binding.Bindings.createBooleanBinding; + +public class EditCustomExplorerWindow extends Overlay { + + private InputTextField nameInputTextField, txUrlInputTextField, addressUrlInputTextField; + private UrlInputValidator urlInputValidator; + private BlockChainExplorer currentExplorer; + private ListView listView; + + public EditCustomExplorerWindow(String coin, + BlockChainExplorer currentExplorer, + ArrayList availableExplorers) { + this.currentExplorer = currentExplorer; + listView = new ListView<>(); + listView.setItems(FXCollections.observableArrayList(availableExplorers)); + headLine = coin + " " + Res.get("settings.preferences.editCustomExplorer.headline"); + } + + public BlockChainExplorer getEditedBlockChainExplorer() { + return new BlockChainExplorer(nameInputTextField.getText(), + txUrlInputTextField.getText(), addressUrlInputTextField.getText()); + } + + public void show() { + + width = 1000; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + + urlInputValidator = new UrlInputValidator(); + txUrlInputTextField.setValidator(urlInputValidator); + addressUrlInputTextField.setValidator(urlInputValidator); + nameInputTextField.setValidator(new LengthValidator(1, 50)); + + actionButton.disableProperty().bind(createBooleanBinding(() -> { + String name = nameInputTextField.getText(); + String txUrl = txUrlInputTextField.getText(); + String addressUrl = addressUrlInputTextField.getText(); + + // Otherwise we require that input is valid + return !nameInputTextField.getValidator().validate(name).isValid || + !txUrlInputTextField.getValidator().validate(txUrl).isValid || + !addressUrlInputTextField.getValidator().validate(addressUrl).isValid; + }, + nameInputTextField.textProperty(), txUrlInputTextField.textProperty(), addressUrlInputTextField.textProperty())); + + applyStyles(); + display(); + } + + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(15); + gridPane.setVgap(15); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints1.setPercentWidth(45); + columnConstraints2.setPercentWidth(55); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + } + + private void addContent() { + Label mlm = addMultilineLabel(gridPane, rowIndex++, Res.get("settings.preferences.editCustomExplorer.description"), 0); + GridPane.setColumnSpan(mlm, 2); + GridPane.setMargin(mlm, new Insets(40, 0, 0, 0)); + + Button button = new AutoTooltipButton(">>"); + button.setOnAction(e -> { + BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); + if (blockChainExplorer != null) { + nameInputTextField.setText(blockChainExplorer.name); + txUrlInputTextField.setText(blockChainExplorer.txUrl); + addressUrlInputTextField.setText(blockChainExplorer.addressUrl); + } + }); + button.setStyle("-fx-pref-width: 50px; -fx-pref-height: 30; -fx-padding: 3 3 3 3;"); + VBox vBox = new VBox(button); + vBox.setAlignment(Pos.CENTER); + final Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("settings.preferences.editCustomExplorer.available"), listView); + listView.setPrefWidth(300); + HBox hBox = new HBox(topLabelWithVBox.second, vBox); + hBox.setAlignment(Pos.CENTER_LEFT); + hBox.setSpacing(20); + hBox.setMaxHeight(200); + gridPane.add(hBox, 0, rowIndex); + GridPane.setColumnIndex(hBox, 0); + GridPane.setValignment(hBox, VPos.TOP); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + + listView.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView list) { + ListCell cell = new ListCell<>() { + final Label label = new AutoTooltipLabel(); + final AnchorPane pane = new AnchorPane(label); + @Override + public void updateItem(final BlockChainExplorer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(item.name); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + + cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.getClickCount() == 2) { + BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); + nameInputTextField.setText(blockChainExplorer.name); + txUrlInputTextField.setText(blockChainExplorer.txUrl); + addressUrlInputTextField.setText(blockChainExplorer.addressUrl); + } + } + }); + return cell; + } + }); + + GridPane autoConfirmGridPane = new GridPane(); + autoConfirmGridPane.setPrefHeight(150); + GridPane.setMargin(autoConfirmGridPane, new Insets(10, 0, 0, 0)); + gridPane.add(autoConfirmGridPane, 1, rowIndex); + addTitledGroupBg(autoConfirmGridPane, 0, 6, Res.get("settings.preferences.editCustomExplorer.chosen"), 0); + int localRowIndex = 0; + nameInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.name"), Layout.FIRST_ROW_DISTANCE); + nameInputTextField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + txUrlInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.txUrl")); + addressUrlInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.addressUrl")); + nameInputTextField.setText(currentExplorer.name); + txUrlInputTextField.setText(currentExplorer.txUrl); + addressUrlInputTextField.setText(currentExplorer.addressUrl); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 74748a0d820..b56b317090e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -161,6 +161,8 @@ private void addContent() { Res.get("filterWindow.disableTradeBelowVersion")); InputTextField bannedPrivilegedDevPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; + InputTextField autoConfExplorersTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.autoConfExplorers")).second; Filter filter = filterManager.getDevFilter(); if (filter != null) { @@ -178,6 +180,7 @@ private void addContent() { setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes()); setupFieldFromList(btcNodesTF, filter.getBtcNodes()); setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); + setupFieldFromList(autoConfExplorersTF, filter.getBannedAutoConfExplorers()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); disableDaoCheckBox.setSelected(filter.isDisableDao()); @@ -215,7 +218,8 @@ private void addContent() { filterManager.getOwnerPubKey(), signerPubKeyAsHex, readAsList(bannedPrivilegedDevPubKeysTF), - disableAutoConfCheckBox.isSelected() + disableAutoConfCheckBox.isSelected(), + readAsList(autoConfExplorersTF) ); // We remove first the old filter diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 6a9b4dff06c..fc08e6bc233 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -215,18 +215,22 @@ String getMarketLabel(PendingTradesListItem item) { } public String getRemainingTradeDurationAsWords() { + checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return tradeUtil.getRemainingTradeDurationAsWords(dataModel.getTrade()); } public double getRemainingTradeDurationAsPercentage() { + checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return tradeUtil.getRemainingTradeDurationAsPercentage(dataModel.getTrade()); } public String getDateForOpenDispute() { + checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return DisplayUtils.formatDateTime(tradeUtil.getDateForOpenDispute(dataModel.getTrade())); } public boolean showWarning() { + checkNotNull(dataModel.getTrade(), "model's trade must not be null"); Date halfTradePeriodDate = tradeUtil.getHalfTradePeriodDate(dataModel.getTrade()); return halfTradePeriodDate != null && new Date().after(halfTradePeriodDate); } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index ef8c6951096..d3aa68de818 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -26,6 +26,7 @@ import bisq.desktop.components.PasswordTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.EditCustomExplorerWindow; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.ImageUtil; import bisq.desktop.util.Layout; @@ -45,7 +46,6 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.provider.fee.FeeService; -import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; @@ -57,6 +57,7 @@ import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.config.Config; +import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import bisq.common.util.Utilities; @@ -73,6 +74,7 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Separator; +import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; @@ -107,8 +109,7 @@ @FxmlView public class PreferencesView extends ActivatableViewAndModel { private final CoinFormatter formatter; - private ComboBox blockChainExplorerComboBox; - private ComboBox bsqBlockChainExplorerComboBox; + private TextField btcExplorerTextField, bsqExplorerTextField; private ComboBox userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; @@ -138,9 +139,8 @@ public class PreferencesView extends ActivatableViewAndModel fiatCurrenciesComboBox; private ListView cryptoCurrenciesListView; private ComboBox cryptoCurrenciesComboBox; - private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton; - private ObservableList blockExplorers; - private ObservableList bsqBlockChainExplorers; + private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton, + editCustomBtcExplorer, editCustomBsqExplorer; private ObservableList languageCodes; private ObservableList countries; private ObservableList fiatCurrencies; @@ -148,11 +148,11 @@ public class PreferencesView extends ActivatableViewAndModel cryptoCurrencies; private ObservableList allCryptoCurrencies; private ObservableList tradeCurrencies; - private InputTextField deviationInputTextField; - private ChangeListener deviationListener, ignoreTradersListListener, ignoreDustThresholdListener, + private InputTextField deviationInputTextField, bsqAverageTrimThresholdTextField; + private ChangeListener deviationListener, bsqAverageTrimThresholdListener, ignoreTradersListListener, ignoreDustThresholdListener, rpcUserListener, rpcPwListener, blockNotifyPortListener, autoConfTradeLimitListener, autoConfServiceAddressListener; - private ChangeListener deviationFocusedListener; + private ChangeListener deviationFocusedListener, bsqAverageTrimThresholdFocusedListener; private ChangeListener useCustomFeeCheckboxListener; private ChangeListener transactionFeeChangeListener; private final boolean daoOptionsSet; @@ -194,8 +194,6 @@ public PreferencesView(PreferencesViewModel model, @Override public void initialize() { - blockExplorers = FXCollections.observableArrayList(preferences.getBlockChainExplorers()); - bsqBlockChainExplorers = FXCollections.observableArrayList(preferences.getBsqBlockChainExplorers()); languageCodes = FXCollections.observableArrayList(LanguageUtil.getUserLanguageCodes()); countries = FXCollections.observableArrayList(CountryUtil.getAllCountries()); fiatCurrencies = preferences.getFiatCurrenciesAsObservable(); @@ -255,15 +253,13 @@ private void initializeGeneralOptions() { userCountryComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.country"), userCountryComboBox, false)); - blockChainExplorerComboBox = addComboBox(root, ++gridRow, - Res.get("setting.preferences.explorer")); - blockChainExplorerComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("setting.preferences.explorer"), - blockChainExplorerComboBox, false)); + Tuple2 btcExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); + btcExplorerTextField = btcExp.first; + editCustomBtcExplorer = btcExp.second; - bsqBlockChainExplorerComboBox = addComboBox(root, ++gridRow, - Res.get("setting.preferences.explorer.bsq")); - bsqBlockChainExplorerComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("setting.preferences.explorer.bsq"), - bsqBlockChainExplorerComboBox, false)); + Tuple2 bsqExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer.bsq")); + bsqExplorerTextField = bsqExp.first; + editCustomBsqExplorer = bsqExp.second; Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.txFee"), Res.get("setting.preferences.useCustomValue")); @@ -318,7 +314,6 @@ private void initializeGeneralOptions() { // deviation deviationInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.deviation")); - deviationListener = (observable, oldValue, newValue) -> { try { double value = ParsingUtils.parsePercentStringToDouble(newValue); @@ -327,16 +322,16 @@ private void initializeGeneralOptions() { preferences.setMaxPriceDistanceInPercent(value); } else { new Popup().warning(Res.get("setting.preferences.deviationToLarge", maxDeviation * 100)).show(); - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } } catch (NumberFormatException t) { log.error("Exception at parseDouble deviation: " + t.toString()); - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } }; deviationFocusedListener = (observable1, oldValue1, newValue1) -> { if (oldValue1 && !newValue1) - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); }; // ignoreTraders @@ -617,7 +612,7 @@ private void initializeDisplayOptions() { } private void initializeDaoOptions() { - daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); + daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 4, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); resyncDaoFromResourcesButton = addButton(root, gridRow, Res.get("setting.preferences.dao.resyncFromResources.label"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); resyncDaoFromResourcesButton.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(resyncDaoFromResourcesButton, Priority.ALWAYS); @@ -626,6 +621,36 @@ private void initializeDaoOptions() { resyncDaoFromGenesisButton.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(resyncDaoFromGenesisButton, Priority.ALWAYS); + bsqAverageTrimThresholdTextField = addInputTextField(root, ++gridRow, + Res.get("setting.preferences.bsqAverageTrimThreshold")); + bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getBsqAverageTrimThreshold())); + + bsqAverageTrimThresholdListener = (observable, oldValue, newValue) -> { + try { + double value = ParsingUtils.parsePercentStringToDouble(newValue); + double maxValue = 0.49; + checkArgument(value >= 0, "Input must be positive"); + if (value <= maxValue) { + preferences.setBsqAverageTrimThreshold(value); + } else { + new Popup().warning(Res.get("setting.preferences.deviationToLarge", + maxValue * 100)).show(); + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + } + } catch (NumberFormatException t) { + log.error("Exception: " + t.toString()); + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + } + }; + bsqAverageTrimThresholdFocusedListener = (observable1, oldValue1, newValue1) -> { + if (oldValue1 && !newValue1) + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + }; + + isDaoFullNodeToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.dao.isDaoFullNode")); rpcUserTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.dao.rpcUser")); rpcUserTextField.setVisible(false); @@ -831,37 +856,10 @@ public Country fromString(String string) { } }); - blockChainExplorerComboBox.setItems(blockExplorers); - blockChainExplorerComboBox.getSelectionModel().select(preferences.getBlockChainExplorer()); - blockChainExplorerComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(BlockChainExplorer blockChainExplorer) { - return blockChainExplorer.name; - } - - @Override - public BlockChainExplorer fromString(String string) { - return null; - } - }); - blockChainExplorerComboBox.setOnAction(e -> preferences.setBlockChainExplorer(blockChainExplorerComboBox.getSelectionModel().getSelectedItem())); - - bsqBlockChainExplorerComboBox.setItems(bsqBlockChainExplorers); - bsqBlockChainExplorerComboBox.getSelectionModel().select(preferences.getBsqBlockChainExplorer()); - bsqBlockChainExplorerComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(BlockChainExplorer bsqBlockChainExplorer) { - return bsqBlockChainExplorer.name; - } + btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + bsqExplorerTextField.setText(preferences.getBsqBlockChainExplorer().name); - @Override - public BlockChainExplorer fromString(String string) { - return null; - } - }); - bsqBlockChainExplorerComboBox.setOnAction(e -> preferences.setBsqBlockChainExplorer(bsqBlockChainExplorerComboBox.getSelectionModel().getSelectedItem())); - - deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())); + deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())); deviationInputTextField.textProperty().addListener(deviationListener); deviationInputTextField.focusedProperty().addListener(deviationFocusedListener); @@ -937,6 +935,34 @@ private void activateDisplayPreferences() { resetDontShowAgainButton.setOnAction(e -> preferences.resetDontShowAgain()); + editCustomBtcExplorer.setOnAction(e -> { + EditCustomExplorerWindow urlWindow = new EditCustomExplorerWindow("BTC", + preferences.getBlockChainExplorer(), preferences.getBlockChainExplorers()); + urlWindow + .actionButtonText(Res.get("shared.save")) + .onAction(() -> { + preferences.setBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); + btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(urlWindow::hide) + .show(); + }); + + editCustomBsqExplorer.setOnAction(e -> { + EditCustomExplorerWindow urlWindow = new EditCustomExplorerWindow("BSQ", + preferences.getBsqBlockChainExplorer(), preferences.getBsqBlockChainExplorers()); + urlWindow + .actionButtonText(Res.get("shared.save")) + .onAction(() -> { + preferences.setBsqBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); + bsqExplorerTextField.setText(preferences.getBsqBlockChainExplorer().name); + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(urlWindow::hide) + .show(); + }); + // We use opposite property (useStandbyMode) in preferences to have the default value (false) set as we want it, // so users who update gets set avoidStandbyMode=true (useStandbyMode=false) if (displayStandbyModeFeature) { @@ -950,6 +976,10 @@ private void activateDisplayPreferences() { private void activateDaoPreferences() { boolean daoFullNode = preferences.isDaoFullNode(); isDaoFullNodeToggleButton.setSelected(daoFullNode); + + bsqAverageTrimThresholdTextField.textProperty().addListener(bsqAverageTrimThresholdListener); + bsqAverageTrimThresholdTextField.focusedProperty().addListener(bsqAverageTrimThresholdFocusedListener); + String rpcUser = preferences.getRpcUser(); String rpcPw = preferences.getRpcPw(); int blockNotifyPort = preferences.getBlockNotifyPort(); @@ -1059,8 +1089,8 @@ private void deactivateGeneralOptions() { //selectBaseCurrencyNetworkComboBox.setOnAction(null); userLanguageComboBox.setOnAction(null); userCountryComboBox.setOnAction(null); - blockChainExplorerComboBox.setOnAction(null); - bsqBlockChainExplorerComboBox.setOnAction(null); + editCustomBtcExplorer.setOnAction(null); + editCustomBsqExplorer.setOnAction(null); deviationInputTextField.textProperty().removeListener(deviationListener); deviationInputTextField.focusedProperty().removeListener(deviationFocusedListener); transactionFeeInputTextField.focusedProperty().removeListener(transactionFeeFocusedListener); @@ -1091,6 +1121,8 @@ private void deactivateDisplayPreferences() { private void deactivateDaoPreferences() { resyncDaoFromResourcesButton.setOnAction(null); resyncDaoFromGenesisButton.setOnAction(null); + bsqAverageTrimThresholdTextField.textProperty().removeListener(bsqAverageTrimThresholdListener); + bsqAverageTrimThresholdTextField.focusedProperty().removeListener(bsqAverageTrimThresholdFocusedListener); isDaoFullNodeToggleButton.setOnAction(null); rpcUserTextField.textProperty().removeListener(rpcUserListener); rpcPwTextField.textProperty().removeListener(rpcPwListener); diff --git a/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java b/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java index 9551ae5fd4e..d80d79a009b 100644 --- a/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java @@ -91,7 +91,7 @@ private static List extractYValues(ObservableList findInlierRange( + public static Tuple2 findInlierRange( List yValues, double percentToTrim, double howManyStdDevsConstituteOutlier diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index 34295b52d60..e85edd16ff3 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.time.DurationFormatUtils; import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.math.BigDecimal; import java.math.RoundingMode; @@ -67,6 +68,15 @@ public static String formatDate(Date date) { } } + public static String formatDateAxis(Date date, String format) { + if (date != null) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(format, GlobalSettings.getLocale()); + return dateFormatter.format(date); + } else { + return ""; + } + } + public static String formatAccountAge(long durationMillis) { durationMillis = Math.max(0, durationMillis); String day = Res.get("time.day").toLowerCase(); diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index f8061e04f99..f91a4c83ff0 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -338,6 +338,31 @@ public static Tuple3 addTopLabelTextField(GridPane gridP return new Tuple3<>(topLabelWithVBox.first, textField, topLabelWithVBox.second); } + public static Tuple2 addTextFieldWithEditButton(GridPane gridPane, int rowIndex, String title) { + TextField textField = new BisqTextField(); + textField.setPromptText(title); + textField.setEditable(false); + textField.setFocusTraversable(false); + textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + + Button button = new AutoTooltipButton("..."); + button.setStyle("-fx-min-width: 35px; -fx-pref-height: 20; -fx-padding: 3 3 3 3; -fx-border-insets: 5px;"); + button.managedProperty().bind(button.visibleProperty()); + VBox vBoxButton = new VBox(button); + vBoxButton.setAlignment(Pos.CENTER); + HBox hBox2 = new HBox(textField, vBoxButton); + + Label label = getTopLabel(title); + VBox textFieldVbox = getTopLabelVBox(0); + textFieldVbox.getChildren().addAll(label, hBox2); + + gridPane.getChildren().add(textFieldVbox); + GridPane.setRowIndex(textFieldVbox, rowIndex); + GridPane.setMargin(textFieldVbox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + + return new Tuple2<>(textField, button); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Confirmation Fields /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 0a38514b777..e90559cd1d0 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -247,25 +247,24 @@ public static void importAccounts(User user, String directory = Paths.get(path).getParent().toString(); preferences.setDirectoryChooserPath(directory); PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler); - PaymentAccountList persisted = persistenceManager.getPersisted(fileName); - if (persisted != null) { - final StringBuilder msg = new StringBuilder(); - final HashSet paymentAccounts = new HashSet<>(); - persisted.getList().forEach(paymentAccount -> { - final String id = paymentAccount.getId(); - if (user.getPaymentAccount(id) == null) { - paymentAccounts.add(paymentAccount); - msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); - } else { - msg.append(Res.get("guiUtil.accountImport.noImport", id)); - } - }); - user.addImportedPaymentAccounts(paymentAccounts); - new Popup().feedback(Res.get("guiUtil.accountImport.imported", path, msg)).show(); - - } else { - new Popup().warning(Res.get("guiUtil.accountImport.noAccountsFound", path, fileName)).show(); - } + persistenceManager.readPersisted(fileName, persisted -> { + StringBuilder msg = new StringBuilder(); + HashSet paymentAccounts = new HashSet<>(); + persisted.getList().forEach(paymentAccount -> { + String id = paymentAccount.getId(); + if (user.getPaymentAccount(id) == null) { + paymentAccounts.add(paymentAccount); + msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); + } else { + msg.append(Res.get("guiUtil.accountImport.noImport", id)); + } + }); + user.addImportedPaymentAccounts(paymentAccounts); + new Popup().feedback(Res.get("guiUtil.accountImport.imported", path, msg)).show(); + }, + () -> { + new Popup().warning(Res.get("guiUtil.accountImport.noAccountsFound", path, fileName)).show(); + }); } else { log.error("The selected file is not the expected file for import. The expected file name is: " + fileName + "."); } diff --git a/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidAccountNameValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidAccountNameValidator.java new file mode 100644 index 00000000000..4c6635e85ab --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidAccountNameValidator.java @@ -0,0 +1,51 @@ + +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.util.validation; + +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.RegexValidator; + +import javax.inject.Inject; + +public final class AustraliaPayidAccountNameValidator extends InputValidator { + @Override + public ValidationResult validate(String input) { + ValidationResult result = super.validate(input); + + if (result.isValid) + result = lengthValidator.validate(input); + if (result.isValid) + result = regexValidator.validate(input); + + return result; + } + + private final LengthValidator lengthValidator; + private final RegexValidator regexValidator; + + @Inject + public AustraliaPayidAccountNameValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { + + lengthValidator.setMinLength(1); + lengthValidator.setMaxLength(40); + this.lengthValidator = lengthValidator; + + this.regexValidator = regexValidator; + } +} diff --git a/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidValidator.java new file mode 100644 index 00000000000..62ddc30cc90 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/util/validation/AustraliaPayidValidator.java @@ -0,0 +1,56 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.util.validation; + +import bisq.core.util.validation.InputValidator; + +public final class AustraliaPayidValidator extends InputValidator { + + private final EmailValidator emailValidator; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + public AustraliaPayidValidator() { + emailValidator = new EmailValidator(); + } + + @Override + public ValidationResult validate(String input) { + ValidationResult result = validateIfNotEmpty(input); + if (!result.isValid) { + return result; + } else { + ValidationResult emailResult = emailValidator.validate(input); + if (emailResult.isValid) + return emailResult; + else + return validatePhoneNumber(input); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + // TODO not impl yet -> see InteracETransferValidator + private ValidationResult validatePhoneNumber(String input) { + return super.validate(input); + } +} diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java index ef176f9222c..8c3eab2e637 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java @@ -135,7 +135,8 @@ protected void execute() { new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler), maxConnections); // init file storage - peerManager.readPersisted(); + peerManager.readPersisted(() -> { + }); PeerExchangeManager peerExchangeManager = new PeerExchangeManager(networkNode, seedNodeRepository, peerManager); diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 927563476b5..a290406fbed 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -52,7 +52,6 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtobufferException; import bisq.common.proto.network.NetworkEnvelope; -import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.util.Utilities; import com.google.inject.Inject; @@ -105,7 +104,7 @@ import static com.google.common.base.Preconditions.checkNotNull; public class P2PService implements SetupListener, MessageListener, ConnectionListener, RequestDataManager.Listener, - HashMapChangedListener, PersistedDataHost { + HashMapChangedListener { private static final Logger log = LoggerFactory.getLogger(P2PService.class); private final SeedNodeRepository seedNodeRepository; @@ -184,12 +183,6 @@ public P2PService(NetworkNode networkNode, }); } - @Override - public void readPersisted() { - p2PDataStorage.readPersisted(); - peerManager.readPersisted(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // API diff --git a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java index 8fd4ae9c931..39755d5f04e 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java @@ -189,11 +189,12 @@ public void shutDown() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { - PeerList persisted = persistenceManager.getPersisted(); - if (persisted != null) { - peerList.setAll(persisted.getSet()); - } + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + peerList.setAll(persisted.getSet()); + completeHandler.run(); + }, + completeHandler); } diff --git a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java index 6fdf847d490..bba3c44e289 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -78,6 +78,12 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + import java.security.KeyPair; import java.security.PublicKey; @@ -143,6 +149,10 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers /// which removes entries after PURGE_AGE_DAYS. private final int maxSequenceNumberMapSizeBeforePurge; + // Don't convert to local variable as it might get GC'ed. + private MonadicBinding readFromResourcesCompleteBinding; + + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -171,23 +181,58 @@ public P2PDataStorage(NetworkNode networkNode, } @Override - public void readPersisted() { + public void readPersisted(Runnable completeHandler) { + persistenceManager.readPersisted(persisted -> { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + completeHandler.run(); + }, + completeHandler); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + public void readPersistedSync() { SequenceNumberMap persisted = persistenceManager.getPersisted(); - if (persisted != null) + if (persisted != null) { sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + } } - // This method is called at startup in a non-user thread. - // We should not have any threading issues here as the p2p network is just initializing + // Threading is done on the persistenceManager level + public void readFromResources(String postFix, Runnable completeHandler) { + BooleanProperty appendOnlyDataStoreServiceReady = new SimpleBooleanProperty(); + BooleanProperty protectedDataStoreServiceReady = new SimpleBooleanProperty(); + BooleanProperty resourceDataStoreServiceReady = new SimpleBooleanProperty(); - public synchronized void readFromResources(String postFix) { - appendOnlyDataStoreService.readFromResources(postFix); - protectedDataStoreService.readFromResources(postFix); - resourceDataStoreService.readFromResources(postFix); + appendOnlyDataStoreService.readFromResources(postFix, () -> appendOnlyDataStoreServiceReady.set(true)); + protectedDataStoreService.readFromResources(postFix, () -> { + map.putAll(protectedDataStoreService.getMap()); + protectedDataStoreServiceReady.set(true); + }); + resourceDataStoreService.readFromResources(postFix, () -> resourceDataStoreServiceReady.set(true)); + + readFromResourcesCompleteBinding = EasyBind.combine(appendOnlyDataStoreServiceReady, + protectedDataStoreServiceReady, + resourceDataStoreServiceReady, + (a, b, c) -> a && b && c); + readFromResourcesCompleteBinding.subscribe((observable, oldValue, newValue) -> { + if (newValue) { + completeHandler.run(); + } + }); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + public void readFromResourcesSync(String postFix) { + appendOnlyDataStoreService.readFromResourcesSync(postFix); + protectedDataStoreService.readFromResourcesSync(postFix); + resourceDataStoreService.readFromResourcesSync(postFix); map.putAll(protectedDataStoreService.getMap()); } + /////////////////////////////////////////////////////////////////////////////////////////// // RequestData API /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java index 916523fcc60..859f09489c2 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java @@ -22,9 +22,12 @@ import javax.inject.Inject; +import com.google.common.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import lombok.Getter; @@ -51,10 +54,24 @@ public void addService(MapStoreService service.readFromResources(postFix)); + public void readFromResources(String postFix, Runnable completeHandler) { + AtomicInteger remaining = new AtomicInteger(services.size()); + services.forEach(service -> { + service.readFromResources(postFix, () -> { + if (remaining.decrementAndGet() == 0) { + completeHandler.run(); + } + }); + }); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + public void readFromResourcesSync(String postFix) { + services.forEach(service -> service.readFromResourcesSync(postFix)); } + public Map getMap() { return services.stream() .flatMap(service -> service.getMap().entrySet().stream()) diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/HistoricalDataStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/HistoricalDataStoreService.java index 2570e37ea90..2bed3ba51c6 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/HistoricalDataStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/HistoricalDataStoreService.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import lombok.extern.slf4j.Slf4j; @@ -145,19 +146,28 @@ protected PersistableNetworkPayload putIfAbsent(P2PDataStorage.ByteArray hash, P @Override - protected void readFromResources(String postFix) { - readStore(); - log.info("We have created the {} store for the live data and filled it with {} entries from the persisted data.", - getFileName(), getMapOfLiveData().size()); - - // Now we add our historical data stores. As they are immutable after created we use an ImmutableMap - ImmutableMap.Builder allHistoricalPayloadsBuilder = ImmutableMap.builder(); - ImmutableMap.Builder> storesByVersionBuilder = ImmutableMap.builder(); - - Version.HISTORICAL_RESOURCE_FILE_VERSION_TAGS.forEach(version -> readHistoricalStoreFromResources(version, postFix, allHistoricalPayloadsBuilder, storesByVersionBuilder)); - - allHistoricalPayloads = allHistoricalPayloadsBuilder.build(); - storesByVersion = storesByVersionBuilder.build(); + protected void readFromResources(String postFix, Runnable completeHandler) { + readStore(persisted -> { + log.info("We have created the {} store for the live data and filled it with {} entries from the persisted data.", + getFileName(), getMapOfLiveData().size()); + + // Now we add our historical data stores. As they are immutable after created we use an ImmutableMap + ImmutableMap.Builder allHistoricalPayloadsBuilder = ImmutableMap.builder(); + ImmutableMap.Builder> storesByVersionBuilder = ImmutableMap.builder(); + + AtomicInteger numFiles = new AtomicInteger(Version.HISTORICAL_RESOURCE_FILE_VERSION_TAGS.size()); + Version.HISTORICAL_RESOURCE_FILE_VERSION_TAGS.forEach(version -> readHistoricalStoreFromResources(version, + postFix, + allHistoricalPayloadsBuilder, + storesByVersionBuilder, + () -> { + if (numFiles.decrementAndGet() == 0) { + allHistoricalPayloads = allHistoricalPayloadsBuilder.build(); + storesByVersion = storesByVersionBuilder.build(); + completeHandler.run(); + } + })); + }); } @@ -168,23 +178,25 @@ protected void readFromResources(String postFix) { private void readHistoricalStoreFromResources(String version, String postFix, ImmutableMap.Builder allHistoricalDataBuilder, - ImmutableMap.Builder> storesByVersionBuilder) { + ImmutableMap.Builder> storesByVersionBuilder, + Runnable completeHandler) { String fileName = getFileName() + "_" + version; boolean wasCreatedFromResources = makeFileFromResourceFile(fileName, postFix); - // If resource file does not exist we return null. We do not create a new store as it would never get filled. - PersistableNetworkPayloadStore historicalStore = persistenceManager.getPersisted(fileName); - if (historicalStore == null) { - log.warn("Resource file with file name {} does not exits.", fileName); - return; - } - - storesByVersionBuilder.put(version, historicalStore); - allHistoricalDataBuilder.putAll(historicalStore.getMap()); - - if (wasCreatedFromResources) { - pruneStore(historicalStore, version); - } + // If resource file does not exist we do not create a new store as it would never get filled. + persistenceManager.readPersisted(fileName, persisted -> { + storesByVersionBuilder.put(version, persisted); + allHistoricalDataBuilder.putAll(persisted.getMap()); + log.info("We have read from {} {} historical items.", fileName, persisted.getMap().size()); + if (wasCreatedFromResources) { + pruneStore(persisted, version); + } + completeHandler.run(); + }, + () -> { + log.warn("Resource file with file name {} does not exits.", fileName); + completeHandler.run(); + }); } private void pruneStore(PersistableNetworkPayloadStore historicalStore, diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/ProtectedDataStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/ProtectedDataStoreService.java index 07f07ad7323..d79fef5ec4f 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/ProtectedDataStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/ProtectedDataStoreService.java @@ -24,9 +24,12 @@ import javax.inject.Inject; +import com.google.common.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -52,8 +55,21 @@ public void addService(MapStoreService service.readFromResources(postFix)); + public void readFromResources(String postFix, Runnable completeHandler) { + AtomicInteger remaining = new AtomicInteger(services.size()); + services.forEach(service -> { + service.readFromResources(postFix, () -> { + if (remaining.decrementAndGet() == 0) { + completeHandler.run(); + } + }); + }); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + public void readFromResourcesSync(String postFix) { + services.forEach(service -> service.readFromResourcesSync(postFix)); } public Map getMap() { diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/ResourceDataStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/ResourceDataStoreService.java index b6002af580e..dac03b4034b 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/ResourceDataStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/ResourceDataStoreService.java @@ -21,8 +21,11 @@ import javax.inject.Inject; +import com.google.common.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import lombok.extern.slf4j.Slf4j; @@ -42,7 +45,20 @@ public void addService(StoreService service) { services.add(service); } - public void readFromResources(String postFix) { - services.forEach(service -> service.readFromResources(postFix)); + public void readFromResources(String postFix, Runnable completeHandler) { + AtomicInteger remaining = new AtomicInteger(services.size()); + services.forEach(service -> { + service.readFromResources(postFix, () -> { + if (remaining.decrementAndGet() == 0) { + completeHandler.run(); + } + }); + }); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + public void readFromResourcesSync(String postFix) { + services.forEach(service -> service.readFromResourcesSync(postFix)); } } diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java index 5d1b57d9120..671b924d04b 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java @@ -22,10 +22,14 @@ import bisq.common.persistence.PersistenceManager; import bisq.common.proto.persistable.PersistableEnvelope; +import com.google.common.annotations.VisibleForTesting; + import java.nio.file.Paths; import java.io.File; +import java.util.function.Consumer; + import lombok.extern.slf4j.Slf4j; /** @@ -76,14 +80,27 @@ protected T getStore() { // Protected /////////////////////////////////////////////////////////////////////////////////////////// - protected void readFromResources(String postFix) { + protected void readFromResources(String postFix, Runnable completeHandler) { String fileName = getFileName(); makeFileFromResourceFile(fileName, postFix); try { - readStore(); + readStore(persisted -> completeHandler.run()); } catch (Throwable t) { makeFileFromResourceFile(fileName, postFix); - readStore(); + readStore(persisted -> completeHandler.run()); + } + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + protected void readFromResourcesSync(String postFix) { + String fileName = getFileName(); + makeFileFromResourceFile(fileName, postFix); + try { + readStoreSync(); + } catch (Throwable t) { + makeFileFromResourceFile(fileName, postFix); + readStoreSync(); } } @@ -112,24 +129,35 @@ protected boolean makeFileFromResourceFile(String fileName, String postFix) { return false; } - protected T getStore(String fileName) { - T store; - T persisted = persistenceManager.getPersisted(fileName); - if (persisted != null) { - store = persisted; - /* int length = store.toProtoMessage().getSerializedSize(); - double size = length > 1_000_000D ? length / 1_000_000D : length / 1_000D; - String unit = length > 1_000_000D ? "MB" : "KB"; - log.info("{}: size of {}: {} {}", this.getClass().getSimpleName(), - persisted.getClass().getSimpleName(), size, unit);*/ - } else { + protected void readStore(String fileName, Consumer consumer) { + persistenceManager.readPersisted(fileName, + consumer, + () -> consumer.accept(createStore())); + } + + protected void readStore(Consumer consumer) { + readStore(getFileName(), + persisted -> { + store = persisted; + initializePersistenceManager(); + consumer.accept(persisted); + }); + } + + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + protected T getStoreSync(String fileName) { + T store = persistenceManager.getPersisted(fileName); + if (store == null) { store = createStore(); } return store; } - protected void readStore() { - store = getStore(getFileName()); + // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. + @VisibleForTesting + protected void readStoreSync() { + store = getStoreSync(getFileName()); initializePersistenceManager(); } diff --git a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java index b04491ed403..7f2360c7746 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java @@ -137,8 +137,8 @@ protectedDataStoreService, mock(ResourceDataStoreService.class), sequenceNrMapPersistenceManager, clock, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); // Currently TestState only supports reading ProtectedStorageEntries off disk. - p2PDataStorage.readFromResources("unused"); - p2PDataStorage.readPersisted(); + p2PDataStorage.readFromResourcesSync("unused"); + p2PDataStorage.readPersistedSync(); p2PDataStorage.addHashMapChangedListener(hashMapChangedListener); p2PDataStorage.addAppendOnlyDataStoreListener(appendOnlyDataStoreListener); diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/MapStoreServiceFake.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/MapStoreServiceFake.java index 0f6f661fda0..4c2e508a4d9 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/MapStoreServiceFake.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/MapStoreServiceFake.java @@ -64,7 +64,7 @@ public boolean canHandle(PersistablePayload payload) { return true; } - protected void readFromResources(String postFix) { + protected void readFromResourcesSync(String postFix) { // do nothing. This Fake only supports in-memory storage. } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 006c6a6f111..d9e0a3973d8 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -51,6 +51,8 @@ service Offers { } rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) { } + rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) { + } } message GetOfferRequest { @@ -85,6 +87,13 @@ message CreateOfferReply { OfferInfo offer = 1; } +message CancelOfferRequest { + string id = 1; +} + +message CancelOfferReply { +} + message OfferInfo { string id = 1; string direction = 2; diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index bec5f921a5d..f84329db5e0 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -660,6 +660,7 @@ message Filter { string signer_pub_key_as_hex = 22; repeated string bannedPrivilegedDevPubKeys = 23; bool disable_auto_conf = 24; + repeated string banned_auto_conf_explorers = 25; } // Deprecated @@ -932,6 +933,7 @@ message PaymentAccountPayload { InstantCryptoCurrencyAccountPayload instant_crypto_currency_account_payload = 27; JapanBankAccountPayload japan_bank_account_payload = 28; TransferwiseAccountPayload Transferwise_account_payload = 29; + AustraliaPayidPayload australia_payid_payload = 30; } map exclude_from_json_data = 15; } @@ -999,6 +1001,11 @@ message JapanBankAccountPayload { string bank_account_number = 7; } +message AustraliaPayidPayload { + string bank_account_name = 1; + string payid = 2; +} + message SpecificBanksAccountPayload { repeated string accepted_banks = 1; } @@ -1581,6 +1588,7 @@ message PreferencesPayload { int32 css_theme = 54; bool tac_accepted_v120 = 55; repeated AutoConfirmSettings auto_confirm_settings = 56; + double bsq_average_trim_threshold = 57; } message AutoConfirmSettings {