diff --git a/cli/test.sh b/apitest/scripts/mainnet-test.sh similarity index 94% rename from cli/test.sh rename to apitest/scripts/mainnet-test.sh index 9878d93fd0..ae3afd73d2 100755 --- a/cli/test.sh +++ b/apitest/scripts/mainnet-test.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bats # -# Integration tests for bisq-cli running against a live bisq-daemon +# Smoke tests for bisq-cli running against a live bisq-daemon (on mainnet) # # Prerequisites: # -# - bats v0.4.0 must be installed (brew install bats on macOS) -# see https://github.com/sstephenson/bats/tree/v0.4.0 +# - bats-core 1.2.0+ must be installed (brew install bats-core on macOS) +# see https://github.com/bats-core/bats-core # # - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR # is empty or otherwise contains an unencrypted wallet with a 0 BTC balance @@ -48,14 +48,14 @@ run ./bisq-cli --password="xyz" getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.5" ] + [ "$output" = "1.3.7" ] } @test "test getversion" { run ./bisq-cli --password=xyz getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.5" ] + [ "$output" = "1.3.7" ] } @test "test setwalletpassword \"a b c\"" { @@ -190,8 +190,8 @@ [ "$output" = "Error: incorrect parameter count, expecting direction (buy|sell), currency code" ] } -@test "test getoffers buy eur check return status" { - run ./bisq-cli --password=xyz getoffers buy eur +@test "test getoffers sell eur check return status" { + run ./bisq-cli --password=xyz getoffers sell eur [ "$status" -eq 0 ] } diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index bf0e4c771d..d195b78701 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -311,17 +311,25 @@ public class Scaffold { bitcoinDaemon.verifyBitcoindRunning(); } + // Start Bisq apps defined by the supportingApps option, in the in proper order. + if (config.hasSupportingApp(seednode.name())) startBisqApp(seednode, executor, countdownLatch); - if (config.hasSupportingApp(arbdaemon.name(), arbdesktop.name())) - startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); + if (config.hasSupportingApp(arbdaemon.name())) + startBisqApp(arbdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(arbdesktop.name())) + startBisqApp(arbdesktop, executor, countdownLatch); - if (config.hasSupportingApp(alicedaemon.name(), alicedesktop.name())) - startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); + if (config.hasSupportingApp(alicedaemon.name())) + startBisqApp(alicedaemon, executor, countdownLatch); + else if (config.hasSupportingApp(alicedesktop.name())) + startBisqApp(alicedesktop, executor, countdownLatch); - if (config.hasSupportingApp(bobdaemon.name(), bobdesktop.name())) - startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); + if (config.hasSupportingApp(bobdaemon.name())) + startBisqApp(bobdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(bobdesktop.name())) + startBisqApp(bobdesktop, executor, countdownLatch); } private void startBisqApp(BisqAppConfig bisqAppConfig, @@ -329,28 +337,24 @@ public class Scaffold { CountDownLatch countdownLatch) throws IOException, InterruptedException { - BisqApp bisqApp; + BisqApp bisqApp = createBisqApp(bisqAppConfig); switch (bisqAppConfig) { case seednode: - bisqApp = createBisqApp(seednode); seedNodeTask = new SetupTask(bisqApp, countdownLatch); seedNodeTaskFuture = executor.submit(seedNodeTask); break; case arbdaemon: case arbdesktop: - bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon); arbNodeTask = new SetupTask(bisqApp, countdownLatch); arbNodeTaskFuture = executor.submit(arbNodeTask); break; case alicedaemon: case alicedesktop: - bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon); aliceNodeTask = new SetupTask(bisqApp, countdownLatch); aliceNodeTaskFuture = executor.submit(aliceNodeTask); break; case bobdaemon: case bobdesktop: - bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon); bobNodeTask = new SetupTask(bisqApp, countdownLatch); bobNodeTaskFuture = executor.submit(bobNodeTask); break; diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 6a9e9a6448..5197a35634 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -65,13 +65,11 @@ public class ApiTestConfig { static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; static final String API_PASSWORD = "apiPassword"; static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; - static final String RUN_ARB_NODE_AS_DESKTOP = "runArbNodeAsDesktop"; - static final String RUN_ALICE_NODE_AS_DESKTOP = "runAliceNodeAsDesktop"; - static final String RUN_BOB_NODE_AS_DESKTOP = "runBobNodeAsDesktop"; static final String BISQ_APP_INIT_TIME = "bisqAppInitTime"; static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; static final String SUPPORTING_APPS = "supportingApps"; + static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging"; // Default values for certain options static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; @@ -98,13 +96,11 @@ public class ApiTestConfig { // Daemon instances can use same gRPC password, but each needs a different apiPort. public final String apiPassword; public final boolean runSubprojectJars; - public final boolean runArbNodeAsDesktop; - public final boolean runAliceNodeAsDesktop; - public final boolean runBobNodeAsDesktop; public final long bisqAppInitTime; public final boolean skipTests; public final boolean shutdownAfterTests; public final List supportingApps; + public final boolean enableBisqDebugging; // Immutable system configurations set in the constructor. public final String bitcoinDatadir; @@ -202,27 +198,6 @@ public class ApiTestConfig { .ofType(Boolean.class) .defaultsTo(false); - ArgumentAcceptingOptionSpec runArbNodeAsDesktopOpt = - parser.accepts(RUN_ARB_NODE_AS_DESKTOP, - "Run Arbitration node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); // TODO how do I register mediator? - - ArgumentAcceptingOptionSpec runAliceNodeAsDesktopOpt = - parser.accepts(RUN_ALICE_NODE_AS_DESKTOP, - "Run Alice node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); - - ArgumentAcceptingOptionSpec runBobNodeAsDesktopOpt = - parser.accepts(RUN_BOB_NODE_AS_DESKTOP, - "Run Bob node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); - ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = parser.accepts(BISQ_APP_INIT_TIME, "Amount of time (ms) to wait on a Bisq instance's initialization") @@ -251,6 +226,12 @@ public class ApiTestConfig { .ofType(String.class) .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); + ArgumentAcceptingOptionSpec enableBisqDebuggingOpt = + parser.accepts(ENABLE_BISQ_DEBUGGING, + "Start Bisq apps with remote debug options") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); try { CompositeOptionSet options = new CompositeOptionSet(); @@ -302,13 +283,11 @@ public class ApiTestConfig { this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); this.apiPassword = options.valueOf(apiPasswordOpt); this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); - this.runArbNodeAsDesktop = options.valueOf(runArbNodeAsDesktopOpt); - this.runAliceNodeAsDesktop = options.valueOf(runAliceNodeAsDesktopOpt); - this.runBobNodeAsDesktop = options.valueOf(runBobNodeAsDesktopOpt); this.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt); this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); + this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt); // Assign values to special-case static fields. BASH_PATH_VALUE = bashPath; diff --git a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java index 662956462e..08a7531ca3 100644 --- a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java @@ -30,58 +30,64 @@ import bisq.daemon.app.BisqDaemonMain; @see dev-setup.md @see dao-setup.md */ -@SuppressWarnings("unused") public enum BisqAppConfig { seednode("bisq-BTC_REGTEST_Seed_2002", "bisq-seednode", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", SeedNodeMain.class.getName(), 2002, 5120, - -1), + -1, + 49996), arbdaemon("bisq-BTC_REGTEST_Arb_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 4444, 5121, - 9997), + 9997, + 49997), arbdesktop("bisq-BTC_REGTEST_Arb_dao", "bisq-desktop", - "\"-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 4444, 5121, - -1), + -1, + 49997), alicedaemon("bisq-BTC_REGTEST_Alice_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 7777, 5122, - 9998), + 9998, + 49998), alicedesktop("bisq-BTC_REGTEST_Alice_dao", "bisq-desktop", - "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 7777, 5122, - -1), + -1, + 49998), bobdaemon("bisq-BTC_REGTEST_Bob_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 8888, 5123, - 9999), + 9999, + 49999), bobdesktop("bisq-BTC_REGTEST_Bob_dao", "bisq-desktop", - "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 8888, 5123, - -1); + -1, + 49999); public final String appName; public final String startupScript; @@ -91,6 +97,7 @@ public enum BisqAppConfig { public final int rpcBlockNotificationPort; // Daemons can use a global gRPC password, but each needs a unique apiPort. public final int apiPort; + public final int remoteDebugPort; BisqAppConfig(String appName, String startupScript, @@ -98,7 +105,8 @@ public enum BisqAppConfig { String mainClassName, int nodePort, int rpcBlockNotificationPort, - int apiPort) { + int apiPort, + int remoteDebugPort) { this.appName = appName; this.startupScript = startupScript; this.javaOpts = javaOpts; @@ -106,6 +114,7 @@ public enum BisqAppConfig { this.nodePort = nodePort; this.rpcBlockNotificationPort = rpcBlockNotificationPort; this.apiPort = apiPort; + this.remoteDebugPort = remoteDebugPort; } @Override @@ -118,6 +127,7 @@ public enum BisqAppConfig { ", nodePort=" + nodePort + "\n" + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + ", apiPort=" + apiPort + "\n" + + ", remoteDebugPort=" + remoteDebugPort + "\n" + '}'; } } diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index f449d0b98f..7c66746f3d 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -53,6 +53,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { private final boolean useLocalhostForP2P; public final boolean useDevPrivilegeKeys; private final String findBisqPidScript; + private final String debugOpts; public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { super(bisqAppConfig.appName, config); @@ -67,6 +68,9 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { this.useDevPrivilegeKeys = true; this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest") + "/scripts/get-bisq-pid.sh"; + this.debugOpts = config.enableBisqDebugging + ? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + bisqAppConfig.remoteDebugPort + : ""; } @Override @@ -112,7 +116,6 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { if (isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName))); - return; } } catch (Exception e) { @@ -209,7 +212,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { } private String getJavaOptsSpec() { - return "export JAVA_OPTS=" + bisqAppConfig.javaOpts + "; "; + return "export JAVA_OPTS=\"" + bisqAppConfig.javaOpts + debugOpts + "\"; "; } private List getOptsList() { diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 286e7f8c20..f9100bee96 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -28,6 +28,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import bisq.apitest.config.ApiTestConfig; import bisq.apitest.method.BitcoinCliHelper; +import bisq.cli.GrpcStubs; /** * Base class for all test types: 'method', 'scenario' and 'e2e'. @@ -65,19 +66,19 @@ public class ApiTestCase { public static void setUpScaffold(String supportingApps) throws InterruptedException, ExecutionException, IOException { - // The supportingApps argument is a comma delimited string of supporting app - // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" scaffold = new Scaffold(supportingApps).setUp(); config = scaffold.config; bitcoinCli = new BitcoinCliHelper((config)); - grpcStubs = new GrpcStubs(alicedaemon, config).init(); + // For now, all grpc requests are sent to the alicedaemon, but this will need to + // be made configurable for new test cases that call arb or bob node daemons. + grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } - public static void setUpScaffold() + public static void setUpScaffold(String[] params) throws InterruptedException, ExecutionException, IOException { - scaffold = new Scaffold(new String[]{}).setUp(); + scaffold = new Scaffold(params).setUp(); config = scaffold.config; - grpcStubs = new GrpcStubs(alicedaemon, config).init(); + grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } public static void tearDownScaffold() { diff --git a/apitest/src/test/java/bisq/apitest/GrpcStubs.java b/apitest/src/test/java/bisq/apitest/GrpcStubs.java deleted file mode 100644 index 6279c61489..0000000000 --- a/apitest/src/test/java/bisq/apitest/GrpcStubs.java +++ /dev/null @@ -1,109 +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; - -import bisq.proto.grpc.GetVersionGrpc; -import bisq.proto.grpc.OffersGrpc; -import bisq.proto.grpc.PaymentAccountsGrpc; -import bisq.proto.grpc.WalletsGrpc; - -import io.grpc.CallCredentials; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; - -import java.util.concurrent.Executor; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static io.grpc.Status.UNAUTHENTICATED; -import static java.lang.String.format; -import static java.util.concurrent.TimeUnit.SECONDS; - - - -import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.config.BisqAppConfig; - -public class GrpcStubs { - - public final CallCredentials credentials; - public final String host; - public final int port; - - public GetVersionGrpc.GetVersionBlockingStub versionService; - public OffersGrpc.OffersBlockingStub offersService; - public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; - public WalletsGrpc.WalletsBlockingStub walletsService; - - public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { - this.credentials = new PasswordCallCredentials(config.apiPassword); - this.host = "localhost"; - this.port = bisqAppConfig.apiPort; - } - - public GrpcStubs init() { - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - channel.shutdown().awaitTermination(1, SECONDS); - } catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - })); - - this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - - return this; - } - - static class PasswordCallCredentials extends CallCredentials { - - public static final String PASSWORD_KEY = "password"; - private final String passwordValue; - - public PasswordCallCredentials(String passwordValue) { - if (passwordValue == null) - throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); - this.passwordValue = passwordValue; - } - - @Override - public void applyRequestMetadata(RequestInfo requestInfo, - Executor appExecutor, - MetadataApplier metadataApplier) { - appExecutor.execute(() -> { - try { - var headers = new Metadata(); - var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); - headers.put(passwordKey, passwordValue); - metadataApplier.apply(headers); - } catch (Throwable ex) { - metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); - } - }); - } - - @Override - public void thisUsesUnstableApi() { - // An experimental api. A noop but never called; tries to make it clearer to - // implementors that they may break in the future. - } - } -} diff --git a/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java new file mode 100644 index 0000000000..9ae5cf41d4 --- /dev/null +++ b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java @@ -0,0 +1,9 @@ +package bisq.asset; + +public class LiquidBitcoinAddressValidator extends RegexAddressValidator { + static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$"; + + public LiquidBitcoinAddressValidator() { + super(REGEX, "validation.altcoin.liquidBitcoin.invalidAddress"); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java index 6d77350994..a84659db90 100644 --- a/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java +++ b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java @@ -19,12 +19,12 @@ package bisq.asset.coins; import bisq.asset.AltCoinAccountDisclaimer; import bisq.asset.Coin; -import bisq.asset.RegexAddressValidator; +import bisq.asset.LiquidBitcoinAddressValidator; @AltCoinAccountDisclaimer("account.altcoin.popup.liquidbitcoin.msg") public class LiquidBitcoin extends Coin { public LiquidBitcoin() { - super("Liquid Bitcoin", "L-BTC", new RegexAddressValidator("^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$", "validation.altcoin.liquidBitcoin.invalidAddress")); + super("Liquid Bitcoin", "L-BTC", new LiquidBitcoinAddressValidator()); } } diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java b/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java new file mode 100644 index 0000000000..b5b50a8ad1 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java @@ -0,0 +1,12 @@ +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.LiquidBitcoinAddressValidator; + +public class TetherUSDLiquid extends Coin { + public TetherUSDLiquid() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (Liquid Bitcoin)", "L-USDT", new LiquidBitcoinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java b/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java new file mode 100644 index 0000000000..b0ab624d71 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java @@ -0,0 +1,12 @@ +package bisq.asset.coins; + +import bisq.asset.Base58BitcoinAddressValidator; +import bisq.asset.Coin; + +public class TetherUSDOmni extends Coin { + public TetherUSDOmni() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (Omni)", "USDT-O", new Base58BitcoinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java new file mode 100644 index 0000000000..cb57361a1f --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java @@ -0,0 +1,11 @@ +package bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class TetherUSDERC20 extends Erc20Token { + public TetherUSDERC20() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (ERC20)", "USDT-E"); + } +} diff --git a/assets/src/main/resources/META-INF/services/bisq.asset.Asset b/assets/src/main/resources/META-INF/services/bisq.asset.Asset index 80a6168463..a2f77816d6 100644 --- a/assets/src/main/resources/META-INF/services/bisq.asset.Asset +++ b/assets/src/main/resources/META-INF/services/bisq.asset.Asset @@ -104,6 +104,8 @@ bisq.asset.coins.Spectrecoin bisq.asset.coins.Starwels bisq.asset.coins.SUB1X bisq.asset.coins.TEO +bisq.asset.coins.TetherUSDLiquid +bisq.asset.coins.TetherUSDOmni bisq.asset.coins.TurtleCoin bisq.asset.coins.UnitedCommunityCoin bisq.asset.coins.Unobtanium @@ -123,6 +125,7 @@ bisq.asset.coins.ZeroClassic bisq.asset.tokens.AugmintEuro bisq.asset.tokens.DaiStablecoin bisq.asset.tokens.EtherStone +bisq.asset.tokens.TetherUSDERC20 bisq.asset.tokens.TrueUSD bisq.asset.tokens.USDCoin bisq.asset.tokens.VectorspaceAI diff --git a/build.gradle b/build.gradle index c82ff3336a..d009a6256a 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ configure(subprojects) { javaxAnnotationVersion = '1.2' jcsvVersion = '1.4.0' jetbrainsAnnotationsVersion = '13.0' - jfoenixVersion = '9.0.6' + jfoenixVersion = '9.0.10' joptVersion = '5.0.4' jsonsimpleVersion = '1.1.1' junitVersion = '4.12' diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 37dbe3f52d..f229fcd453 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -23,17 +23,12 @@ import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; -import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.LockWalletRequest; -import bisq.proto.grpc.OffersGrpc; -import bisq.proto.grpc.PaymentAccountsGrpc; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletRequest; -import bisq.proto.grpc.WalletsGrpc; -import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import joptsimple.OptionParser; @@ -43,7 +38,6 @@ import java.io.IOException; import java.io.PrintStream; import java.util.List; -import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @@ -133,21 +127,11 @@ public class CliMain { if (password == null) throw new IllegalArgumentException("missing required 'password' option"); - var credentials = new PasswordCallCredentials(password); - - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - })); - - var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + GrpcStubs grpcStubs = new GrpcStubs(host, port, password); + var versionService = grpcStubs.versionService; + var offersService = grpcStubs.offersService; + var paymentAccountsService = grpcStubs.paymentAccountsService; + var walletsService = grpcStubs.walletsService; try { switch (method) { diff --git a/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java new file mode 100644 index 0000000000..e12a6efa7c --- /dev/null +++ b/cli/src/main/java/bisq/cli/GrpcStubs.java @@ -0,0 +1,54 @@ +/* + * 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.cli; + +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.PaymentAccountsGrpc; +import bisq.proto.grpc.WalletsGrpc; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannelBuilder; + +import static java.util.concurrent.TimeUnit.SECONDS; + +public class GrpcStubs { + + public final GetVersionGrpc.GetVersionBlockingStub versionService; + public final OffersGrpc.OffersBlockingStub offersService; + public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; + public final WalletsGrpc.WalletsBlockingStub walletsService; + + public GrpcStubs(String apiHost, int apiPort, String apiPassword) { + CallCredentials credentials = new PasswordCallCredentials(apiPassword); + + var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + })); + + this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + } +} diff --git a/common/src/main/java/bisq/common/proto/ProtoUtil.java b/common/src/main/java/bisq/common/proto/ProtoUtil.java index 3d15db91f3..5f79abe02a 100644 --- a/common/src/main/java/bisq/common/proto/ProtoUtil.java +++ b/common/src/main/java/bisq/common/proto/ProtoUtil.java @@ -18,12 +18,15 @@ package bisq.common.proto; import bisq.common.Proto; +import bisq.common.util.CollectionUtils; import com.google.protobuf.ByteString; import com.google.protobuf.Message; +import com.google.protobuf.ProtocolStringList; import com.google.common.base.Enums; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -101,4 +104,9 @@ public class ProtoUtil { Function extra) { return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList()); } + + public static List protocolStringListToList(ProtocolStringList protocolStringList) { + return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList); + } + } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java index a926045df4..e27bf2493c 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -180,7 +180,7 @@ public class SignedWitnessService { public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getWitnessOwnerPubKey) - .anyMatch(ownerPubKey -> filterManager.isSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); + .anyMatch(ownerPubKey -> filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); } private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { @@ -442,7 +442,7 @@ public class SignedWitnessService { private boolean isValidSignerWitnessInternal(SignedWitness signedWitness, long childSignedWitnessDateMillis, Stack excludedPubKeys) { - if (filterManager.isSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { + if (filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { return false; } if (!verifySignature(signedWitness)) { diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 884fbf266d..4747e3feb4 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -718,13 +718,13 @@ public class AccountAgeWitnessService { filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || filterManager.isPaymentMethodBanned( PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || - filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getBuyerPaymentAccountPayload(), + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload(), new PaymentAccountFilter[1]) || - filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getSellerPaymentAccountPayload(), + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload(), new PaymentAccountFilter[1]) || - filterManager.isSignerPubKeyBanned( + filterManager.isWitnessSignerPubKeyBanned( Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || - filterManager.isSignerPubKeyBanned( + filterManager.isWitnessSignerPubKeyBanned( Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes())); return !isFiltered; } diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 567550682b..9307e25b92 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -96,6 +96,7 @@ public class BisqHeadlessApp implements HeadlessApp { bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString())); bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); //TODO move to bisqSetup corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 18873821a0..764b427f66 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -44,6 +44,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts; import bisq.core.notifications.alerts.price.PriceAlert; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.payment.TradeLimits; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; @@ -221,6 +222,9 @@ public class BisqSetup { @Setter @Nullable private Runnable showPopupIfInvalidBtcConfigHandler; + @Setter + @Nullable + private Consumer> revolutAccountsUpdateHandler; @Getter final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); @@ -824,6 +828,8 @@ public class BisqSetup { priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); + user.onAllServicesInitialized(revolutAccountsUpdateHandler); + allBasicServicesInitialized = true; } diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java index 1f841e66cb..b31875323a 100644 --- a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -164,11 +164,13 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements UserThread.runAfter(() -> { // We check every hour if we are in the target hour. UserThread.runPeriodically(() -> { - int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT0")).getHour(); + int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).getHour(); if (currentHour == target) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + - "Shut down node at hour {}" + - "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", target); + "Shut down node at hour {} (UTC time is {})" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + target, + ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toString()); shutDown(gracefulShutDownHandler); } }, TimeUnit.MINUTES.toSeconds(10)); diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java index 69d6c7ea63..8300c76b23 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java @@ -156,7 +156,7 @@ public abstract class StateNetworkService listener.onNewBlockReceived(newBlockBroadcastMessage)); } else { log.debug("We had that message already and do not further broadcast it. extBlockId={}", extBlockId); diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 99b7b2526e..d46c88862e 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -21,8 +21,10 @@ import bisq.network.p2p.storage.payload.ExpirablePayload; import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.common.crypto.Sig; +import bisq.common.proto.ProtoUtil; import bisq.common.util.CollectionUtils; import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; import com.google.protobuf.ByteString; @@ -30,152 +32,140 @@ import com.google.common.annotations.VisibleForTesting; import java.security.PublicKey; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j -@Getter -@EqualsAndHashCode -@ToString +@Value public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final List bannedOfferIds; private final List bannedNodeAddress; private final List bannedPaymentAccounts; - - // Because we added those fields in v 0.5.4 and old versions do not have it we annotate it with @Nullable - @Nullable private final List bannedCurrencies; - @Nullable private final List bannedPaymentMethods; - - // added in v0.6.0 - @Nullable private final List arbitrators; - @Nullable private final List seedNodes; - @Nullable private final List priceRelayNodes; private final boolean preventPublicBtcNetwork; - - // added in v0.6.2 - @Nullable private final List btcNodes; + // SignatureAsBase64 is not set initially as we use the serialized data for signing. We set it after signature is + // created by cloning the object with a non-null sig. + @Nullable + private final String signatureAsBase64; + // The pub EC key from the dev who has signed and published the filter (different to ownerPubKeyBytes) + private final String signerPubKeyAsHex; + // The pub key used for the data protection in the p2p storage + private final byte[] ownerPubKeyBytes; + private final boolean disableDao; + private final String disableDaoBelowVersion; + private final String disableTradeBelowVersion; + private final List mediators; + private final List refundAgents; + + private final List bannedAccountWitnessSignerPubKeys; + + private final List btcFeeReceiverAddresses; + + private final long creationDate; + + private final List bannedPrivilegedDevPubKeys; - private String signatureAsBase64; - private byte[] ownerPubKeyBytes; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable private Map extraDataMap; - private PublicKey ownerPubKey; - // added in v0.9.4 - private final boolean disableDao; + private transient PublicKey ownerPubKey; - // added in v0.9.8 - @Nullable - private final String disableDaoBelowVersion; - @Nullable - private final String disableTradeBelowVersion; - - // added in v1.1.6 - @Nullable - private final List mediators; - - // added in v1.2.0 - @Nullable - private final List refundAgents; - - // added in v1.2.x - @Nullable - private final List bannedSignerPubKeys; - - // added in v1.3.2 - @Nullable - private final List btcFeeReceiverAddresses; - - // added after v1.3.7 + // added at v1.3.8 private final boolean disableAutoConf; - public Filter(List bannedOfferIds, - List bannedNodeAddress, - List bannedPaymentAccounts, - @Nullable List bannedCurrencies, - @Nullable List bannedPaymentMethods, - @Nullable List arbitrators, - @Nullable List seedNodes, - @Nullable List priceRelayNodes, - boolean preventPublicBtcNetwork, - @Nullable List btcNodes, - boolean disableDao, - @Nullable String disableDaoBelowVersion, - @Nullable String disableTradeBelowVersion, - @Nullable List mediators, - @Nullable List refundAgents, - @Nullable List bannedSignerPubKeys, - @Nullable List btcFeeReceiverAddresses, - boolean disableAutoConf) { - this.bannedOfferIds = bannedOfferIds; - this.bannedNodeAddress = bannedNodeAddress; - this.bannedPaymentAccounts = bannedPaymentAccounts; - this.bannedCurrencies = bannedCurrencies; - this.bannedPaymentMethods = bannedPaymentMethods; - this.arbitrators = arbitrators; - this.seedNodes = seedNodes; - this.priceRelayNodes = priceRelayNodes; - this.preventPublicBtcNetwork = preventPublicBtcNetwork; - this.btcNodes = btcNodes; - this.disableDao = disableDao; - this.disableDaoBelowVersion = disableDaoBelowVersion; - this.disableTradeBelowVersion = disableTradeBelowVersion; - this.mediators = mediators; - this.refundAgents = refundAgents; - this.bannedSignerPubKeys = bannedSignerPubKeys; - this.btcFeeReceiverAddresses = btcFeeReceiverAddresses; - this.disableAutoConf = disableAutoConf; + // After we have created the signature from the filter data we clone it and apply the signature + static Filter cloneWithSig(Filter filter, String signatureAsBase64) { + return new Filter(filter.getBannedOfferIds(), + filter.getBannedNodeAddress(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedAccountWitnessSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + signatureAsBase64, + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys(), + filter.isDisableAutoConf()); } + // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again + static Filter cloneWithoutSig(Filter filter) { + return new Filter(filter.getBannedOfferIds(), + filter.getBannedNodeAddress(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedAccountWitnessSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + null, + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys(), + filter.isDisableAutoConf()); + } - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @VisibleForTesting public Filter(List bannedOfferIds, List bannedNodeAddress, List bannedPaymentAccounts, - @Nullable List bannedCurrencies, - @Nullable List bannedPaymentMethods, - @Nullable List arbitrators, - @Nullable List seedNodes, - @Nullable List priceRelayNodes, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, boolean preventPublicBtcNetwork, - @Nullable List btcNodes, + List btcNodes, boolean disableDao, - @Nullable String disableDaoBelowVersion, - @Nullable String disableTradeBelowVersion, - String signatureAsBase64, - byte[] ownerPubKeyBytes, - @Nullable Map extraDataMap, - @Nullable List mediators, - @Nullable List refundAgents, - @Nullable List bannedSignerPubKeys, - @Nullable List btcFeeReceiverAddresses, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedAccountWitnessSignerPubKeys, + List btcFeeReceiverAddresses, + PublicKey ownerPubKey, + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys, boolean disableAutoConf) { this(bannedOfferIds, bannedNodeAddress, @@ -192,76 +182,146 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { disableTradeBelowVersion, mediators, refundAgents, - bannedSignerPubKeys, + bannedAccountWitnessSignerPubKeys, btcFeeReceiverAddresses, + Sig.getPublicKeyBytes(ownerPubKey), + System.currentTimeMillis(), + null, + null, + signerPubKeyAsHex, + bannedPrivilegedDevPubKeys, disableAutoConf); - this.signatureAsBase64 = signatureAsBase64; - this.ownerPubKeyBytes = ownerPubKeyBytes; - this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + } - ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @VisibleForTesting + public Filter(List bannedOfferIds, + List bannedNodeAddress, + List bannedPaymentAccounts, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, + boolean preventPublicBtcNetwork, + List btcNodes, + boolean disableDao, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedAccountWitnessSignerPubKeys, + List btcFeeReceiverAddresses, + byte[] ownerPubKeyBytes, + long creationDate, + @Nullable Map extraDataMap, + @Nullable String signatureAsBase64, + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys, + boolean disableAutoConf) { + this.bannedOfferIds = bannedOfferIds; + this.bannedNodeAddress = bannedNodeAddress; + this.bannedPaymentAccounts = bannedPaymentAccounts; + this.bannedCurrencies = bannedCurrencies; + this.bannedPaymentMethods = bannedPaymentMethods; + this.arbitrators = arbitrators; + this.seedNodes = seedNodes; + this.priceRelayNodes = priceRelayNodes; + this.preventPublicBtcNetwork = preventPublicBtcNetwork; + this.btcNodes = btcNodes; + this.disableDao = disableDao; + this.disableDaoBelowVersion = disableDaoBelowVersion; + this.disableTradeBelowVersion = disableTradeBelowVersion; + this.mediators = mediators; + this.refundAgents = refundAgents; + this.bannedAccountWitnessSignerPubKeys = bannedAccountWitnessSignerPubKeys; + this.btcFeeReceiverAddresses = btcFeeReceiverAddresses; + this.disableAutoConf = disableAutoConf; + this.ownerPubKeyBytes = ownerPubKeyBytes; + this.creationDate = creationDate; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + this.signatureAsBase64 = signatureAsBase64; + this.signerPubKeyAsHex = signerPubKeyAsHex; + this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; + + // ownerPubKeyBytes can be null when called from tests + if (ownerPubKeyBytes != null) { + ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + } else { + ownerPubKey = null; + } } @Override public protobuf.StoragePayload toProtoMessage() { - checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); - checkNotNull(ownerPubKeyBytes, "ownerPubKeyBytes must not be null"); List paymentAccountFilterList = bannedPaymentAccounts.stream() .map(PaymentAccountFilter::toProtoMessage) .collect(Collectors.toList()); - final protobuf.Filter.Builder builder = protobuf.Filter.newBuilder() - .addAllBannedOfferIds(bannedOfferIds) + + protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds) .addAllBannedNodeAddress(bannedNodeAddress) .addAllBannedPaymentAccounts(paymentAccountFilterList) - .setSignatureAsBase64(signatureAsBase64) - .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .addAllBannedCurrencies(bannedCurrencies) + .addAllBannedPaymentMethods(bannedPaymentMethods) + .addAllArbitrators(arbitrators) + .addAllSeedNodes(seedNodes) + .addAllPriceRelayNodes(priceRelayNodes) .setPreventPublicBtcNetwork(preventPublicBtcNetwork) + .addAllBtcNodes(btcNodes) .setDisableDao(disableDao) + .setDisableDaoBelowVersion(disableDaoBelowVersion) + .setDisableTradeBelowVersion(disableTradeBelowVersion) + .addAllMediators(mediators) + .addAllRefundAgents(refundAgents) + .addAllBannedSignerPubKeys(bannedAccountWitnessSignerPubKeys) + .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) + .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .setSignerPubKeyAsHex(signerPubKeyAsHex) + .setCreationDate(creationDate) + .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys) .setDisableAutoConf(disableAutoConf); - Optional.ofNullable(bannedCurrencies).ifPresent(builder::addAllBannedCurrencies); - Optional.ofNullable(bannedPaymentMethods).ifPresent(builder::addAllBannedPaymentMethods); - Optional.ofNullable(arbitrators).ifPresent(builder::addAllArbitrators); - Optional.ofNullable(seedNodes).ifPresent(builder::addAllSeedNodes); - Optional.ofNullable(priceRelayNodes).ifPresent(builder::addAllPriceRelayNodes); - Optional.ofNullable(btcNodes).ifPresent(builder::addAllBtcNodes); - Optional.ofNullable(disableDaoBelowVersion).ifPresent(builder::setDisableDaoBelowVersion); - Optional.ofNullable(disableTradeBelowVersion).ifPresent(builder::setDisableTradeBelowVersion); + Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); - Optional.ofNullable(mediators).ifPresent(builder::addAllMediators); - Optional.ofNullable(refundAgents).ifPresent(builder::addAllRefundAgents); - Optional.ofNullable(bannedSignerPubKeys).ifPresent(builder::addAllBannedSignerPubKeys); - Optional.ofNullable(btcFeeReceiverAddresses).ifPresent(builder::addAllBtcFeeReceiverAddresses); return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); } public static Filter fromProto(protobuf.Filter proto) { - return new Filter(new ArrayList<>(proto.getBannedOfferIdsList()), - new ArrayList<>(proto.getBannedNodeAddressList()), - proto.getBannedPaymentAccountsList().stream() - .map(PaymentAccountFilter::fromProto) - .collect(Collectors.toList()), - CollectionUtils.isEmpty(proto.getBannedCurrenciesList()) ? null : new ArrayList<>(proto.getBannedCurrenciesList()), - CollectionUtils.isEmpty(proto.getBannedPaymentMethodsList()) ? null : new ArrayList<>(proto.getBannedPaymentMethodsList()), - CollectionUtils.isEmpty(proto.getArbitratorsList()) ? null : new ArrayList<>(proto.getArbitratorsList()), - CollectionUtils.isEmpty(proto.getSeedNodesList()) ? null : new ArrayList<>(proto.getSeedNodesList()), - CollectionUtils.isEmpty(proto.getPriceRelayNodesList()) ? null : new ArrayList<>(proto.getPriceRelayNodesList()), + List bannedPaymentAccountsList = proto.getBannedPaymentAccountsList().stream() + .map(PaymentAccountFilter::fromProto) + .collect(Collectors.toList()); + + + return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()), + ProtoUtil.protocolStringListToList(proto.getBannedNodeAddressList()), + bannedPaymentAccountsList, + ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()), + ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()), + ProtoUtil.protocolStringListToList(proto.getArbitratorsList()), + ProtoUtil.protocolStringListToList(proto.getSeedNodesList()), + ProtoUtil.protocolStringListToList(proto.getPriceRelayNodesList()), proto.getPreventPublicBtcNetwork(), - CollectionUtils.isEmpty(proto.getBtcNodesList()) ? null : new ArrayList<>(proto.getBtcNodesList()), + ProtoUtil.protocolStringListToList(proto.getBtcNodesList()), proto.getDisableDao(), - proto.getDisableDaoBelowVersion().isEmpty() ? null : proto.getDisableDaoBelowVersion(), - proto.getDisableTradeBelowVersion().isEmpty() ? null : proto.getDisableTradeBelowVersion(), - proto.getSignatureAsBase64(), + proto.getDisableDaoBelowVersion(), + proto.getDisableTradeBelowVersion(), + ProtoUtil.protocolStringListToList(proto.getMediatorsList()), + ProtoUtil.protocolStringListToList(proto.getRefundAgentsList()), + ProtoUtil.protocolStringListToList(proto.getBannedSignerPubKeysList()), + ProtoUtil.protocolStringListToList(proto.getBtcFeeReceiverAddressesList()), proto.getOwnerPubKeyBytes().toByteArray(), + proto.getCreationDate(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), - CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList()), - CollectionUtils.isEmpty(proto.getRefundAgentsList()) ? null : new ArrayList<>(proto.getRefundAgentsList()), - CollectionUtils.isEmpty(proto.getBannedSignerPubKeysList()) ? - null : new ArrayList<>(proto.getBannedSignerPubKeysList()), - CollectionUtils.isEmpty(proto.getBtcFeeReceiverAddressesList()) ? null : - new ArrayList<>(proto.getBtcFeeReceiverAddressesList()), - proto.getDisableAutoConf()); + proto.getSignatureAsBase64(), + proto.getSignerPubKeyAsHex(), + ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()), + proto.getDisableAutoConf() + ); } @@ -274,13 +334,6 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { return TimeUnit.DAYS.toMillis(180); } - void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { - this.signatureAsBase64 = signatureAsBase64; - this.ownerPubKey = ownerPubKey; - - ownerPubKeyBytes = Sig.getPublicKeyBytes(this.ownerPubKey); - } - @Override public String toString() { return "Filter{" + @@ -294,15 +347,20 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n priceRelayNodes=" + priceRelayNodes + ",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork + ",\n btcNodes=" + btcNodes + - ",\n extraDataMap=" + extraDataMap + + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' + + ",\n signerPubKeyAsHex='" + signerPubKeyAsHex + '\'' + + ",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) + ",\n disableDao=" + disableDao + ",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' + ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + ",\n mediators=" + mediators + ",\n refundAgents=" + refundAgents + - ",\n bannedSignerPubKeys=" + bannedSignerPubKeys + + ",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys + + ",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + ",\n disableAutoConf=" + disableAutoConf + + ",\n creationDate=" + creationDate + + ",\n extraDataMap=" + extraDataMap + "\n}"; } } diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index f903638532..0302fc24f7 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -29,9 +29,7 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PServiceListener; import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; -import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.app.Version; import bisq.common.config.Config; @@ -39,7 +37,7 @@ import bisq.common.config.ConfigFileEditor; import bisq.common.crypto.KeyRing; import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.Utils; +import org.bitcoinj.core.Sha256Hash; import javax.inject.Inject; import javax.inject.Named; @@ -47,33 +45,36 @@ import javax.inject.Named; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import java.security.SignatureException; +import org.spongycastle.util.encoders.Base64; + +import java.security.PublicKey; + +import java.nio.charset.StandardCharsets; import java.math.BigInteger; -import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; import java.lang.reflect.Method; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkNotNull; import static org.bitcoinj.core.Utils.HEX; +/** + * We only support one active filter, if we receive multiple we use the one with the more recent creationDate. + */ +@Slf4j public class FilterManager { - - private static final Logger log = LoggerFactory.getLogger(FilterManager.class); - - public static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; - public static final String BANNED_SEED_NODES = "bannedSeedNodes"; - public static final String BANNED_BTC_NODES = "bannedBtcNodes"; + private static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; + private static final String BANNED_SEED_NODES = "bannedSeedNodes"; + private static final String BANNED_BTC_NODES = "bannedBtcNodes"; /////////////////////////////////////////////////////////////////////////////////////////// @@ -90,16 +91,15 @@ public class FilterManager { private final Preferences preferences; private final ConfigFileEditor configFileEditor; private final ProvidersRepository providersRepository; - private boolean ignoreDevMsg; + private final boolean ignoreDevMsg; private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); private final List listeners = new CopyOnWriteArrayList<>(); - - private final String pubKeyAsHex; + private final List publicKeys; private ECKey filterSigningKey; /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, Initialization + // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject @@ -118,48 +118,53 @@ public class FilterManager { this.configFileEditor = new ConfigFileEditor(config.configFile); this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; - pubKeyAsHex = useDevPrivilegeKeys ? - DevEnv.DEV_PRIVILEGE_PUB_KEY : - "022ac7b7766b0aedff82962522c2c14fb8d1961dabef6e5cfd10edc679456a32f1"; + + publicKeys = useDevPrivilegeKeys ? + Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : + List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", + "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", + "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + public void onAllServicesInitialized() { - if (!ignoreDevMsg) { - - final List list = new ArrayList<>(p2PService.getP2PDataStorage().getMap().values()); - list.forEach(e -> { - final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload(); - if (protectedStoragePayload instanceof Filter) - addFilter((Filter) protectedStoragePayload); - }); - - p2PService.addHashSetChangedListener(new HashMapChangedListener() { - @Override - public void onAdded(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) { - Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - boolean wasValid = addFilter(filter); - if (!wasValid) { - UserThread.runAfter(() -> p2PService.getP2PDataStorage().removeInvalidProtectedStorageEntry(protectedStorageEntry), 1); - } - } - }); - } - - @Override - public void onRemoved(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) { - Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (verifySignature(filter) && getFilter().equals(filter)) - resetFilters(); - } - }); - } - }); + if (ignoreDevMsg) { + return; } + p2PService.getP2PDataStorage().getMap().values().stream() + .map(ProtectedStorageEntry::getProtectedStoragePayload) + .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) + .map(protectedStoragePayload -> (Filter) protectedStoragePayload) + .forEach(this::onFilterAddedFromNetwork); + + p2PService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + onFilterAddedFromNetwork(filter); + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + onFilterRemovedFromNetwork(filter); + }); + } + }); + p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onDataReceived() { @@ -175,12 +180,12 @@ public class FilterManager { @Override public void onUpdatedDataReceived() { - // We should have received all data at that point and if the filers were not set we - // clean up as it might be that we missed the filter remove message if we have not been online. - UserThread.runAfter(() -> { - if (filterProperty.get() == null) - resetFilters(); - }, 1); + // We should have received all data at that point and if the filters were not set we + // clean up the persisted banned nodes in the options file as it might be that we missed the filter + // remove message if we have not been online. + if (filterProperty.get() == null) { + clearBannedNodes(); + } } @Override @@ -201,54 +206,94 @@ public class FilterManager { }); } - private void resetFilters() { - saveBannedNodes(BANNED_BTC_NODES, null); - saveBannedNodes(BANNED_SEED_NODES, null); - saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); - - if (providersRepository.getBannedNodes() != null) - providersRepository.applyBannedNodes(null); - - filterProperty.set(null); - } - - private boolean addFilter(Filter filter) { - if (verifySignature(filter)) { - // Seed nodes are requested at startup before we get the filter so we only apply the banned - // nodes at the next startup and don't update the list in the P2P network domain. - // We persist it to the property file which is read before any other initialisation. - saveBannedNodes(BANNED_SEED_NODES, filter.getSeedNodes()); - saveBannedNodes(BANNED_BTC_NODES, filter.getBtcNodes()); - - // Banned price relay nodes we can apply at runtime - final List priceRelayNodes = filter.getPriceRelayNodes(); - saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); - - providersRepository.applyBannedNodes(priceRelayNodes); - - filterProperty.set(filter); - listeners.forEach(e -> e.onFilterAdded(filter)); - - if (filter.isPreventPublicBtcNetwork() && - preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) - preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); - return true; - } else { + public boolean isPrivilegedDevPubKeyBanned(String pubKeyAsHex) { + Filter filter = getFilter(); + if (filter == null) { return false; } + + return filter.getBannedPrivilegedDevPubKeys().contains(pubKeyAsHex); } - private void saveBannedNodes(String optionName, List bannedNodes) { - if (bannedNodes != null) - configFileEditor.setOption(optionName, String.join(",", bannedNodes)); - else - configFileEditor.clearOption(optionName); + public boolean canAddDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { + return false; + } + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("Key in invalid"); + return false; + } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } + return true; } + public String getSignerPubKeyAsHex(String privKeyString) { + ECKey ecKey = toECKey(privKeyString); + return getPubKeyAsHex(ecKey); + } - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// + public void addDevFilter(Filter filterWithoutSig, String privKeyString) { + setFilterSigningKey(privKeyString); + String signatureAsBase64 = getSignature(filterWithoutSig); + Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); + user.setDevelopersFilter(filterWithSig); + + p2PService.addProtectedStorageEntry(filterWithSig); + } + + public boolean canRemoveDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { + return false; + } + + Filter developersFilter = getDevFilter(); + if (developersFilter == null) { + log.warn("There is no persisted dev filter to be removed."); + return false; + } + + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("Key in invalid."); + return false; + } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (!developersFilter.getSignerPubKeyAsHex().equals(pubKeyAsHex)) { + log.warn("pubKeyAsHex derived from private key does not match filterSignerPubKey. " + + "filterSignerPubKey={}, pubKeyAsHex derived from private key={}", + developersFilter.getSignerPubKeyAsHex(), pubKeyAsHex); + return false; + } + + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } + + return true; + } + + public void removeDevFilter(String privKeyString) { + setFilterSigningKey(privKeyString); + Filter filterWithSig = user.getDevelopersFilter(); + if (filterWithSig == null) { + // Should not happen as UI button is deactivated in that case + return; + } + + if (p2PService.removeData(filterWithSig)) { + user.setDevelopersFilter(null); + } else { + log.warn("Removing dev filter from network failed"); + } + } public void addListener(Listener listener) { listeners.add(listener); @@ -263,85 +308,15 @@ public class FilterManager { return filterProperty.get(); } - public boolean addFilterMessageIfKeyIsValid(Filter filter, String privKeyString) { - // if there is a previous message we remove that first - if (user.getDevelopersFilter() != null) - removeFilterMessageIfKeyIsValid(privKeyString); - - boolean isKeyValid = isKeyValid(privKeyString); - if (isKeyValid) { - signAndAddSignatureToFilter(filter); - user.setDevelopersFilter(filter); - - boolean result = p2PService.addProtectedStorageEntry(filter); - if (result) - log.trace("Add filter to network was successful. FilterMessage = {}", filter); - - } - return isKeyValid; - } - - public boolean removeFilterMessageIfKeyIsValid(String privKeyString) { - if (isKeyValid(privKeyString)) { - Filter filter = user.getDevelopersFilter(); - if (filter == null) { - log.warn("Developers filter is null"); - } else if (p2PService.removeData(filter)) { - log.trace("Remove filter from network was successful. FilterMessage = {}", filter); - user.setDevelopersFilter(null); - } else { - log.warn("Filter remove failed"); - } - return true; - } else { - return false; - } - } - - private boolean isKeyValid(String privKeyString) { - try { - filterSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); - return pubKeyAsHex.equals(Utils.HEX.encode(filterSigningKey.getPubKey())); - } catch (Throwable t) { - return false; - } - } - - private void signAndAddSignatureToFilter(Filter filter) { - filter.setSigAndPubKey(filterSigningKey.signMessage(getHexFromData(filter)), keyRing.getSignatureKeyPair().getPublic()); - } - - private boolean verifySignature(Filter filter) { - try { - ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(getHexFromData(filter), filter.getSignatureAsBase64()); - return true; - } catch (SignatureException e) { - log.warn("verifySignature failed. filter={}", filter); - return false; - } - } - - // We don't use full data from Filter as we are only interested in the filter data not the sig and keys - private String getHexFromData(Filter filter) { - protobuf.Filter.Builder builder = protobuf.Filter.newBuilder() - .addAllBannedOfferIds(filter.getBannedOfferIds()) - .addAllBannedNodeAddress(filter.getBannedNodeAddress()) - .addAllBannedPaymentAccounts(filter.getBannedPaymentAccounts().stream() - .map(PaymentAccountFilter::toProtoMessage) - .collect(Collectors.toList())); - - Optional.ofNullable(filter.getBannedCurrencies()).ifPresent(builder::addAllBannedCurrencies); - Optional.ofNullable(filter.getBannedPaymentMethods()).ifPresent(builder::addAllBannedPaymentMethods); - Optional.ofNullable(filter.getBannedSignerPubKeys()).ifPresent(builder::addAllBannedSignerPubKeys); - - return Utils.HEX.encode(builder.build().toByteArray()); - } - @Nullable - public Filter getDevelopersFilter() { + public Filter getDevFilter() { return user.getDevelopersFilter(); } + public PublicKey getOwnerPubKey() { + return keyRing.getSignatureKeyPair().getPublic(); + } + public boolean isCurrencyBanned(String currencyCode) { return getFilter() != null && getFilter().getBannedCurrencies() != null && @@ -396,8 +371,8 @@ public class FilterManager { return requireUpdateToNewVersion; } - public boolean isPeersPaymentAccountDataAreBanned(PaymentAccountPayload paymentAccountPayload, - PaymentAccountFilter[] appliedPaymentAccountFilter) { + public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload, + PaymentAccountFilter[] appliedPaymentAccountFilter) { return getFilter() != null && getFilter().getBannedPaymentAccounts().stream() .anyMatch(paymentAccountFilter -> { @@ -419,11 +394,183 @@ public class FilterManager { }); } - public boolean isSignerPubKeyBanned(String signerPubKeyAsHex) { + public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) { return getFilter() != null && - getFilter().getBannedSignerPubKeys() != null && - getFilter().getBannedSignerPubKeys().stream() - .anyMatch(e -> e.equals(signerPubKeyAsHex)); + getFilter().getBannedAccountWitnessSignerPubKeys() != null && + getFilter().getBannedAccountWitnessSignerPubKeys().stream() + .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onFilterAddedFromNetwork(Filter newFilter) { + Filter currentFilter = getFilter(); + + if (!isFilterPublicKeyInList(newFilter)) { + log.warn("isFilterPublicKeyInList failed. Filter={}", newFilter); + return; + } + if (!isSignatureValid(newFilter)) { + log.warn("verifySignature failed. Filter={}", newFilter); + return; + } + + if (currentFilter != null) { + if (currentFilter.getCreationDate() > newFilter.getCreationDate()) { + log.warn("We received a new filter from the network but the creation date is older than the " + + "filter we have already. We ignore the new filter.\n" + + "New filer={}\n" + + "Old filter={}", + newFilter, filterProperty.get()); + return; + } + + if (isPrivilegedDevPubKeyBanned(newFilter.getSignerPubKeyAsHex())) { + log.warn("Pub key of filter is banned. currentFilter={}, newFilter={}", currentFilter, newFilter); + return; + } + } + + // Our new filter is newer so we apply it. + // We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key + // for deploying filters and this is only in place to avoid unintended situations of multiple filters + // from multiple devs or if same dev publishes new filter from different app without the persisted devFilter. + filterProperty.set(newFilter); + + // Seed nodes are requested at startup before we get the filter so we only apply the banned + // nodes at the next startup and don't update the list in the P2P network domain. + // We persist it to the property file which is read before any other initialisation. + saveBannedNodes(BANNED_SEED_NODES, newFilter.getSeedNodes()); + saveBannedNodes(BANNED_BTC_NODES, newFilter.getBtcNodes()); + + // Banned price relay nodes we can apply at runtime + List priceRelayNodes = newFilter.getPriceRelayNodes(); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); + + //TODO should be moved to client with listening on onFilterAdded + providersRepository.applyBannedNodes(priceRelayNodes); + + //TODO should be moved to client with listening on onFilterAdded + if (newFilter.isPreventPublicBtcNetwork() && + preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) { + preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); + } + + listeners.forEach(e -> e.onFilterAdded(newFilter)); + } + + private void onFilterRemovedFromNetwork(Filter filter) { + if (!isFilterPublicKeyInList(filter)) { + log.warn("isFilterPublicKeyInList failed. Filter={}", filter); + return; + } + if (!isSignatureValid(filter)) { + log.warn("verifySignature failed. Filter={}", filter); + return; + } + + // We don't check for banned filter as we want to remove a banned filter anyway. + + if (!filterProperty.get().equals(filter)) { + return; + } + + clearBannedNodes(); + + if (filter.equals(user.getDevelopersFilter())) { + user.setDevelopersFilter(null); + } + filterProperty.set(null); + } + + // Clears options files from banned nodes + private void clearBannedNodes() { + saveBannedNodes(BANNED_BTC_NODES, null); + saveBannedNodes(BANNED_SEED_NODES, null); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); + + if (providersRepository.getBannedNodes() != null) { + providersRepository.applyBannedNodes(null); + } + } + + private void saveBannedNodes(String optionName, List bannedNodes) { + if (bannedNodes != null) + configFileEditor.setOption(optionName, String.join(",", bannedNodes)); + else + configFileEditor.clearOption(optionName); + } + + private boolean isValidDevPrivilegeKey(String privKeyString) { + try { + ECKey filterSigningKey = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(filterSigningKey); + return isPublicKeyInList(pubKeyAsHex); + } catch (Throwable t) { + return false; + } + } + + private void setFilterSigningKey(String privKeyString) { + this.filterSigningKey = toECKey(privKeyString); + } + + private String getSignature(Filter filterWithoutSig) { + Sha256Hash hash = getSha256Hash(filterWithoutSig); + ECKey.ECDSASignature ecdsaSignature = filterSigningKey.sign(hash); + byte[] encodeToDER = ecdsaSignature.encodeToDER(); + return new String(Base64.encode(encodeToDER), StandardCharsets.UTF_8); + } + + private boolean isFilterPublicKeyInList(Filter filter) { + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + if (!isPublicKeyInList(signerPubKeyAsHex)) { + log.warn("signerPubKeyAsHex from filter is not part of our pub key list. filter={}, publicKeys={}", filter, publicKeys); + return false; + } + return true; + } + + private boolean isPublicKeyInList(String pubKeyAsHex) { + boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); + if (!isPublicKeyInList) { + log.warn("pubKeyAsHex is not part of our pub key list. pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); + } + return isPublicKeyInList; + } + + private boolean isSignatureValid(Filter filter) { + try { + Filter filterForSigVerification = Filter.cloneWithoutSig(filter); + Sha256Hash hash = getSha256Hash(filterForSigVerification); + + checkNotNull(filter.getSignatureAsBase64(), "filter.getSignatureAsBase64() must not be null"); + byte[] sigData = Base64.decode(filter.getSignatureAsBase64()); + ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(sigData); + + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + byte[] decode = HEX.decode(signerPubKeyAsHex); + ECKey ecPubKey = ECKey.fromPublicOnly(decode); + return ecPubKey.verify(hash, ecdsaSignature); + } catch (Throwable e) { + log.warn("verifySignature failed. filter={}", filter); + return false; + } + } + + private ECKey toECKey(String privKeyString) { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + } + + private Sha256Hash getSha256Hash(Filter filter) { + byte[] filterData = filter.toProtoMessage().toByteArray(); + return Sha256Hash.of(filterData); + } + + private String getPubKeyAsHex(ECKey ecKey) { + return HEX.encode(ecKey.getPubKey()); + } } diff --git a/core/src/main/java/bisq/core/monetary/Price.java b/core/src/main/java/bisq/core/monetary/Price.java index 6e58835368..e07a896efe 100644 --- a/core/src/main/java/bisq/core/monetary/Price.java +++ b/core/src/main/java/bisq/core/monetary/Price.java @@ -129,7 +129,9 @@ public class Price extends MonetaryWrapper implements Comparable { } public String toFriendlyString() { - return monetary instanceof Altcoin ? ((Altcoin) monetary).toFriendlyString() : ((Fiat) monetary).toFriendlyString(); + return monetary instanceof Altcoin ? + ((Altcoin) monetary).toFriendlyString() + "/BTC" : + ((Fiat) monetary).toFriendlyString().replace(((Fiat) monetary).currencyCode, "") + "BTC/" + ((Fiat) monetary).currencyCode; } public String toPlainString() { diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index 3f4411d87a..7b82506690 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -48,6 +48,9 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +// OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a +// backward compatible way. Maybe a candidate when segwit activation is done as hardfork? + @EqualsAndHashCode @Getter @Slf4j diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java index 2afd0b6694..07282769f6 100644 --- a/core/src/main/java/bisq/core/payment/RevolutAccount.java +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -36,11 +36,15 @@ public final class RevolutAccount extends PaymentAccount { return new RevolutAccountPayload(paymentMethod.getId(), id); } - public void setAccountId(String accountId) { - ((RevolutAccountPayload) paymentAccountPayload).setAccountId(accountId); + public void setUserName(String userName) { + ((RevolutAccountPayload) paymentAccountPayload).setUserName(userName); } - public String getAccountId() { - return ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); + public String getUserName() { + return ((RevolutAccountPayload) paymentAccountPayload).getUserName(); + } + + public boolean userNameNotSet() { + return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet(); } } diff --git a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java index 24856d1405..7c8926ab10 100644 --- a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -19,27 +19,37 @@ package bisq.core.payment.payload; import bisq.core.locale.Res; +import bisq.common.proto.ProtoUtil; + import com.google.protobuf.Message; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; + @EqualsAndHashCode(callSuper = true) @ToString -@Setter -@Getter @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { + // Not used anymore from outside. Only used as internal Id to not break existing account witness objects private String accountId = ""; + // Was added in 1.3.8 + // To not break signed accounts we keep accountId as internal id used for signing. + // Old accounts get a popup to add the new required field userName but accountId is + // left unchanged. Newly created accounts fill accountId with the value of userName. + // In the UI we only use userName. + @Nullable + private String userName = null; + public RevolutAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } @@ -52,6 +62,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { private RevolutAccountPayload(String paymentMethod, String id, String accountId, + @Nullable String userName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -60,20 +71,24 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.accountId = accountId; + this.userName = userName; } @Override public Message toProtoMessage() { - return getPaymentAccountPayloadBuilder() - .setRevolutAccountPayload(protobuf.RevolutAccountPayload.newBuilder() - .setAccountId(accountId)) - .build(); + protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() + .setAccountId(accountId); + Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName); + return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } + public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload(); return new RevolutAccountPayload(proto.getPaymentMethodId(), proto.getId(), - proto.getRevolutAccountPayload().getAccountId(), + revolutAccountPayload.getAccountId(), + ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -85,7 +100,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + getUserName(); } @Override @@ -95,6 +110,24 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public byte[] getAgeWitnessInputData() { + // getAgeWitnessInputData is called at new account creation when accountId is empty string. return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); } + + public void setUserName(@Nullable String userName) { + this.userName = userName; + // We only set accountId to userName for new accounts. Existing accounts have accountId set with email + // or phone nr. and we keep that to not break account signing. + if (accountId.isEmpty()) { + accountId = userName; + } + } + + public String getUserName() { + return userName != null ? userName : accountId; + } + + public boolean userNameNotSet() { + return userName == null; + } } diff --git a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java index 4d2928d5b8..75128ea59b 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java @@ -84,7 +84,6 @@ public class PriceFeedService { private final StringProperty currencyCodeProperty = new SimpleStringProperty(); private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); private long epochInMillisAtLastRequest; - private Map timeStampMap = new HashMap<>(); private long retryDelay = 1; private long requestTs; @Nullable @@ -126,6 +125,10 @@ public class PriceFeedService { request(false); } + public boolean hasPrices() { + return !cache.isEmpty(); + } + public void requestPriceFeed(Consumer resultHandler, FaultHandler faultHandler) { this.priceConsumer = resultHandler; this.faultHandler = faultHandler; @@ -156,7 +159,7 @@ public class PriceFeedService { // At applyPriceToConsumer we also check if price is not exceeding max. age for price data. boolean success = applyPriceToConsumer(); if (success) { - final MarketPrice marketPrice = cache.get(currencyCode); + MarketPrice marketPrice = cache.get(currencyCode); if (marketPrice != null) log.debug("Received new {} from provider {} after {} sec.", marketPrice, @@ -326,7 +329,7 @@ public class PriceFeedService { boolean result = false; String errorMessage = null; if (currencyCode != null) { - final String baseUrl = priceProvider.getBaseUrl(); + String baseUrl = priceProvider.getBaseUrl(); if (cache.containsKey(currencyCode)) { try { MarketPrice marketPrice = cache.get(currencyCode); @@ -383,14 +386,12 @@ public class PriceFeedService { public void onSuccess(@Nullable Tuple2, Map> result) { UserThread.execute(() -> { checkNotNull(result, "Result must not be null at requestAllPrices"); - timeStampMap = result.first; - // Each currency rate has a different timestamp, depending on when // the pricenode aggregate rate was calculated // However, the request timestamp is when the pricenode was queried epochInMillisAtLastRequest = System.currentTimeMillis(); - final Map priceMap = result.second; + Map priceMap = result.second; cache.putAll(priceMap); diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java index d0ac27c2d7..bc29ecc382 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceProvider.java +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -70,7 +70,24 @@ public class PriceProvider extends HttpClientProvider { final double price = (Double) treeMap.get("price"); // json uses double for our timestampSec long value... final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); - marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + + // We do not have support for the case of multiChain assets where a common price ticker used for + // different flavours of the asset. It would be quite a bit of effort to add generic support to the + // asset and tradeCurrency classes and to handle it correctly from their many client classes. + // So we decided to hack in the sub-assets as copies of the price and accept the annoyance to see + // 3 different prices for the same master asset. But who knows, maybe prices will differ over time for + // the sub assets so then we are better prepared that way... + if (currencyCode.equals("USDT")) { + addPrice(marketPriceMap, "USDT-O", price, timestampSec); + addPrice(marketPriceMap, "USDT-E", price, timestampSec); + addPrice(marketPriceMap, "L-USDT", price, timestampSec); + } else { + // NON_EXISTING_SYMBOL is returned from service for nto found items + // Sometimes it has post fixes as well so we use a 'contains' check. + if (!currencyCode.contains("NON_EXISTING_SYMBOL")) { + addPrice(marketPriceMap, currencyCode, price, timestampSec); + } + } } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); @@ -80,6 +97,13 @@ public class PriceProvider extends HttpClientProvider { return new Tuple2<>(tsMap, marketPriceMap); } + private void addPrice(Map marketPriceMap, + String currencyCode, + double price, + long timestampSec) { + marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + } + public String getBaseUrl() { return httpClient.getBaseUrl(); } diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index 908a594aee..5d3f33200c 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -62,10 +62,14 @@ public abstract class SupportManager { // We get first the message handler called then the onBootstrapped p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); }); p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); }); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 4e13adb1fa..9998e1f2eb 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -19,9 +19,16 @@ package bisq.core.support.dispute; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportManager; import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.OpenNewDisputeMessage; @@ -37,13 +44,18 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.SendMailboxMessageListener; +import bisq.common.UserThread; import bisq.common.app.Version; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; import bisq.common.storage.Storage; +import bisq.common.util.MathUtils; import bisq.common.util.Tuple2; +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + import javafx.beans.property.IntegerProperty; import javafx.collections.ObservableList; @@ -51,6 +63,7 @@ import javafx.collections.ObservableList; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -68,6 +81,7 @@ public abstract class DisputeManager disputeListService; + private final PriceFeedService priceFeedService; /////////////////////////////////////////////////////////////////////////////////////////// @@ -82,7 +96,8 @@ public abstract class DisputeManager disputeListService) { + DisputeListService disputeListService, + PriceFeedService priceFeedService) { super(p2PService, walletsSetup); this.tradeWalletService = tradeWalletService; @@ -92,6 +107,7 @@ public abstract class DisputeManager storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent()) { disputeList.add(dispute); - errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing); + sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); } else { // valid case if both have opened a dispute and agent was not online. log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", @@ -286,23 +303,11 @@ public abstract class DisputeManager messages = dispute.getChatMessages(); if (!messages.isEmpty()) { ChatMessage chatMessage = messages.get(0); - PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); + PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); sendAckMessage(chatMessage, sendersPubKeyRing, errorMessage == null, errorMessage); } - // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. - if (dispute.getMediatorsDisputeResult() != null) { - String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); - ChatMessage mediatorsDisputeResultMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - pubKeyRing.hashCode(), - false, - mediatorsDisputeResult, - p2PService.getAddress()); - mediatorsDisputeResultMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); - } + addMediationResultMessage(dispute); } // not dispute requester receives that from dispute agent @@ -468,14 +473,27 @@ public abstract class DisputeManager doSendPeerOpenedDisputeMessage(disputeFromOpener, + contractFromOpener, + pubKeyRing), + 100, TimeUnit.MILLISECONDS); + } + + private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); - return null; + return; } Dispute dispute = new Dispute(disputeListService.getStorage(), @@ -500,91 +518,94 @@ public abstract class DisputeManager storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { - String disputeInfo = getDisputeInfo(dispute); - String disputeMessage = getDisputeIntroForPeer(disputeInfo); - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) - : disputeMessage; - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - pubKeyRing.hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress()); - chatMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(chatMessage); - disputeList.add(dispute); - // we mirrored dispute already! - Contract contract = dispute.getContract(); - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); - NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); - PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType()); - - log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - peersPubKeyRing, - peerOpenedDisputeMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - disputeList.persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - disputeList.persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid(), errorMessage); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - disputeList.persist(); - } - } - ); - return null; - } else { - // valid case if both have opened a dispute and agent was not online. - log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + // Valid case if both have opened a dispute and agent was not online. + if (storedDisputeOptional.isPresent()) { + log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); - return null; + return; } + + String disputeInfo = getDisputeInfo(dispute); + String disputeMessage = getDisputeIntroForPeer(disputeInfo); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) + : disputeMessage; + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + + addPriceInfoMessage(dispute, 0); + + disputeList.add(dispute); + + // We mirrored dispute already! + Contract contract = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); + PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + + log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + peerOpenedDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + disputeList.persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + disputeList.persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + disputeList.persist(); + } + } + ); } // dispute agent send result to trader @@ -731,4 +752,112 @@ public abstract class DisputeManager e.getTradeId().equals(tradeId)) .findAny(); } + + private void addMediationResultMessage(Dispute dispute) { + // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. + if (dispute.getMediatorsDisputeResult() != null) { + String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); + ChatMessage mediatorsDisputeResultMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + mediatorsDisputeResult, + p2PService.getAddress()); + mediatorsDisputeResultMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); + } + } + + // If price was going down between take offer time and open dispute time the buyer has an incentive to + // not send the payment but to try to make a new trade with the better price. We risks to lose part of the + // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated + // proposal). But if gain is larger than this loss he has economically an incentive to default in the trade. + // We do all those calculations to give a hint to mediators to detect option trades. + protected void addPriceInfoMessage(Dispute dispute, int counter) { + if (!priceFeedService.hasPrices()) { + if (counter < 3) { + log.info("Price provider has still no data. This is expected at startup. We try again in 10 sec."); + UserThread.runAfter(() -> addPriceInfoMessage(dispute, counter + 1), 10); + } else { + log.warn("Price provider still has no data after 3 repeated requests and 30 seconds delay. We give up."); + } + return; + } + + Contract contract = dispute.getContract(); + OfferPayload offerPayload = contract.getOfferPayload(); + Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); + if (priceAtDisputeOpening == null) { + log.info("Price provider did not provide a price for {}. " + + "This is expected if this currency is not supported by the price providers.", + offerPayload.getCurrencyCode()); + return; + } + + // The amount we would get if we do a new trade with current price + Coin potentialAmountAtDisputeOpening = priceAtDisputeOpening.getAmountByVolume(contract.getTradeVolume()); + Coin buyerSecurityDeposit = Coin.valueOf(offerPayload.getBuyerSecurityDeposit()); + Coin minRefundAtMediatedDispute = Restrictions.getMinRefundAtMediatedDispute(); + // minRefundAtMediatedDispute is always larger as buyerSecurityDeposit at mediated payout, we ignore refund agent case here as there it can be 0. + Coin maxLossSecDeposit = buyerSecurityDeposit.subtract(minRefundAtMediatedDispute); + Coin tradeAmount = contract.getTradeAmount(); + Coin potentialGain = potentialAmountAtDisputeOpening.subtract(tradeAmount).subtract(maxLossSecDeposit); + String optionTradeDetails; + // We don't translate those strings (yet) as it is only displayed to mediators/arbitrators. + String headline; + if (potentialGain.isPositive()) { + headline = "This might be a potential option trade!"; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible gain from an option trade is: " + potentialGain.toFriendlyString(); + } else { + headline = "It does not appear to be an option trade."; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible loss from an option trade is: " + potentialGain.multiply(-1).toFriendlyString(); + } + + String percentagePriceDetails = offerPayload.isUseMarketBasedPrice() ? + " (market based price was used: " + offerPayload.getMarketPriceMargin() * 100 + "%)" : + " (fix price was used)"; + + String priceInfoText = "System message: " + headline + + "\n\nTrade price: " + contract.getTradePrice().toFriendlyString() + percentagePriceDetails + + "\nTrade amount: " + tradeAmount.toFriendlyString() + + "\nPrice at dispute opening: " + priceAtDisputeOpening.toFriendlyString() + + optionTradeDetails; + + // We use the existing msg to copy over the users data + ChatMessage priceInfoMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + priceInfoText, + p2PService.getAddress()); + priceInfoMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(priceInfoMessage); + } + + @Nullable + private Price getPrice(String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double marketPriceAsDouble = marketPrice.getPrice(); + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e.toString()); + return null; + } + } else { + return null; + } + } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index e063e1c852..e77daf2b04 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -28,6 +28,7 @@ import bisq.core.btc.wallet.WalletService; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; @@ -88,9 +89,10 @@ public final class ArbitrationManager extends DisputeManager ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, PubKeyRing pubKeyRing, - MediationDisputeListService mediationDisputeListService) { + MediationDisputeListService mediationDisputeListService, + PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, mediationDisputeListService); + openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 139832a1c1..f28208f050 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -23,6 +23,7 @@ import bisq.core.btc.wallet.TradeWalletService; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; @@ -74,9 +75,10 @@ public final class RefundManager extends DisputeManager { ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, PubKeyRing pubKeyRing, - RefundDisputeListService refundDisputeListService) { + RefundDisputeListService refundDisputeListService, + PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, refundDisputeListService); + openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -140,6 +142,14 @@ public final class RefundManager extends DisputeManager { return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); } + @Override + protected void addPriceInfoMessage(Dispute dispute, int counter) { + // At refund agent we do not add the option trade price check as the time for dispute opening is not correct. + // In case of an option trade the mediator adds to the result summary message automatically the system message + // with the option trade detection info so the refund agent can see that as well. + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/trade/autoconf/xmr/XmrTxProofModel.java b/core/src/main/java/bisq/core/trade/autoconf/xmr/XmrTxProofModel.java index f8812c8f07..27a21b55b1 100644 --- a/core/src/main/java/bisq/core/trade/autoconf/xmr/XmrTxProofModel.java +++ b/core/src/main/java/bisq/core/trade/autoconf/xmr/XmrTxProofModel.java @@ -26,6 +26,8 @@ import bisq.common.app.DevEnv; import org.bitcoinj.core.Coin; +import com.google.common.annotations.VisibleForTesting; + import java.util.Date; import lombok.Value; @@ -51,7 +53,7 @@ public class XmrTxProofModel { private final int confirmsRequired; private final String serviceAddress; - public XmrTxProofModel(Trade trade, String serviceAddress, int confirmsRequired) { + XmrTxProofModel(Trade trade, String serviceAddress, int confirmsRequired) { this.serviceAddress = serviceAddress; this.confirmsRequired = confirmsRequired; Coin tradeAmount = trade.getTradeAmount(); @@ -68,4 +70,25 @@ public class XmrTxProofModel { tradeDate = trade.getDate(); tradeId = trade.getId(); } + + // Used only for testing + @VisibleForTesting + XmrTxProofModel(String tradeId, + String txHash, + String txKey, + String recipientAddress, + long amount, + Date tradeDate, + int confirmsRequired, + String serviceAddress) { + + this.tradeId = tradeId; + this.txHash = txHash; + this.txKey = txKey; + this.recipientAddress = recipientAddress; + this.amount = amount; + this.tradeDate = tradeDate; + this.confirmsRequired = confirmsRequired; + this.serviceAddress = serviceAddress; + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java index a892cb4c11..55e40c120f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java @@ -59,7 +59,7 @@ public class ApplyFilter extends TradeTask { } else if (filterManager.isPaymentMethodBanned(trade.getOffer().getPaymentMethod())) { failed("Payment method is banned.\n" + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); - } else if (filterManager.isPeersPaymentAccountDataAreBanned(paymentAccountPayload, appliedPaymentAccountFilter)) { + } else if (filterManager.arePeersPaymentAccountDataBanned(paymentAccountPayload, appliedPaymentAccountFilter)) { failed("Other trader is banned by their trading account data.\n" + "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails() + "\n" + "banFilter=" + appliedPaymentAccountFilter[0].toString()); diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 75891395d2..0f6957c83e 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -24,6 +24,7 @@ import bisq.core.locale.TradeCurrency; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.refund.refundagent.RefundAgent; @@ -50,6 +51,7 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import lombok.AllArgsConstructor; @@ -126,6 +128,16 @@ public class User implements PersistedDataHost { // API /////////////////////////////////////////////////////////////////////////////////////////// + public void onAllServicesInitialized(@Nullable Consumer> resultHandler) { + if (resultHandler != null) { + resultHandler.accept(paymentAccountsAsObservable.stream() + .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) + .map(paymentAccount -> (RevolutAccount) paymentAccount) + .filter(RevolutAccount::userNameNotSet) + .collect(Collectors.toList())); + } + } + @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 594b1b09ef..f0a4183907 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1065,7 +1065,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -1200,8 +1200,8 @@ setting.about.support=Support Bisq setting.about.def=Bisq is not a company—it is a project open to the community. If you want to participate or support Bisq please follow the links below. setting.about.contribute=Contribute setting.about.providers=Data providers -+setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. -+setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Market prices provided by setting.about.feeEstimation.label=Mining fee estimation provided by setting.about.versionDetails=Version details @@ -2398,18 +2398,32 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.reason=Reason of dispute -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=Usability -disputeSummaryWindow.reason.protocolViolation=Protocol violation -disputeSummaryWindow.reason.noReply=No reply -disputeSummaryWindow.reason.scam=Scam -disputeSummaryWindow.reason.other=Other -disputeSummaryWindow.reason.bank=Bank -disputeSummaryWindow.reason.optionTrade=Option trade -disputeSummaryWindow.reason.sellerNotResponding=Seller not responding -disputeSummaryWindow.reason.wrongSenderAccount=Wrong sender account -disputeSummaryWindow.reason.peerWasLate=Peer was late -disputeSummaryWindow.reason.tradeAlreadySettled=Trade already settled + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usability +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protocol violation +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=No reply +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Other +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Bank +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes @@ -2418,7 +2432,8 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\ Summary:\n\ Payout amount for BTC buyer: {1}\n\ Payout amount for BTC seller: {2}\n\n\ -Summary notes:\n{3} +Reason for dispute: {3}\n\n\ +Summary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\ Open trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\n\ @@ -2456,7 +2471,8 @@ filterWindow.onions=Filtered onion addresses (comma sep.) filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -3014,6 +3030,7 @@ seed.restore.error=An error occurred when restoring the wallets with seed words. payment.account=Account payment.account.no=Account no. payment.account.name=Account name +payment.account.userName=User name payment.account.owner=Account owner full name payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region @@ -3048,8 +3065,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Email or phone no. payment.venmo.venmoUserName=Venmo username payment.popmoney.accountId=Email or phone no. -payment.revolut.email=Email -payment.revolut.phoneNr=Registered phone no. payment.promptPay.promptPayId=Citizen ID/Tax ID or phone no. payment.supportedCurrencies=Supported currencies payment.limitations=Limitations @@ -3153,8 +3168,12 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \ For example, Bank of America and Wells Fargo no longer allow such deposits. -payment.revolut.info=Please be sure that the phone number you used for your Revolut account is registered at Revolut \ - otherwise the BTC buyer cannot send you the funds. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\n\ + Your existing Revolut account ({1}) does not has set the ''User name''.\n\ + Please enter your Revolut ''User name'' to update your account data.\n\ + This will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\ \n\ diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java index 1091548315..338a9b3a70 100644 --- a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java @@ -397,14 +397,14 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3); // Second account is banned, first account is still a signer but the other two are no longer signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First account is banned, no accounts in the tree below it are signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); @@ -434,14 +434,14 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3); // Only second account is banned, first account is still a signer but the other two are no longer signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Only first account is banned, account2 and account3 are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); @@ -484,21 +484,21 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3p); // First account is banned, the other two are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Second account is banned, the other two are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First and second account is banned, the third is no longer a signer - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 36d52df74d..b37a47d057 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -218,8 +218,8 @@ public class AccountAgeWitnessServiceTest { when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isCurrencyBanned(any())).thenReturn(false); when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); - when(filterManager.isPeersPaymentAccountDataAreBanned(any(), any())).thenReturn(false); - when(filterManager.isSignerPubKeyBanned(any())).thenReturn(false); + when(filterManager.arePeersPaymentAccountDataBanned(any(), any())).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false); when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); diff --git a/core/src/test/java/bisq/core/monetary/PriceTest.java b/core/src/test/java/bisq/core/monetary/PriceTest.java index 3f82492158..f72aca7ae8 100644 --- a/core/src/test/java/bisq/core/monetary/PriceTest.java +++ b/core/src/test/java/bisq/core/monetary/PriceTest.java @@ -27,14 +27,14 @@ public class PriceTest { Price result = Price.parse("USD", "0.1"); Assert.assertEquals( "Fiat value should be formatted with two decimals.", - "0.10 USD", + "0.10 BTC/USD", result.toFriendlyString() ); result = Price.parse("EUR", "0.1234"); Assert.assertEquals( "Fiat value should be given two decimals", - "0.1234 EUR", + "0.1234 BTC/EUR", result.toFriendlyString() ); @@ -57,19 +57,19 @@ public class PriceTest { Assert.assertEquals( "Comma (',') as decimal separator should be converted to period ('.')", - "0.0001 USD", + "0.0001 BTC/USD", Price.parse("USD", "0,0001").toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded up properly.", - "10000.2346 LTC", + "10000.2346 LTC/BTC", Price.parse("LTC", "10000,23456789").toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded down properly.", - "10000.2345 LTC", + "10000.2345 LTC/BTC", Price.parse("LTC", "10000,23454999").toFriendlyString() ); @@ -95,14 +95,14 @@ public class PriceTest { Price result = Price.valueOf("USD", 1); Assert.assertEquals( "Fiat value should have four decimals.", - "0.0001 USD", + "0.0001 BTC/USD", result.toFriendlyString() ); result = Price.valueOf("EUR", 1234); Assert.assertEquals( "Fiat value should be given two decimals", - "0.1234 EUR", + "0.1234 BTC/EUR", result.toFriendlyString() ); @@ -114,13 +114,13 @@ public class PriceTest { Assert.assertEquals( "Too many decimals should get rounded up properly.", - "10000.2346 LTC", + "10000.2346 LTC/BTC", Price.valueOf("LTC", 1000023456789L).toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded down properly.", - "10000.2345 LTC", + "10000.2345 LTC/BTC", Price.valueOf("LTC", 1000023454999L).toFriendlyString() ); diff --git a/core/src/test/java/bisq/core/trade/autoconf/xmr/XmrTxProofParserTest.java b/core/src/test/java/bisq/core/trade/autoconf/xmr/XmrTxProofParserTest.java index 1f80392245..cbd34f2be9 100644 --- a/core/src/test/java/bisq/core/trade/autoconf/xmr/XmrTxProofParserTest.java +++ b/core/src/test/java/bisq/core/trade/autoconf/xmr/XmrTxProofParserTest.java @@ -51,23 +51,23 @@ public class XmrTxProofParserTest { public void testJsonTopLevel() { // testing the top level fields: data and status assertTrue(XmrTxProofParser.parse(xmrTxProofModel, - "{'data':{'title':''},'status':'fail'}" ) + "{'data':{'title':''},'status':'fail'}") .getDetail() == XmrTxProofRequest.Detail.TX_NOT_FOUND); assertTrue(XmrTxProofParser.parse(xmrTxProofModel, - "{'data':{'title':''},'missingstatus':'success'}" ) + "{'data':{'title':''},'missingstatus':'success'}") .getDetail() == XmrTxProofRequest.Detail.API_INVALID); assertTrue(XmrTxProofParser.parse(xmrTxProofModel, - "{'missingdata':{'title':''},'status':'success'}" ) + "{'missingdata':{'title':''},'status':'success'}") .getDetail() == XmrTxProofRequest.Detail.API_INVALID); } @Test public void testJsonAddress() { assertTrue(XmrTxProofParser.parse(xmrTxProofModel, - "{'data':{'missingaddress':'irrelevant'},'status':'success'}" ) + "{'data':{'missingaddress':'irrelevant'},'status':'success'}") .getDetail() == XmrTxProofRequest.Detail.API_INVALID); assertTrue(XmrTxProofParser.parse(xmrTxProofModel, - "{'data':{'address':'e957dac7'},'status':'success'}" ) + "{'data':{'address':'e957dac7'},'status':'success'}") .getDetail() == XmrTxProofRequest.Detail.ADDRESS_INVALID); } diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index d64576bf5d..6fc717b263 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -53,14 +53,18 @@ public class UserPayloadModelVOTest { false, null, null, - "string", - new byte[]{10, 0, 0}, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + null, + 0, + null, + null, + null, null, - Lists.newArrayList(), - Lists.newArrayList(), - Lists.newArrayList(), - Lists.newArrayList(), false)); + vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index 455e6cda19..ce2f3f6f83 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -22,6 +22,7 @@ import bisq.core.dao.governance.param.Param; import bisq.core.filter.Filter; import bisq.core.filter.FilterManager; +import com.google.common.collect.Lists; import com.google.common.primitives.Longs; import java.util.HashMap; @@ -98,10 +99,29 @@ public class FeeReceiverSelectorTest { } private static Filter filterWithReceivers(List btcFeeReceiverAddresses) { - return new Filter(null, null, null, null, - null, null, null, null, - false, null, false, null, - null, null, null, null, - btcFeeReceiverAddresses, false); + return new Filter(Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + false, + Lists.newArrayList(), + false, + null, + null, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + btcFeeReceiverAddresses, + null, + 0, + null, + null, + null, + null, + false); } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java index a89803c835..9ddd087419 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -23,8 +23,6 @@ import bisq.desktop.util.Layout; import bisq.desktop.util.validation.RevolutValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.Country; -import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; @@ -34,46 +32,28 @@ import bisq.core.payment.payload.RevolutAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; -import com.jfoenix.controls.JFXComboBox; - -import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; - -import javafx.collections.FXCollections; - -import javafx.util.StringConverter; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; -import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; private RevolutValidator validator; - private InputTextField accountIdInputTextField; - private Country selectedCountry; + private InputTextField userNameInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - String accountId = ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); - addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, getTitle(accountId), accountId); + String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName); return gridRow; } - private static String getTitle(String accountId) { - // From 0.9.4 on we only allow phone nr. as with emails we got too many disputes as users used an email which was - // not registered at Revolut. It seems that phone numbers need to be registered at least we have no reports from - // arbitrators with such cases. Thought email is still supported for backward compatibility. - // We might still get emails from users who have registered when email was supported - return accountId.contains("@") ? Res.get("payment.revolut.email") : Res.get("payment.revolut.phoneNr"); - } - public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { @@ -86,63 +66,16 @@ public class RevolutForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - // country selection is added only to prevent anymore email id input and - // solely to validate the given phone number - ComboBox countryComboBox = addCountrySelection(); - setCountryComboBoxAction(countryComboBox); - countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllRevolutCountries())); - - accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.revolut.phoneNr")); - accountIdInputTextField.setValidator(validator); - accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - account.setAccountId(newValue.trim()); + userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + userNameInputTextField.setValidator(validator); + userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setUserName(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); - - //set default country as selected - selectedCountry = CountryUtil.getDefaultCountry(); - if (CountryUtil.getAllRevolutCountries().contains(selectedCountry)) { - countryComboBox.getSelectionModel().select(selectedCountry); - } - } - - ComboBox addCountrySelection() { - HBox hBox = new HBox(); - - hBox.setSpacing(5); - ComboBox countryComboBox = new JFXComboBox<>(); - hBox.getChildren().add(countryComboBox); - - addTopLabelWithVBox(gridPane, ++gridRow, Res.get("payment.bank.country"), hBox, 0); - - countryComboBox.setPromptText(Res.get("payment.select.bank.country")); - countryComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Country country) { - return country.name + " (" + country.code + ")"; - } - - @Override - public Country fromString(String s) { - return null; - } - }); - return countryComboBox; - } - - void setCountryComboBoxAction(ComboBox countryComboBox) { - countryComboBox.setOnAction(e -> { - selectedCountry = countryComboBox.getSelectionModel().getSelectedItem(); - updateFromInputs(); - accountIdInputTextField.resetValidation(); - accountIdInputTextField.validate(); - accountIdInputTextField.requestFocus(); - countryComboBox.requestFocus(); - }); } private void addCurrenciesGrid(boolean isEditable) { @@ -161,18 +94,18 @@ public class RevolutForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountIdInputTextField.getText()); + setAccountNameWithString(userNameInputTextField.getText()); } @Override public void addFormForDisplayAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.userName"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); - String accountId = account.getAccountId(); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, getTitle(accountId), accountId).second; + String userName = account.getUserName(); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); @@ -181,7 +114,7 @@ public class RevolutForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && validator.validate(account.getAccountId(), selectedCountry.code).isValid + && validator.validate(account.getUserName()).isValid && account.getTradeCurrencies().size() > 0); } } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 6bda1d4126..0f92d6954c 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -28,12 +28,13 @@ import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow; import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow; import bisq.desktop.main.overlays.windows.TacWindow; import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; +import bisq.desktop.main.overlays.windows.UpdateRevolutAccountWindow; import bisq.desktop.main.overlays.windows.WalletPasswordWindow; import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; import bisq.desktop.main.presentation.AccountPresentation; -import bisq.desktop.main.presentation.SettingsPresentation; import bisq.desktop.main.presentation.DaoPresentation; import bisq.desktop.main.presentation.MarketPricePresentation; +import bisq.desktop.main.presentation.SettingsPresentation; import bisq.desktop.main.shared.PriceFeedComboBoxItem; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; @@ -50,6 +51,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.AliPayAccount; import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.presentation.BalancePresentation; import bisq.core.presentation.SupportTicketsPresentation; import bisq.core.presentation.TradePresentation; @@ -88,12 +90,15 @@ import javafx.beans.property.StringProperty; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; +import java.util.List; import java.util.Optional; import java.util.PriorityQueue; import java.util.Queue; import java.util.Random; +import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -306,10 +311,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { .useReportBugButton() .show())); bisqSetup.setDisplayTorNetworkSettingsHandler(show -> { - if (show) + if (show) { torNetworkSettingsWindow.show(); - else + } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); + } }); bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) .actionButtonText(Res.get("settings.net.reSyncSPVChainButton")) @@ -380,6 +386,12 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { + // We copy the array as we will mutate it later + showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); + }); + + corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> new Popup() .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) .useShutDownButton() @@ -409,6 +421,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); } + private void showRevolutAccountUpdateWindow(List revolutAccountList) { + if (!revolutAccountList.isEmpty()) { + RevolutAccount revolutAccount = revolutAccountList.get(0); + revolutAccountList.remove(0); + new UpdateRevolutAccountWindow(revolutAccount, user).onClose(() -> { + // We delay a bit in case we have multiple account for better UX + UserThread.runAfter(() -> showRevolutAccountUpdateWindow(revolutAccountList), 300, TimeUnit.MILLISECONDS); + }).show(); + } + } + private void setupP2PNumPeersWatcher() { p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { int numPeers = (int) newValue; diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java index dc6fcf4de3..faa0245573 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -50,7 +50,6 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -127,14 +126,12 @@ class OfferBookChartViewModel extends ActivatableViewModel { fillTradeCurrencies(); }; - currenciesUpdatedListener = new ChangeListener<>() { - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if (!isAnyPricePresent()) { - offerBook.fillOfferBookListItems(); - updateChartData(); - priceFeedService.updateCounterProperty().removeListener(currenciesUpdatedListener); - } + currenciesUpdatedListener = (observable, oldValue, newValue) -> { + if (!isAnyPriceAbsent()) { + offerBook.fillOfferBookListItems(); + updateChartData(); + var self = this; + priceFeedService.updateCounterProperty().removeListener(self.currenciesUpdatedListener); } }; @@ -163,7 +160,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { fillTradeCurrencies(); updateChartData(); - if (isAnyPricePresent()) + if (isAnyPriceAbsent()) priceFeedService.updateCounterProperty().addListener(currenciesUpdatedListener); syncPriceFeedCurrency(); @@ -271,7 +268,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { priceFeedService.setCurrencyCode(getCurrencyCode()); } - private boolean isAnyPricePresent() { + private boolean isAnyPriceAbsent() { return offerBookListItems.stream().anyMatch(item -> item.getOffer().getPrice() == null); } @@ -291,11 +288,11 @@ class OfferBookChartViewModel extends ActivatableViewModel { Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); var buyOfferSortComparator = - offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. - .thenComparing(offerAmountComparator); + offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. + .thenComparing(offerAmountComparator); var sellOfferSortComparator = - offerPriceComparator - .thenComparing(offerAmountComparator); + offerPriceComparator + .thenComparing(offerAmountComparator); List allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 40ebfc78b7..5753c14c0b 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -71,7 +71,6 @@ import javax.inject.Named; import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; -import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; @@ -731,6 +730,12 @@ public class OfferBookView extends ActivatableViewAndModel asPriceDependentObservable(OfferBookListItem item) { + return item.getOffer().isUseMarketBasedPrice() + ? EasyBind.map(model.priceFeedService.updateCounterProperty(), n -> item) + : new ReadOnlyObjectWrapper<>(item); + } + private AutoTooltipTableColumn getPriceColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>("") { { @@ -738,59 +743,20 @@ public class OfferBookView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { - private OfferBookListItem offerBookListItem; - private ChangeListener priceChangedListener; - ChangeListener sceneChangeListener; - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - if (getTableView().getScene() != null && sceneChangeListener == null) { - sceneChangeListener = (observable, oldValue, newValue) -> { - if (newValue == null) { - if (priceChangedListener != null) { - model.priceFeedService.updateCounterProperty().removeListener(priceChangedListener); - priceChangedListener = null; - } - offerBookListItem = null; - setGraphic(null); - getTableView().sceneProperty().removeListener(sceneChangeListener); - sceneChangeListener = null; - } - }; - getTableView().sceneProperty().addListener(sceneChangeListener); - } - - this.offerBookListItem = item; - - if (priceChangedListener == null) { - priceChangedListener = (observable, oldValue, newValue) -> { - if (offerBookListItem != null && offerBookListItem.getOffer().getPrice() != null) { - setGraphic(getPriceLabel(model.getPrice(offerBookListItem), offerBookListItem)); - } - }; - model.priceFeedService.updateCounterProperty().addListener(priceChangedListener); - } - setGraphic(getPriceLabel(item.getOffer().getPrice() == null ? Res.get("shared.na") : model.getPrice(item), item)); + setGraphic(getPriceLabel(model.getPrice(item), item)); } else { - if (priceChangedListener != null) { - model.priceFeedService.updateCounterProperty().removeListener(priceChangedListener); - priceChangedListener = null; - } - if (sceneChangeListener != null) { - getTableView().sceneProperty().removeListener(sceneChangeListener); - sceneChangeListener = null; - } - this.offerBookListItem = null; setGraphic(null); } } @@ -845,35 +811,19 @@ public class OfferBookView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { - private OfferBookListItem offerBookListItem; - final ChangeListener listener = new ChangeListener<>() { - @Override - public void changed(ObservableValue observable, - Number oldValue, - Number newValue) { - if (offerBookListItem != null && offerBookListItem.getOffer().getVolume() != null) { - setText(""); - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(offerBookListItem), - model.getNumberOfDecimalsForVolume(offerBookListItem))); - model.priceFeedService.updateCounterProperty().removeListener(listener); - } - } - }; - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); + if (item != null && !empty) { if (item.getOffer().getPrice() == null) { - this.offerBookListItem = item; - model.priceFeedService.updateCounterProperty().addListener(listener); setText(Res.get("shared.na")); setGraphic(null); } else { @@ -882,8 +832,6 @@ public class OfferBookView extends ActivatableViewAndModel tableRow = getTableRow(); if (newItem != null && !empty) { final Offer offer = newItem.getOffer(); boolean myOffer = model.isMyOffer(offer); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index b55aa07e08..6202b6b204 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -486,18 +486,18 @@ public class DisputeSummaryWindow extends Overlay { } private void addReasonControls() { - reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bug")); - reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.usability")); - reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.protocolViolation")); - reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.noReply")); - reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.scam")); - reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bank")); - reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.other")); - reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.optionTrade")); - reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.sellerNotResponding")); - reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.wrongSenderAccount")); - reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.peerWasLate")); - reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.tradeAlreadySettled")); + reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BUG.name())); + reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.USABILITY.name())); + reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PROTOCOL_VIOLATION.name())); + reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.NO_REPLY.name())); + reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SCAM.name())); + reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BANK_PROBLEMS.name())); + reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OTHER.name())); + reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OPTION_TRADE.name())); + reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SELLER_NOT_RESPONDING.name())); + reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.WRONG_SENDER_ACCOUNT.name())); + reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PEER_WAS_LATE.name())); + reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.TRADE_ALREADY_SETTLED.name())); HBox feeRadioButtonPane = new HBox(); feeRadioButtonPane.setSpacing(20); @@ -745,12 +745,20 @@ public class DisputeSummaryWindow extends Overlay { disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); dispute.setIsClosed(true); + DisputeResult.Reason reason = disputeResult.getReason(); String text = Res.get("disputeSummaryWindow.close.msg", DisplayUtils.formatDateTime(disputeResult.getCloseDate()), formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + Res.get("disputeSummaryWindow.reason." + reason.name()), disputeResult.summaryNotesProperty().get()); + if (reason == DisputeResult.Reason.OPTION_TRADE && + dispute.getChatMessages().size() > 1 && + dispute.getChatMessages().get(1).isSystemMessage()) { + text += "\n\n" + dispute.getChatMessages().get(1).getMessage(); + } + if (dispute.getSupportType() == SupportType.MEDIATION) { text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); } else if (dispute.getSupportType() == SupportType.REFUND) { 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 d1e9872dac..bf98f05ea1 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 @@ -79,7 +79,7 @@ public class FilterWindow extends Overlay { if (headLine == null) headLine = Res.get("filterWindow.headline"); - width = 968; + width = 1000; createGridPane(); @@ -87,7 +87,7 @@ public class FilterWindow extends Overlay { scrollPane.setContent(gridPane); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); - scrollPane.setMaxHeight(1000); + scrollPane.setMaxHeight(700); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); addHeadLine(); @@ -112,93 +112,127 @@ public class FilterWindow extends Overlay { gridPane.getColumnConstraints().remove(1); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); - InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); - if (useDevPrivilegeKeys) - keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); + InputTextField keyTF = addInputTextField(gridPane, ++rowIndex, + Res.get("shared.unlock"), 10); + if (useDevPrivilegeKeys) { + keyTF.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); + } - InputTextField offerIdsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); - InputTextField nodesInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; - nodesInputTextField.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate - InputTextField paymentAccountFilterInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.accounts")).second; - GridPane.setHalignment(paymentAccountFilterInputTextField, HPos.RIGHT); - paymentAccountFilterInputTextField.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate - InputTextField bannedCurrenciesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedCurrencies")); - InputTextField bannedPaymentMethodsInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; - bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate - InputTextField bannedSignerPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedSignerPubKeys")).second; - bannedSignerPubKeysInputTextField.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate - InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); - InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); - InputTextField refundAgentsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); - InputTextField btcFeeReceiverAddressesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcFeeReceiverAddresses")); - InputTextField seedNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); - InputTextField priceRelayNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); - InputTextField btcNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcNode")); - CheckBox preventPublicBtcNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.preventPublicBtcNetwork")); - CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableDao")); - CheckBox disableAutoConfCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableAutoConf")); - InputTextField disableDaoBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableDaoBelowVersion")); - InputTextField disableTradeBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); + InputTextField offerIdsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.offers")); + InputTextField nodesTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.onions")).second; + nodesTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate + InputTextField paymentAccountFilterTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.accounts")).second; + GridPane.setHalignment(paymentAccountFilterTF, HPos.RIGHT); + paymentAccountFilterTF.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate + InputTextField bannedCurrenciesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedCurrencies")); + InputTextField bannedPaymentMethodsTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedPaymentMethods")).second; + bannedPaymentMethodsTF.setPromptText("E.g. PERFECT_MONEY"); // Do not translate + InputTextField bannedAccountWitnessSignerPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second; + bannedAccountWitnessSignerPubKeysTF.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate + InputTextField arbitratorsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.arbitrators")); + InputTextField mediatorsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.mediators")); + InputTextField refundAgentsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.refundAgents")); + InputTextField btcFeeReceiverAddressesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.btcFeeReceiverAddresses")); + InputTextField seedNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.seedNode")); + InputTextField priceRelayNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.priceRelayNode")); + InputTextField btcNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.btcNode")); + CheckBox preventPublicBtcNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.preventPublicBtcNetwork")); + CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.disableDao")); + CheckBox disableAutoConfCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.disableAutoConf")); + InputTextField disableDaoBelowVersionTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.disableDaoBelowVersion")); + InputTextField disableTradeBelowVersionTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.disableTradeBelowVersion")); + InputTextField bannedPrivilegedDevPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; - final Filter filter = filterManager.getDevelopersFilter(); + Filter filter = filterManager.getDevFilter(); if (filter != null) { - setupFieldFromList(offerIdsInputTextField, filter.getBannedOfferIds()); - setupFieldFromList(nodesInputTextField, filter.getBannedNodeAddress()); - setupFieldFromPaymentAccountFiltersList(paymentAccountFilterInputTextField, filter.getBannedPaymentAccounts()); - setupFieldFromList(bannedCurrenciesInputTextField, filter.getBannedCurrencies()); - setupFieldFromList(bannedPaymentMethodsInputTextField, filter.getBannedPaymentMethods()); - setupFieldFromList(bannedSignerPubKeysInputTextField, filter.getBannedSignerPubKeys()); - setupFieldFromList(arbitratorsInputTextField, filter.getArbitrators()); - setupFieldFromList(mediatorsInputTextField, filter.getMediators()); - setupFieldFromList(refundAgentsInputTextField, filter.getRefundAgents()); - setupFieldFromList(btcFeeReceiverAddressesInputTextField, filter.getBtcFeeReceiverAddresses()); - setupFieldFromList(seedNodesInputTextField, filter.getSeedNodes()); - setupFieldFromList(priceRelayNodesInputTextField, filter.getPriceRelayNodes()); - setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); + setupFieldFromList(offerIdsTF, filter.getBannedOfferIds()); + setupFieldFromList(nodesTF, filter.getBannedNodeAddress()); + setupFieldFromPaymentAccountFiltersList(paymentAccountFilterTF, filter.getBannedPaymentAccounts()); + setupFieldFromList(bannedCurrenciesTF, filter.getBannedCurrencies()); + setupFieldFromList(bannedPaymentMethodsTF, filter.getBannedPaymentMethods()); + setupFieldFromList(bannedAccountWitnessSignerPubKeysTF, filter.getBannedAccountWitnessSignerPubKeys()); + setupFieldFromList(arbitratorsTF, filter.getArbitrators()); + setupFieldFromList(mediatorsTF, filter.getMediators()); + setupFieldFromList(refundAgentsTF, filter.getRefundAgents()); + setupFieldFromList(btcFeeReceiverAddressesTF, filter.getBtcFeeReceiverAddresses()); + setupFieldFromList(seedNodesTF, filter.getSeedNodes()); + setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes()); + setupFieldFromList(btcNodesTF, filter.getBtcNodes()); + setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); disableDaoCheckBox.setSelected(filter.isDisableDao()); disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf()); - disableDaoBelowVersionInputTextField.setText(filter.getDisableDaoBelowVersion()); - disableTradeBelowVersionInputTextField.setText(filter.getDisableTradeBelowVersion()); + disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion()); + disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion()); } - Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); - sendButton.setOnAction(e -> { - if (filterManager.addFilterMessageIfKeyIsValid( - new Filter( - readAsList(offerIdsInputTextField), - readAsList(nodesInputTextField), - readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), - readAsList(bannedCurrenciesInputTextField), - readAsList(bannedPaymentMethodsInputTextField), - readAsList(arbitratorsInputTextField), - readAsList(seedNodesInputTextField), - readAsList(priceRelayNodesInputTextField), - preventPublicBtcNetworkCheckBox.isSelected(), - readAsList(btcNodesInputTextField), - disableDaoCheckBox.isSelected(), - disableDaoBelowVersionInputTextField.getText(), - disableTradeBelowVersionInputTextField.getText(), - readAsList(mediatorsInputTextField), - readAsList(refundAgentsInputTextField), - readAsList(bannedSignerPubKeysInputTextField), - readAsList(btcFeeReceiverAddressesInputTextField), - disableAutoConfCheckBox.isSelected() - ), - keyInputTextField.getText()) - ) - hide(); - else - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); - }); Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); + removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); + + Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); + sendButton.setOnAction(e -> { + String privKeyString = keyTF.getText(); + if (filterManager.canAddDevFilter(privKeyString)) { + String signerPubKeyAsHex = filterManager.getSignerPubKeyAsHex(privKeyString); + Filter newFilter = new Filter( + readAsList(offerIdsTF), + readAsList(nodesTF), + readAsPaymentAccountFiltersList(paymentAccountFilterTF), + readAsList(bannedCurrenciesTF), + readAsList(bannedPaymentMethodsTF), + readAsList(arbitratorsTF), + readAsList(seedNodesTF), + readAsList(priceRelayNodesTF), + preventPublicBtcNetworkCheckBox.isSelected(), + readAsList(btcNodesTF), + disableDaoCheckBox.isSelected(), + disableDaoBelowVersionTF.getText(), + disableTradeBelowVersionTF.getText(), + readAsList(mediatorsTF), + readAsList(refundAgentsTF), + readAsList(bannedAccountWitnessSignerPubKeysTF), + readAsList(btcFeeReceiverAddressesTF), + filterManager.getOwnerPubKey(), + signerPubKeyAsHex, + readAsList(bannedPrivilegedDevPubKeysTF), + disableAutoConfCheckBox.isSelected() + ); + + filterManager.addDevFilter(newFilter, privKeyString); + removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); + hide(); + } else { + new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); + } + }); + removeFilterMessageButton.setOnAction(e -> { - if (keyInputTextField.getText().length() > 0) { - if (filterManager.removeFilterMessageIfKeyIsValid(keyInputTextField.getText())) - hide(); - else - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + String privKeyString = keyTF.getText(); + if (filterManager.canRemoveDevFilter(privKeyString)) { + filterManager.removeDevFilter(privKeyString); + hide(); + } else { + new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); } }); @@ -218,13 +252,13 @@ public class FilterWindow extends Overlay { private void setupFieldFromList(InputTextField field, List values) { if (values != null) - field.setText(values.stream().collect(Collectors.joining(", "))); + field.setText(String.join(", ", values)); } private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List values) { if (values != null) { StringBuilder sb = new StringBuilder(); - values.stream().forEach(e -> { + values.forEach(e -> { if (e != null && e.getPaymentMethodId() != null) { sb .append(e.getPaymentMethodId()) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java new file mode 100644 index 0000000000..0c72c5a1a0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java @@ -0,0 +1,96 @@ +/* + * 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.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.RevolutValidator; + +import bisq.core.locale.Res; +import bisq.core.payment.RevolutAccount; +import bisq.core.user.User; + +import javafx.scene.Scene; + +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addLabel; + +public class UpdateRevolutAccountWindow extends Overlay { + private final RevolutValidator revolutValidator; + private final RevolutAccount revolutAccount; + private final User user; + private InputTextField userNameInputTextField; + + public UpdateRevolutAccountWindow(RevolutAccount revolutAccount, User user) { + super(); + this.revolutAccount = revolutAccount; + this.user = user; + type = Type.Attention; + hideCloseButton = true; + revolutValidator = new RevolutValidator(); + actionButtonText = Res.get("shared.save"); + } + + @Override + protected void setupKeyHandler(Scene scene) { + // We do not support enter or escape here + } + + @Override + public void show() { + if (headLine == null) + headLine = Res.get("payment.revolut.addUserNameInfo.headLine"); + + width = 868; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + private void addContent() { + addLabel(gridPane, ++rowIndex, Res.get("payment.account.revolut.addUserNameInfo", Res.get("payment.revolut.info"), revolutAccount.getAccountName())); + userNameInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("payment.account.userName"), Layout.COMPACT_FIRST_ROW_DISTANCE); + userNameInputTextField.setValidator(revolutValidator); + userNameInputTextField.textProperty().addListener((observable, oldValue, newValue) -> + actionButton.setDisable(!revolutValidator.validate(newValue).isValid)); + } + + @Override + protected void addButtons() { + super.addButtons(); + + // We do not allow close in case the userName is not correctly added so we + // overwrote the default handler + actionButton.setOnAction(event -> { + String userName = userNameInputTextField.getText(); + if (revolutValidator.validate(userName).isValid) { + revolutAccount.setUserName(userName); + user.persist(); + closeHandlerOptional.ifPresent(Runnable::run); + hide(); + } + }); + actionButton.setDisable(true); + } + +} + diff --git a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java index 44eea0023d..0cf464b30a 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java @@ -17,10 +17,17 @@ package bisq.desktop.util.validation; -public final class RevolutValidator extends PhoneNumberValidator { +public final class RevolutValidator extends LengthValidator { + public RevolutValidator() { + // Not sure what are requirements for Revolut user names + // Please keep in mind that even we force users to set user name at startup we should handle also the case + // that the old accountID as phone number or email is displayed at the username text field and we do not + // want to break validation in those cases. So being too strict on the validators might cause more troubles + // as its worth... + super(5, 100); + } - public ValidationResult validate(String input, String code) { - super.setIsoCountryCode(code); + public ValidationResult validate(String input) { return super.validate(input); } diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 08f74f9f51..856355e473 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -42,7 +42,7 @@ dependencyVerification { 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', - 'com.jfoenix:jfoenix:4739e37a05e67c3bc9d5b391a1b93717b5a48fa872992616b0964d3f827f8fe6', + 'com.jfoenix:jfoenix:8060235fec5eb49617ec8d81d379e8c945f6cc722d0645e97190045100de2084', 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf', diff --git a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java index d4ff8f46a8..61756c732d 100644 --- a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java +++ b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java @@ -17,6 +17,7 @@ package bisq.network.p2p; +import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.common.app.Capabilities; @@ -36,7 +37,7 @@ import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value -public final class BundleOfEnvelopes extends NetworkEnvelope implements ExtendedDataSizePermission, CapabilityRequiringPayload { +public final class BundleOfEnvelopes extends BroadcastMessage implements ExtendedDataSizePermission, CapabilityRequiringPayload { private final List envelopes; @@ -44,6 +45,10 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended this(new ArrayList<>(), Version.getP2PMessageVersion()); } + public BundleOfEnvelopes(List envelopes) { + this(envelopes, Version.getP2PMessageVersion()); + } + public void add(NetworkEnvelope networkEnvelope) { envelopes.add(networkEnvelope); } @@ -67,7 +72,9 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended .build(); } - public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, NetworkProtoResolver resolver, int messageVersion) { + public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, + NetworkProtoResolver resolver, + int messageVersion) { List envelopes = proto.getEnvelopesList() .stream() .map(envelope -> { diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 252dcd7d69..3b0c188e27 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -37,7 +37,6 @@ import bisq.network.p2p.seed.SeedNodeRepository; import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.messages.AddDataMessage; -import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.network.p2p.storage.messages.RefreshOfferMessage; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.network.p2p.storage.payload.MailboxStoragePayload; @@ -53,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; @@ -77,6 +75,7 @@ import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -122,9 +121,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); - private volatile boolean shutDownInProgress; - @Getter - private boolean shutDownComplete; private final Subscription networkReadySubscription; private boolean isBootstrapped; private final KeepAliveManager keepAliveManager; @@ -212,48 +208,48 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } public void shutDown(Runnable shutDownCompleteHandler) { - if (!shutDownInProgress) { - shutDownInProgress = true; + shutDownResultHandlers.add(shutDownCompleteHandler); - shutDownResultHandlers.add(shutDownCompleteHandler); - - if (p2PDataStorage != null) - p2PDataStorage.shutDown(); - - if (peerManager != null) - peerManager.shutDown(); - - if (broadcaster != null) - broadcaster.shutDown(); - - if (requestDataManager != null) - requestDataManager.shutDown(); - - if (peerExchangeManager != null) - peerExchangeManager.shutDown(); - - if (keepAliveManager != null) - keepAliveManager.shutDown(); - - if (networkReadySubscription != null) - networkReadySubscription.unsubscribe(); - - if (networkNode != null) { - networkNode.shutDown(() -> { - shutDownResultHandlers.stream().forEach(Runnable::run); - shutDownComplete = true; - }); - } else { - shutDownResultHandlers.stream().forEach(Runnable::run); - shutDownComplete = true; - } + // We need to make sure queued up messages are flushed out before we continue shut down other network + // services + if (broadcaster != null) { + broadcaster.shutDown(this::doShutDown); } else { - log.debug("shutDown already in progress"); - if (shutDownComplete) { - shutDownCompleteHandler.run(); - } else { - shutDownResultHandlers.add(shutDownCompleteHandler); - } + doShutDown(); + } + } + + private void doShutDown() { + if (p2PDataStorage != null) { + p2PDataStorage.shutDown(); + } + + if (peerManager != null) { + peerManager.shutDown(); + } + + if (requestDataManager != null) { + requestDataManager.shutDown(); + } + + if (peerExchangeManager != null) { + peerExchangeManager.shutDown(); + } + + if (keepAliveManager != null) { + keepAliveManager.shutDown(); + } + + if (networkReadySubscription != null) { + networkReadySubscription.unsubscribe(); + } + + if (networkNode != null) { + networkNode.shutDown(() -> { + shutDownResultHandlers.forEach(Runnable::run); + }); + } else { + shutDownResultHandlers.forEach(Runnable::run); } } @@ -448,7 +444,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onRemoved(Collection protectedStorageEntries) { - // not handled + // not used } /////////////////////////////////////////////////////////////////////////////////////////// @@ -672,43 +668,21 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override - public void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts) { + public void onSufficientlyBroadcast(List broadcastRequests) { + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getMessage() instanceof AddDataMessage) + .filter(broadcastRequest -> { + AddDataMessage addDataMessage = (AddDataMessage) broadcastRequest.getMessage(); + return addDataMessage.getProtectedStorageEntry().equals(protectedMailboxStorageEntry); + }) + .forEach(e -> sendMailboxMessageListener.onStoredInMailbox()); } @Override - public void onBroadcastedToFirstPeer(BroadcastMessage message) { - // The reason for that check was to separate different callback for different send calls. - // We only want to notify our sendMailboxMessageListener for the calls he is interested in. - if (message instanceof AddDataMessage && - ((AddDataMessage) message).getProtectedStorageEntry().equals(protectedMailboxStorageEntry)) { - // We delay a bit to give more time for sufficient propagation in the P2P network. - // This should help to avoid situations where a user closes the app too early and the msg - // does not arrive. - // We could use onBroadcastCompleted instead but it might take too long if one peer - // is very badly connected. - // TODO We could check for a certain threshold of no. of incoming network_messages of the same msg - // to see how well it is propagated. BitcoinJ uses such an approach for tx propagation. - UserThread.runAfter(() -> { - log.info("Broadcasted to first peer (3 sec. ago): Message = {}", Utilities.toTruncatedString(message)); - sendMailboxMessageListener.onStoredInMailbox(); - }, 3); - } - } - - @Override - public void onBroadcastCompleted(BroadcastMessage message, - int numOfCompletedBroadcasts, - int numOfFailedBroadcasts) { - log.info("Broadcast completed: Sent to {} peers (failed: {}). Message = {}", - numOfCompletedBroadcasts, numOfFailedBroadcasts, Utilities.toTruncatedString(message)); - if (numOfCompletedBroadcasts == 0) - sendMailboxMessageListener.onFault("Broadcast completed without any successful broadcast"); - } - - @Override - public void onBroadcastFailed(String errorMessage) { - // TODO investigate why not sending sendMailboxMessageListener.onFault. Related probably - // to the logic from BroadcastHandler.sendToPeer + public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) { + sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" + + "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" + + "numOfFailedBroadcast=" + numOfFailedBroadcast); } }; boolean result = p2PDataStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener); @@ -721,7 +695,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis log.error("Unexpected state: adding mailbox message that already exists."); } } catch (CryptoException e) { - log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); + log.error("Signing at getMailboxDataWithSignedSeqNr failed."); } } else { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 3f315b77cb..050978d969 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -225,7 +225,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { // Called from various threads public void sendMessage(NetworkEnvelope networkEnvelope) { - log.debug(">> Send networkEnvelope of type: " + networkEnvelope.getClass().getSimpleName()); + log.debug(">> Send networkEnvelope of type: {}", networkEnvelope.getClass().getSimpleName()); if (!stopped) { if (noCapabilityRequiredOrCapabilityIsSupported(networkEnvelope)) { @@ -319,6 +319,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { } } + // TODO: If msg is BundleOfEnvelopes we should check each individual message for capability and filter out those + // which fail. public boolean noCapabilityRequiredOrCapabilityIsSupported(Proto msg) { boolean result; if (msg instanceof AddDataMessage) { @@ -408,12 +410,13 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { checkArgument(connection.equals(this)); - if (networkEnvelope instanceof BundleOfEnvelopes) + if (networkEnvelope instanceof BundleOfEnvelopes) { for (NetworkEnvelope current : ((BundleOfEnvelopes) networkEnvelope).getEnvelopes()) { UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(current, connection))); } - else + } else { UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection))); + } } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index cc8469d8bd..d370be6bd3 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -17,6 +17,7 @@ package bisq.network.p2p.peers; +import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.NetworkNode; @@ -24,7 +25,6 @@ import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.common.Timer; import bisq.common.UserThread; -import bisq.common.util.Utilities; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -33,7 +33,6 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -41,11 +40,10 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; @Slf4j public class BroadcastHandler implements PeerManager.Listener { - private static final long TIMEOUT = 60; + private static final long BASE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60); /////////////////////////////////////////////////////////////////////////////////////////// @@ -54,20 +52,12 @@ public class BroadcastHandler implements PeerManager.Listener { interface ResultHandler { void onCompleted(BroadcastHandler broadcastHandler); - - void onFault(BroadcastHandler broadcastHandler); } public interface Listener { - @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) - void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts); + void onSufficientlyBroadcast(List broadcastRequests); - void onBroadcastedToFirstPeer(BroadcastMessage message); - - void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts); - - @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) - void onBroadcastFailed(String errorMessage); + void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast); } @@ -76,16 +66,12 @@ public class BroadcastHandler implements PeerManager.Listener { /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; - public final String uid; private final PeerManager peerManager; - private boolean stopped = false; - private int numOfCompletedBroadcasts = 0; - private int numOfFailedBroadcasts = 0; - private BroadcastMessage message; - private ResultHandler resultHandler; - @Nullable - private Listener listener; - private int numPeers; + private final ResultHandler resultHandler; + private final String uid; + + private boolean stopped, timeoutTriggered; + private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeersForBroadcast; private Timer timeoutTimer; @@ -93,16 +79,13 @@ public class BroadcastHandler implements PeerManager.Listener { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) { + BroadcastHandler(NetworkNode networkNode, PeerManager peerManager, ResultHandler resultHandler) { this.networkNode = networkNode; this.peerManager = peerManager; - peerManager.addListener(this); + this.resultHandler = resultHandler; uid = UUID.randomUUID().toString(); - } - public void cancel() { - stopped = true; - onFault("Broadcast canceled.", false); + peerManager.addListener(this); } @@ -110,110 +93,73 @@ public class BroadcastHandler implements PeerManager.Listener { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, ResultHandler resultHandler, - @Nullable Listener listener) { - this.message = message; - this.resultHandler = resultHandler; - this.listener = listener; + public void broadcast(List broadcastRequests, boolean shutDownRequested) { + List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); + Collections.shuffle(confirmedConnections); - Set connectedPeersSet = networkNode.getConfirmedConnections() - .stream() - .filter(connection -> !connection.getPeersNodeAddressOptional().get().equals(sender)) - .collect(Collectors.toSet()); - if (!connectedPeersSet.isEmpty()) { - numOfCompletedBroadcasts = 0; - - List connectedPeersList = new ArrayList<>(connectedPeersSet); - Collections.shuffle(connectedPeersList); - numPeers = connectedPeersList.size(); - int delay = 50; - - boolean isDataOwner = (sender != null) && sender.equals(networkNode.getNodeAddress()); - if (!isDataOwner) { - // for not data owner (relay nodes) we send to max. 7 nodes and use a longer delay - numPeers = Math.min(7, connectedPeersList.size()); + int delay; + if (shutDownRequested) { + delay = 1; + // We sent to all peers as in case we had offers we want that it gets removed with higher reliability + numPeersForBroadcast = confirmedConnections.size(); + } else { + if (requestsContainOwnMessage(broadcastRequests)) { + // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and + // with shorter delay + numPeersForBroadcast = confirmedConnections.size(); + delay = 50; + } else { + // Relay nodes only send to max 7 peers and with longer delay + numPeersForBroadcast = Math.min(7, confirmedConnections.size()); delay = 100; } + } - long timeoutDelay = TIMEOUT + delay * numPeers; - timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions - String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec."; + setupTimeoutHandler(broadcastRequests, delay, shutDownRequested); - log.debug(errorMessage + "\n\t" + - "numOfPeers=" + numPeers + "\n\t" + - "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" + - "numOfFailedBroadcasts=" + numOfFailedBroadcasts); - onFault(errorMessage, false); - }, timeoutDelay); + int iterations = numPeersForBroadcast; + for (int i = 0; i < iterations; i++) { + long minDelay = (i + 1) * delay; + long maxDelay = (i + 2) * delay; + Connection connection = confirmedConnections.get(i); + UserThread.runAfterRandomDelay(() -> { + if (stopped) { + return; + } - log.debug("Broadcast message to {} peers out of {} total connected peers.", numPeers, connectedPeersSet.size()); - for (int i = 0; i < numPeers; i++) { - if (stopped) - break; // do not continue sending after a timeout or a cancellation + // We use broadcastRequests which have excluded the requests for messages the connection has + // originated to avoid sending back the message we received. We also remove messages not satisfying + // capability checks. + List broadcastRequestsForConnection = getBroadcastRequestsForConnection(connection, broadcastRequests); - final long minDelay = (i + 1) * delay; - final long maxDelay = (i + 2) * delay; - final Connection connection = connectedPeersList.get(i); - UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS); - } - } else { - onFault("Message not broadcasted because we have no available peers yet.\n\t" + - "message = " + Utilities.toTruncatedString(message), false); + // Could be empty list... + if (broadcastRequestsForConnection.isEmpty()) { + // We decrease numPeers in that case for making completion checks correct. + if (numPeersForBroadcast > 0) { + numPeersForBroadcast--; + } + checkForCompletion(); + return; + } + + if (connection.isStopped()) { + // Connection has died in the meantime. We skip it. + // We decrease numPeers in that case for making completion checks correct. + if (numPeersForBroadcast > 0) { + numPeersForBroadcast--; + } + checkForCompletion(); + return; + } + + sendToPeer(connection, broadcastRequestsForConnection); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } - private void sendToPeer(Connection connection, BroadcastMessage message) { - String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" + - "message = " + Utilities.toTruncatedString(message); - if (!stopped) { - if (!connection.isStopped()) { - if (connection.noCapabilityRequiredOrCapabilityIsSupported(message)) { - NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); - SettableFuture future = networkNode.sendMessage(connection, message); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(Connection connection) { - numOfCompletedBroadcasts++; - if (!stopped) { - if (listener != null) - listener.onBroadcasted(message, numOfCompletedBroadcasts); - - if (listener != null && numOfCompletedBroadcasts == 1) - listener.onBroadcastedToFirstPeer(message); - - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { - if (listener != null) - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); - - cleanup(); - resultHandler.onCompleted(BroadcastHandler.this); - } - } else { - // TODO investigate why that is called very often at seed nodes - onFault("stopped at onSuccess: " + errorMessage, false); - } - } - - @Override - public void onFailure(@NotNull Throwable throwable) { - numOfFailedBroadcasts++; - if (!stopped) { - log.info("Broadcast to " + nodeAddress + " failed.\n\t" + - "ErrorMessage=" + throwable.getMessage()); - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) - onFault("stopped at onFailure: " + errorMessage); - } else { - onFault("stopped at onFailure: " + errorMessage); - } - } - }); - } - } else { - onFault("Connection stopped already", false); - } - } else { - onFault("stopped at sendToPeer: " + errorMessage, false); - } + public void cancel() { + stopped = true; + cleanup(); } @@ -223,7 +169,7 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public void onAllConnectionsLost() { - onFault("All connections lost", false); + cleanup(); } @Override @@ -239,37 +185,142 @@ public class BroadcastHandler implements PeerManager.Listener { // Private /////////////////////////////////////////////////////////////////////////////////////////// + // Check if we have at least one message originated by ourselves + private boolean requestsContainOwnMessage(List broadcastRequests) { + NodeAddress myAddress = networkNode.getNodeAddress(); + if (myAddress == null) + return false; + + return broadcastRequests.stream().anyMatch(e -> myAddress.equals(e.getSender())); + } + + private void setupTimeoutHandler(List broadcastRequests, + int delay, + boolean shutDownRequested) { + // In case of shutdown we try to complete fast and set a short 1 second timeout + long baseTimeoutMs = shutDownRequested ? TimeUnit.SECONDS.toMillis(1) : BASE_TIMEOUT_MS; + long timeoutDelay = baseTimeoutMs + delay * (numPeersForBroadcast + 1); // We added 1 in the loop + timeoutTimer = UserThread.runAfter(() -> { + if (stopped) { + return; + } + + timeoutTriggered = true; + + log.warn("Broadcast did not complete after {} sec.\n" + + "numPeersForBroadcast={}\n" + + "numOfCompletedBroadcasts={}\n" + + "numOfFailedBroadcasts={}", + timeoutDelay / 1000d, + numPeersForBroadcast, + numOfCompletedBroadcasts, + numOfFailedBroadcasts); + + maybeNotifyListeners(broadcastRequests); + + cleanup(); + + }, timeoutDelay, TimeUnit.MILLISECONDS); + } + + // We exclude the requests containing a message we received from that connection + // Also we filter out messages which requires a capability but peer does not support it. + private List getBroadcastRequestsForConnection(Connection connection, + List broadcastRequests) { + return broadcastRequests.stream() + .filter(broadcastRequest -> !connection.getPeersNodeAddressOptional().isPresent() || + !connection.getPeersNodeAddressOptional().get().equals(broadcastRequest.getSender())) + .filter(broadcastRequest -> connection.noCapabilityRequiredOrCapabilityIsSupported(broadcastRequest.getMessage())) + .collect(Collectors.toList()); + } + + private void sendToPeer(Connection connection, List broadcastRequestsForConnection) { + // Can be BundleOfEnvelopes or a single BroadcastMessage + BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection); + SettableFuture future = networkNode.sendMessage(connection, broadcastMessage); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + numOfCompletedBroadcasts++; + + if (stopped) { + return; + } + + maybeNotifyListeners(broadcastRequestsForConnection); + checkForCompletion(); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.warn("Broadcast to {} failed. ErrorMessage={}", connection.getPeersNodeAddressOptional(), + throwable.getMessage()); + numOfFailedBroadcasts++; + + if (stopped) { + return; + } + + maybeNotifyListeners(broadcastRequestsForConnection); + checkForCompletion(); + } + }); + } + + private BroadcastMessage getMessage(List broadcastRequests) { + if (broadcastRequests.size() == 1) { + // If we only have 1 message we avoid the overhead of the BundleOfEnvelopes and send the message directly + return broadcastRequests.get(0).getMessage(); + } else { + return new BundleOfEnvelopes(broadcastRequests.stream() + .map(Broadcaster.BroadcastRequest::getMessage) + .collect(Collectors.toList())); + } + } + + private void maybeNotifyListeners(List broadcastRequests) { + int numOfCompletedBroadcastsTarget = Math.max(1, Math.min(numPeersForBroadcast, 3)); + // We use equal checks to avoid duplicated listener calls as it would be the case with >= checks. + if (numOfCompletedBroadcasts == numOfCompletedBroadcastsTarget) { + // We have heard back from 3 peers (or all peers if numPeers is lower) so we consider the message was sufficiently broadcast. + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getListener() != null) + .map(Broadcaster.BroadcastRequest::getListener) + .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); + } else { + // We check if number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. + // Thus we never can reach required resilience as too many numOfFailedBroadcasts occurred. + int maxPossibleSuccessCases = numPeersForBroadcast - numOfFailedBroadcasts; + // We subtract 1 as we want to have it called only once, with a < comparision we would trigger repeatedly. + boolean notEnoughSucceededOrOpen = maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1; + // We did not reach resilience level and timeout prevents to reach it later + boolean timeoutAndNotEnoughSucceeded = timeoutTriggered && numOfCompletedBroadcasts < numOfCompletedBroadcastsTarget; + if (notEnoughSucceededOrOpen || timeoutAndNotEnoughSucceeded) { + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getListener() != null) + .map(Broadcaster.BroadcastRequest::getListener) + .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); + } + } + } + + private void checkForCompletion() { + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeersForBroadcast) { + cleanup(); + } + } + private void cleanup() { stopped = true; - peerManager.removeListener(this); if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } + peerManager.removeListener(this); + resultHandler.onCompleted(this); } - private void onFault(String errorMessage) { - onFault(errorMessage, true); - } - - private void onFault(String errorMessage, boolean logWarning) { - cleanup(); - - if (logWarning) - log.warn(errorMessage); - else - log.debug(errorMessage); - - if (listener != null) - listener.onBroadcastFailed(errorMessage); - - if (listener != null && (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers || stopped)) - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); - - resultHandler.onFault(this); - } - - @Override public boolean equals(Object o) { if (this == o) return true; @@ -277,11 +328,11 @@ public class BroadcastHandler implements PeerManager.Listener { BroadcastHandler that = (BroadcastHandler) o; - return !(uid != null ? !uid.equals(that.uid) : that.uid != null); + return uid.equals(that.uid); } @Override public int hashCode() { - return uid != null ? uid.hashCode() : 0; + return uid.hashCode(); } } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 727c488b0e..cdfdde5c46 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -21,18 +21,34 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.common.Timer; +import bisq.common.UserThread; + import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; +@Slf4j public class Broadcaster implements BroadcastHandler.ResultHandler { + private static final long BROADCAST_INTERVAL_MS = 2000; + private final NetworkNode networkNode; private final PeerManager peerManager; - private final Set broadcastHandlers = new CopyOnWriteArraySet<>(); + private final List broadcastRequests = new ArrayList<>(); + private Timer timer; + private boolean shutDownRequested; + private Runnable shutDownResultHandler; /////////////////////////////////////////////////////////////////////////////////////////// @@ -45,9 +61,24 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { this.peerManager = peerManager; } - public void shutDown() { - broadcastHandlers.stream().forEach(BroadcastHandler::cancel); - broadcastHandlers.clear(); + public void shutDown(Runnable resultHandler) { + shutDownRequested = true; + shutDownResultHandler = resultHandler; + if (broadcastRequests.isEmpty()) { + doShutDown(); + } else { + // We set delay of broadcasts and timeout to very low values, + // so we can expect that we get onCompleted called very fast and trigger the doShutDown from there. + maybeBroadcastBundle(); + } + } + + private void doShutDown() { + broadcastHandlers.forEach(BroadcastHandler::cancel); + if (timer != null) { + timer.stop(); + } + shutDownResultHandler.run(); } @@ -55,11 +86,38 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, + public void broadcast(BroadcastMessage message, + @Nullable NodeAddress sender) { + broadcast(message, sender, null); + } + + + public void broadcast(BroadcastMessage message, + @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { - BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager); - broadcastHandler.broadcast(message, sender, this, listener); - broadcastHandlers.add(broadcastHandler); + broadcastRequests.add(new BroadcastRequest(message, sender, listener)); + // Keep that log on INFO for better debugging if the feature works as expected. Later it can + // be remove or set to DEBUG + log.debug("Broadcast requested for {}. We queue it up for next bundled broadcast.", + message.getClass().getSimpleName()); + + if (timer == null) { + timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + } + + private void maybeBroadcastBundle() { + if (!broadcastRequests.isEmpty()) { + log.debug("Broadcast bundled requests of {} messages. Message types: {}", + broadcastRequests.size(), + broadcastRequests.stream().map(e -> e.getMessage().getClass().getSimpleName()).collect(Collectors.toList())); + BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); + broadcastHandlers.add(broadcastHandler); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested); + broadcastRequests.clear(); + + timer = null; + } } @@ -70,10 +128,30 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { @Override public void onCompleted(BroadcastHandler broadcastHandler) { broadcastHandlers.remove(broadcastHandler); + if (shutDownRequested) { + doShutDown(); + } } - @Override - public void onFault(BroadcastHandler broadcastHandler) { - broadcastHandlers.remove(broadcastHandler); + + /////////////////////////////////////////////////////////////////////////////////////////// + // BroadcastRequest class + /////////////////////////////////////////////////////////////////////////////////////////// + + @Value + public static class BroadcastRequest { + private BroadcastMessage message; + @Nullable + private NodeAddress sender; + @Nullable + private BroadcastHandler.Listener listener; + + private BroadcastRequest(BroadcastMessage message, + @Nullable NodeAddress sender, + @Nullable BroadcastHandler.Listener listener) { + this.message = message; + this.sender = sender; + this.listener = listener; + } } } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java index 6115df52e2..6ace6d87e9 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java @@ -49,8 +49,8 @@ import org.jetbrains.annotations.NotNull; public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class); - private static final int INTERVAL_SEC = new Random().nextInt(5) + 30; - private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC / 2; + private static final int INTERVAL_SEC = new Random().nextInt(30) + 30; + private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC * 1000 / 2; private final NetworkNode networkNode; private final PeerManager peerManager; 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 34e29f3f5f..2283547a1f 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -529,7 +529,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers // Broadcast the payload if requested by caller if (allowBroadcast) - broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender, null); + broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender); return true; } @@ -675,7 +675,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000); // Always broadcast refreshes - broadcaster.broadcast(refreshTTLMessage, sender, null); + broadcaster.broadcast(refreshTTLMessage, sender); return true; } @@ -725,9 +725,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers printData("after remove"); if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { - broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null); + broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender); } else { - broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender, null); + broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender); } return true; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java index b5fe473732..29d6ada7dd 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java @@ -33,8 +33,6 @@ import org.junit.Before; import org.junit.Test; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -69,7 +67,7 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test @@ -82,7 +80,7 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test @@ -96,6 +94,6 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } } 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 0ede4a03c8..e71246a031 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java @@ -19,7 +19,6 @@ package bisq.network.p2p.storage; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.BroadcastHandler; import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.storage.messages.AddDataMessage; import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; @@ -51,10 +50,10 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.junit.Assert; - import org.mockito.ArgumentCaptor; +import org.junit.Assert; + import static org.mockito.Mockito.*; /** @@ -160,8 +159,7 @@ public class TestState { /** * Common test helpers that verify the correct events were signaled based on the test expectation and before/after states. */ - private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, - int sequenceNumber) { + private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, int sequenceNumber) { final ArgumentCaptor captor = ArgumentCaptor.forClass(SequenceNumberMap.class); verify(this.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong()); @@ -187,10 +185,9 @@ public class TestState { verify(this.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); if (expectedBroadcast) - verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), - nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), nullable(NodeAddress.class)); else - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } void verifyProtectedStorageAdd(SavedTestState beforeState, @@ -219,13 +216,17 @@ public class TestState { if (expectedBroadcast) { final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); + // If we remove the last argument (isNull()) tests fail. No idea why as the broadcast method has an + // overloaded method with nullable listener. Seems a testframework issue as it should not matter if the + // method with listener is called with null argument or the other method with no listener. We removed the + // null value from all other calls but here we can't as it breaks the test. verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull()); BroadcastMessage broadcastMessage = captor.getValue(); Assert.assertTrue(broadcastMessage instanceof AddDataMessage); Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); } else { - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } if (expectedSequenceNrMapWrite) { @@ -275,7 +276,7 @@ public class TestState { verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); if (!expectedBroadcast) - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); protectedStorageEntries.forEach(protectedStorageEntry -> { @@ -287,9 +288,9 @@ public class TestState { if (expectedBroadcast) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) - verify(this.mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), nullable(NodeAddress.class)); else - verify(this.mockBroadcaster).broadcast(any(RemoveDataMessage.class), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(RemoveDataMessage.class), nullable(NodeAddress.class)); } @@ -319,7 +320,7 @@ public class TestState { Assert.assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate); final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); - verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class)); BroadcastMessage broadcastMessage = captor.getValue(); Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage); @@ -336,7 +337,7 @@ public class TestState { Assert.assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); } - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index a9b768fc1f..2f334e06e0 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -639,7 +639,10 @@ message Filter { repeated string refundAgents = 18; repeated string bannedSignerPubKeys = 19; repeated string btc_fee_receiver_addresses = 20; - bool disable_auto_conf = 21; + int64 creation_date = 21; + string signer_pub_key_as_hex = 22; + repeated string bannedPrivilegedDevPubKeys = 23; + bool disable_auto_conf = 24; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older @@ -1087,6 +1090,7 @@ message PopmoneyAccountPayload { message RevolutAccountPayload { string account_id = 1; + string user_name = 2; } message PerfectMoneyAccountPayload {