Merge master

- fix tests
This commit is contained in:
chimp1984 2020-09-01 19:53:23 -05:00
commit 039860935d
No known key found for this signature in database
GPG Key ID: 9801B4EC591F90E3
67 changed files with 1935 additions and 1281 deletions

View File

@ -1,11 +1,11 @@
#!/usr/bin/env bats #!/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: # Prerequisites:
# #
# - bats v0.4.0 must be installed (brew install bats on macOS) # - bats-core 1.2.0+ must be installed (brew install bats-core on macOS)
# see https://github.com/sstephenson/bats/tree/v0.4.0 # see https://github.com/bats-core/bats-core
# #
# - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR # - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR
# is empty or otherwise contains an unencrypted wallet with a 0 BTC balance # is empty or otherwise contains an unencrypted wallet with a 0 BTC balance
@ -48,14 +48,14 @@
run ./bisq-cli --password="xyz" getversion run ./bisq-cli --password="xyz" getversion
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
echo "actual output: $output" >&2 echo "actual output: $output" >&2
[ "$output" = "1.3.5" ] [ "$output" = "1.3.7" ]
} }
@test "test getversion" { @test "test getversion" {
run ./bisq-cli --password=xyz getversion run ./bisq-cli --password=xyz getversion
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
echo "actual output: $output" >&2 echo "actual output: $output" >&2
[ "$output" = "1.3.5" ] [ "$output" = "1.3.7" ]
} }
@test "test setwalletpassword \"a b c\"" { @test "test setwalletpassword \"a b c\"" {
@ -190,8 +190,8 @@
[ "$output" = "Error: incorrect parameter count, expecting direction (buy|sell), currency code" ] [ "$output" = "Error: incorrect parameter count, expecting direction (buy|sell), currency code" ]
} }
@test "test getoffers buy eur check return status" { @test "test getoffers sell eur check return status" {
run ./bisq-cli --password=xyz getoffers buy eur run ./bisq-cli --password=xyz getoffers sell eur
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }

View File

@ -311,17 +311,25 @@ public class Scaffold {
bitcoinDaemon.verifyBitcoindRunning(); bitcoinDaemon.verifyBitcoindRunning();
} }
// Start Bisq apps defined by the supportingApps option, in the in proper order.
if (config.hasSupportingApp(seednode.name())) if (config.hasSupportingApp(seednode.name()))
startBisqApp(seednode, executor, countdownLatch); startBisqApp(seednode, executor, countdownLatch);
if (config.hasSupportingApp(arbdaemon.name(), arbdesktop.name())) if (config.hasSupportingApp(arbdaemon.name()))
startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); startBisqApp(arbdaemon, executor, countdownLatch);
else if (config.hasSupportingApp(arbdesktop.name()))
startBisqApp(arbdesktop, executor, countdownLatch);
if (config.hasSupportingApp(alicedaemon.name(), alicedesktop.name())) if (config.hasSupportingApp(alicedaemon.name()))
startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); startBisqApp(alicedaemon, executor, countdownLatch);
else if (config.hasSupportingApp(alicedesktop.name()))
startBisqApp(alicedesktop, executor, countdownLatch);
if (config.hasSupportingApp(bobdaemon.name(), bobdesktop.name())) if (config.hasSupportingApp(bobdaemon.name()))
startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); startBisqApp(bobdaemon, executor, countdownLatch);
else if (config.hasSupportingApp(bobdesktop.name()))
startBisqApp(bobdesktop, executor, countdownLatch);
} }
private void startBisqApp(BisqAppConfig bisqAppConfig, private void startBisqApp(BisqAppConfig bisqAppConfig,
@ -329,28 +337,24 @@ public class Scaffold {
CountDownLatch countdownLatch) CountDownLatch countdownLatch)
throws IOException, InterruptedException { throws IOException, InterruptedException {
BisqApp bisqApp; BisqApp bisqApp = createBisqApp(bisqAppConfig);
switch (bisqAppConfig) { switch (bisqAppConfig) {
case seednode: case seednode:
bisqApp = createBisqApp(seednode);
seedNodeTask = new SetupTask(bisqApp, countdownLatch); seedNodeTask = new SetupTask(bisqApp, countdownLatch);
seedNodeTaskFuture = executor.submit(seedNodeTask); seedNodeTaskFuture = executor.submit(seedNodeTask);
break; break;
case arbdaemon: case arbdaemon:
case arbdesktop: case arbdesktop:
bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon);
arbNodeTask = new SetupTask(bisqApp, countdownLatch); arbNodeTask = new SetupTask(bisqApp, countdownLatch);
arbNodeTaskFuture = executor.submit(arbNodeTask); arbNodeTaskFuture = executor.submit(arbNodeTask);
break; break;
case alicedaemon: case alicedaemon:
case alicedesktop: case alicedesktop:
bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon);
aliceNodeTask = new SetupTask(bisqApp, countdownLatch); aliceNodeTask = new SetupTask(bisqApp, countdownLatch);
aliceNodeTaskFuture = executor.submit(aliceNodeTask); aliceNodeTaskFuture = executor.submit(aliceNodeTask);
break; break;
case bobdaemon: case bobdaemon:
case bobdesktop: case bobdesktop:
bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon);
bobNodeTask = new SetupTask(bisqApp, countdownLatch); bobNodeTask = new SetupTask(bisqApp, countdownLatch);
bobNodeTaskFuture = executor.submit(bobNodeTask); bobNodeTaskFuture = executor.submit(bobNodeTask);
break; break;

View File

@ -65,13 +65,11 @@ public class ApiTestConfig {
static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; static final String ROOT_APP_DATA_DIR = "rootAppDataDir";
static final String API_PASSWORD = "apiPassword"; static final String API_PASSWORD = "apiPassword";
static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; 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 BISQ_APP_INIT_TIME = "bisqAppInitTime";
static final String SKIP_TESTS = "skipTests"; static final String SKIP_TESTS = "skipTests";
static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests";
static final String SUPPORTING_APPS = "supportingApps"; static final String SUPPORTING_APPS = "supportingApps";
static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging";
// Default values for certain options // Default values for certain options
static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; 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. // Daemon instances can use same gRPC password, but each needs a different apiPort.
public final String apiPassword; public final String apiPassword;
public final boolean runSubprojectJars; public final boolean runSubprojectJars;
public final boolean runArbNodeAsDesktop;
public final boolean runAliceNodeAsDesktop;
public final boolean runBobNodeAsDesktop;
public final long bisqAppInitTime; public final long bisqAppInitTime;
public final boolean skipTests; public final boolean skipTests;
public final boolean shutdownAfterTests; public final boolean shutdownAfterTests;
public final List<String> supportingApps; public final List<String> supportingApps;
public final boolean enableBisqDebugging;
// Immutable system configurations set in the constructor. // Immutable system configurations set in the constructor.
public final String bitcoinDatadir; public final String bitcoinDatadir;
@ -202,27 +198,6 @@ public class ApiTestConfig {
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> 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<Boolean> runAliceNodeAsDesktopOpt =
parser.accepts(RUN_ALICE_NODE_AS_DESKTOP,
"Run Alice node as desktop")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> runBobNodeAsDesktopOpt =
parser.accepts(RUN_BOB_NODE_AS_DESKTOP,
"Run Bob node as desktop")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Long> bisqAppInitTimeOpt = ArgumentAcceptingOptionSpec<Long> bisqAppInitTimeOpt =
parser.accepts(BISQ_APP_INIT_TIME, parser.accepts(BISQ_APP_INIT_TIME,
"Amount of time (ms) to wait on a Bisq instance's initialization") "Amount of time (ms) to wait on a Bisq instance's initialization")
@ -251,6 +226,12 @@ public class ApiTestConfig {
.ofType(String.class) .ofType(String.class)
.defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon");
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
parser.accepts(ENABLE_BISQ_DEBUGGING,
"Start Bisq apps with remote debug options")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
try { try {
CompositeOptionSet options = new CompositeOptionSet(); CompositeOptionSet options = new CompositeOptionSet();
@ -302,13 +283,11 @@ public class ApiTestConfig {
this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt);
this.apiPassword = options.valueOf(apiPasswordOpt); this.apiPassword = options.valueOf(apiPasswordOpt);
this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); 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.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt);
this.skipTests = options.valueOf(skipTestsOpt); this.skipTests = options.valueOf(skipTestsOpt);
this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt);
this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(","));
this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt);
// Assign values to special-case static fields. // Assign values to special-case static fields.
BASH_PATH_VALUE = bashPath; BASH_PATH_VALUE = bashPath;

View File

@ -30,58 +30,64 @@ import bisq.daemon.app.BisqDaemonMain;
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a> @see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a>
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md">dao-setup.md</a> @see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md">dao-setup.md</a>
*/ */
@SuppressWarnings("unused")
public enum BisqAppConfig { public enum BisqAppConfig {
seednode("bisq-BTC_REGTEST_Seed_2002", seednode("bisq-BTC_REGTEST_Seed_2002",
"bisq-seednode", "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(), SeedNodeMain.class.getName(),
2002, 2002,
5120, 5120,
-1), -1,
49996),
arbdaemon("bisq-BTC_REGTEST_Arb_dao", arbdaemon("bisq-BTC_REGTEST_Arb_dao",
"bisq-daemon", "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(), BisqDaemonMain.class.getName(),
4444, 4444,
5121, 5121,
9997), 9997,
49997),
arbdesktop("bisq-BTC_REGTEST_Arb_dao", arbdesktop("bisq-BTC_REGTEST_Arb_dao",
"bisq-desktop", "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(), BisqAppMain.class.getName(),
4444, 4444,
5121, 5121,
-1), -1,
49997),
alicedaemon("bisq-BTC_REGTEST_Alice_dao", alicedaemon("bisq-BTC_REGTEST_Alice_dao",
"bisq-daemon", "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(), BisqDaemonMain.class.getName(),
7777, 7777,
5122, 5122,
9998), 9998,
49998),
alicedesktop("bisq-BTC_REGTEST_Alice_dao", alicedesktop("bisq-BTC_REGTEST_Alice_dao",
"bisq-desktop", "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(), BisqAppMain.class.getName(),
7777, 7777,
5122, 5122,
-1), -1,
49998),
bobdaemon("bisq-BTC_REGTEST_Bob_dao", bobdaemon("bisq-BTC_REGTEST_Bob_dao",
"bisq-daemon", "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(), BisqDaemonMain.class.getName(),
8888, 8888,
5123, 5123,
9999), 9999,
49999),
bobdesktop("bisq-BTC_REGTEST_Bob_dao", bobdesktop("bisq-BTC_REGTEST_Bob_dao",
"bisq-desktop", "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(), BisqAppMain.class.getName(),
8888, 8888,
5123, 5123,
-1); -1,
49999);
public final String appName; public final String appName;
public final String startupScript; public final String startupScript;
@ -91,6 +97,7 @@ public enum BisqAppConfig {
public final int rpcBlockNotificationPort; public final int rpcBlockNotificationPort;
// Daemons can use a global gRPC password, but each needs a unique apiPort. // Daemons can use a global gRPC password, but each needs a unique apiPort.
public final int apiPort; public final int apiPort;
public final int remoteDebugPort;
BisqAppConfig(String appName, BisqAppConfig(String appName,
String startupScript, String startupScript,
@ -98,7 +105,8 @@ public enum BisqAppConfig {
String mainClassName, String mainClassName,
int nodePort, int nodePort,
int rpcBlockNotificationPort, int rpcBlockNotificationPort,
int apiPort) { int apiPort,
int remoteDebugPort) {
this.appName = appName; this.appName = appName;
this.startupScript = startupScript; this.startupScript = startupScript;
this.javaOpts = javaOpts; this.javaOpts = javaOpts;
@ -106,6 +114,7 @@ public enum BisqAppConfig {
this.nodePort = nodePort; this.nodePort = nodePort;
this.rpcBlockNotificationPort = rpcBlockNotificationPort; this.rpcBlockNotificationPort = rpcBlockNotificationPort;
this.apiPort = apiPort; this.apiPort = apiPort;
this.remoteDebugPort = remoteDebugPort;
} }
@Override @Override
@ -118,6 +127,7 @@ public enum BisqAppConfig {
", nodePort=" + nodePort + "\n" + ", nodePort=" + nodePort + "\n" +
", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" +
", apiPort=" + apiPort + "\n" + ", apiPort=" + apiPort + "\n" +
", remoteDebugPort=" + remoteDebugPort + "\n" +
'}'; '}';
} }
} }

View File

@ -53,6 +53,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
private final boolean useLocalhostForP2P; private final boolean useLocalhostForP2P;
public final boolean useDevPrivilegeKeys; public final boolean useDevPrivilegeKeys;
private final String findBisqPidScript; private final String findBisqPidScript;
private final String debugOpts;
public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
super(bisqAppConfig.appName, config); super(bisqAppConfig.appName, config);
@ -67,6 +68,9 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
this.useDevPrivilegeKeys = true; this.useDevPrivilegeKeys = true;
this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest") this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest")
+ "/scripts/get-bisq-pid.sh"; + "/scripts/get-bisq-pid.sh";
this.debugOpts = config.enableBisqDebugging
? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + bisqAppConfig.remoteDebugPort
: "";
} }
@Override @Override
@ -112,7 +116,6 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
if (isAlive(pid)) { if (isAlive(pid)) {
this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName))); this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName)));
return;
} }
} catch (Exception e) { } catch (Exception e) {
@ -209,7 +212,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
} }
private String getJavaOptsSpec() { private String getJavaOptsSpec() {
return "export JAVA_OPTS=" + bisqAppConfig.javaOpts + "; "; return "export JAVA_OPTS=\"" + bisqAppConfig.javaOpts + debugOpts + "\"; ";
} }
private List<String> getOptsList() { private List<String> getOptsList() {

View File

@ -28,6 +28,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import bisq.apitest.config.ApiTestConfig; import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper; import bisq.apitest.method.BitcoinCliHelper;
import bisq.cli.GrpcStubs;
/** /**
* Base class for all test types: 'method', 'scenario' and 'e2e'. * Base class for all test types: 'method', 'scenario' and 'e2e'.
@ -65,19 +66,19 @@ public class ApiTestCase {
public static void setUpScaffold(String supportingApps) public static void setUpScaffold(String supportingApps)
throws InterruptedException, ExecutionException, IOException { 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(); scaffold = new Scaffold(supportingApps).setUp();
config = scaffold.config; config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((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 { throws InterruptedException, ExecutionException, IOException {
scaffold = new Scaffold(new String[]{}).setUp(); scaffold = new Scaffold(params).setUp();
config = scaffold.config; config = scaffold.config;
grpcStubs = new GrpcStubs(alicedaemon, config).init(); grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword);
} }
public static void tearDownScaffold() { public static void tearDownScaffold() {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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.
}
}
}

View File

@ -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");
}
}

View File

@ -19,12 +19,12 @@ package bisq.asset.coins;
import bisq.asset.AltCoinAccountDisclaimer; import bisq.asset.AltCoinAccountDisclaimer;
import bisq.asset.Coin; import bisq.asset.Coin;
import bisq.asset.RegexAddressValidator; import bisq.asset.LiquidBitcoinAddressValidator;
@AltCoinAccountDisclaimer("account.altcoin.popup.liquidbitcoin.msg") @AltCoinAccountDisclaimer("account.altcoin.popup.liquidbitcoin.msg")
public class LiquidBitcoin extends Coin { public class LiquidBitcoin extends Coin {
public LiquidBitcoin() { 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());
} }
} }

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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");
}
}

View File

@ -104,6 +104,8 @@ bisq.asset.coins.Spectrecoin
bisq.asset.coins.Starwels bisq.asset.coins.Starwels
bisq.asset.coins.SUB1X bisq.asset.coins.SUB1X
bisq.asset.coins.TEO bisq.asset.coins.TEO
bisq.asset.coins.TetherUSDLiquid
bisq.asset.coins.TetherUSDOmni
bisq.asset.coins.TurtleCoin bisq.asset.coins.TurtleCoin
bisq.asset.coins.UnitedCommunityCoin bisq.asset.coins.UnitedCommunityCoin
bisq.asset.coins.Unobtanium bisq.asset.coins.Unobtanium
@ -123,6 +125,7 @@ bisq.asset.coins.ZeroClassic
bisq.asset.tokens.AugmintEuro bisq.asset.tokens.AugmintEuro
bisq.asset.tokens.DaiStablecoin bisq.asset.tokens.DaiStablecoin
bisq.asset.tokens.EtherStone bisq.asset.tokens.EtherStone
bisq.asset.tokens.TetherUSDERC20
bisq.asset.tokens.TrueUSD bisq.asset.tokens.TrueUSD
bisq.asset.tokens.USDCoin bisq.asset.tokens.USDCoin
bisq.asset.tokens.VectorspaceAI bisq.asset.tokens.VectorspaceAI

View File

@ -51,7 +51,7 @@ configure(subprojects) {
javaxAnnotationVersion = '1.2' javaxAnnotationVersion = '1.2'
jcsvVersion = '1.4.0' jcsvVersion = '1.4.0'
jetbrainsAnnotationsVersion = '13.0' jetbrainsAnnotationsVersion = '13.0'
jfoenixVersion = '9.0.6' jfoenixVersion = '9.0.10'
joptVersion = '5.0.4' joptVersion = '5.0.4'
jsonsimpleVersion = '1.1.1' jsonsimpleVersion = '1.1.1'
junitVersion = '4.12' junitVersion = '4.12'

View File

@ -23,17 +23,12 @@ import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletRequest; import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import joptsimple.OptionParser; import joptsimple.OptionParser;
@ -43,7 +38,6 @@ import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -133,21 +127,11 @@ public class CliMain {
if (password == null) if (password == null)
throw new IllegalArgumentException("missing required 'password' option"); throw new IllegalArgumentException("missing required 'password' option");
var credentials = new PasswordCallCredentials(password); GrpcStubs grpcStubs = new GrpcStubs(host, port, password);
var versionService = grpcStubs.versionService;
var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); var offersService = grpcStubs.offersService;
Runtime.getRuntime().addShutdownHook(new Thread(() -> { var paymentAccountsService = grpcStubs.paymentAccountsService;
try { var walletsService = grpcStubs.walletsService;
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);
try { try {
switch (method) { switch (method) {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -18,12 +18,15 @@
package bisq.common.proto; package bisq.common.proto;
import bisq.common.Proto; import bisq.common.Proto;
import bisq.common.util.CollectionUtils;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import com.google.protobuf.ProtocolStringList;
import com.google.common.base.Enums; import com.google.common.base.Enums;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -101,4 +104,9 @@ public class ProtoUtil {
Function<? super Message, T> extra) { Function<? super Message, T> extra) {
return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList()); return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList());
} }
public static List<String> protocolStringListToList(ProtocolStringList protocolStringList) {
return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList);
}
} }

View File

@ -180,7 +180,7 @@ public class SignedWitnessService {
public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) {
return getSignedWitnessSet(accountAgeWitness).stream() return getSignedWitnessSet(accountAgeWitness).stream()
.map(SignedWitness::getWitnessOwnerPubKey) .map(SignedWitness::getWitnessOwnerPubKey)
.anyMatch(ownerPubKey -> filterManager.isSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); .anyMatch(ownerPubKey -> filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey)));
} }
private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) {
@ -442,7 +442,7 @@ public class SignedWitnessService {
private boolean isValidSignerWitnessInternal(SignedWitness signedWitness, private boolean isValidSignerWitnessInternal(SignedWitness signedWitness,
long childSignedWitnessDateMillis, long childSignedWitnessDateMillis,
Stack<P2PDataStorage.ByteArray> excludedPubKeys) { Stack<P2PDataStorage.ByteArray> excludedPubKeys) {
if (filterManager.isSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { if (filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) {
return false; return false;
} }
if (!verifySignature(signedWitness)) { if (!verifySignature(signedWitness)) {

View File

@ -718,13 +718,13 @@ public class AccountAgeWitnessService {
filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) ||
filterManager.isPaymentMethodBanned( filterManager.isPaymentMethodBanned(
PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) ||
filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getBuyerPaymentAccountPayload(), filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload(),
new PaymentAccountFilter[1]) || new PaymentAccountFilter[1]) ||
filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getSellerPaymentAccountPayload(), filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload(),
new PaymentAccountFilter[1]) || new PaymentAccountFilter[1]) ||
filterManager.isSignerPubKeyBanned( filterManager.isWitnessSignerPubKeyBanned(
Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) ||
filterManager.isSignerPubKeyBanned( filterManager.isWitnessSignerPubKeyBanned(
Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes())); Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes()));
return !isFiltered; return !isFiltered;
} }

View File

@ -96,6 +96,7 @@ public class BisqHeadlessApp implements HeadlessApp {
bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString())); bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString()));
bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
//TODO move to bisqSetup //TODO move to bisqSetup
corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));

View File

@ -44,6 +44,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert; import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.TradeLimits; import bisq.core.payment.TradeLimits;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
@ -221,6 +222,9 @@ public class BisqSetup {
@Setter @Setter
@Nullable @Nullable
private Runnable showPopupIfInvalidBtcConfigHandler; private Runnable showPopupIfInvalidBtcConfigHandler;
@Setter
@Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@Getter @Getter
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
@ -824,6 +828,8 @@ public class BisqSetup {
priceAlert.onAllServicesInitialized(); priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized();
user.onAllServicesInitialized(revolutAccountsUpdateHandler);
allBasicServicesInitialized = true; allBasicServicesInitialized = true;
} }

View File

@ -164,11 +164,13 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
// We check every hour if we are in the target hour. // We check every hour if we are in the target hour.
UserThread.runPeriodically(() -> { 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) { if (currentHour == target) {
log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" +
"Shut down node at hour {}" + "Shut down node at hour {} (UTC time is {})" +
"\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", target); "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n",
target,
ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toString());
shutDown(gracefulShutDownHandler); shutDown(gracefulShutDownHandler);
} }
}, TimeUnit.MINUTES.toSeconds(10)); }, TimeUnit.MINUTES.toSeconds(10));

View File

@ -156,7 +156,7 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
public void broadcastMyStateHash(StH myStateHash) { public void broadcastMyStateHash(StH myStateHash) {
NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash); NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash);
broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress(), null); broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress());
} }
public void requestHashes(int fromHeight, String peersAddress) { public void requestHashes(int fromHeight, String peersAddress) {

View File

@ -105,7 +105,7 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List
log.info("Publish new block at height={} and block hash={}", block.getHeight(), block.getHash()); log.info("Publish new block at height={} and block hash={}", block.getHeight(), block.getHash());
RawBlock rawBlock = RawBlock.fromBlock(block); RawBlock rawBlock = RawBlock.fromBlock(block);
NewBlockBroadcastMessage newBlockBroadcastMessage = new NewBlockBroadcastMessage(rawBlock); NewBlockBroadcastMessage newBlockBroadcastMessage = new NewBlockBroadcastMessage(rawBlock);
broadcaster.broadcast(newBlockBroadcastMessage, networkNode.getNodeAddress(), null); broadcaster.broadcast(newBlockBroadcastMessage, networkNode.getNodeAddress());
} }

View File

@ -238,7 +238,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
log.debug("We received a new message from peer {} and broadcast it to our peers. extBlockId={}", log.debug("We received a new message from peer {} and broadcast it to our peers. extBlockId={}",
connection.getPeersNodeAddressOptional().orElse(null), extBlockId); connection.getPeersNodeAddressOptional().orElse(null), extBlockId);
receivedBlocks.add(extBlockId); receivedBlocks.add(extBlockId);
broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null), null); broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null));
listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage)); listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage));
} else { } else {
log.debug("We had that message already and do not further broadcast it. extBlockId={}", extBlockId); log.debug("We had that message already and do not further broadcast it. extBlockId={}", extBlockId);

View File

@ -21,8 +21,10 @@ import bisq.network.p2p.storage.payload.ExpirablePayload;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
import bisq.common.crypto.Sig; import bisq.common.crypto.Sig;
import bisq.common.proto.ProtoUtil;
import bisq.common.util.CollectionUtils; import bisq.common.util.CollectionUtils;
import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.ExtraDataMapValidator;
import bisq.common.util.Utilities;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -30,152 +32,140 @@ import com.google.common.annotations.VisibleForTesting;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.EqualsAndHashCode; import lombok.Value;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
@Getter @Value
@EqualsAndHashCode
@ToString
public final class Filter implements ProtectedStoragePayload, ExpirablePayload { public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
private final List<String> bannedOfferIds; private final List<String> bannedOfferIds;
private final List<String> bannedNodeAddress; private final List<String> bannedNodeAddress;
private final List<PaymentAccountFilter> bannedPaymentAccounts; private final List<PaymentAccountFilter> 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<String> bannedCurrencies; private final List<String> bannedCurrencies;
@Nullable
private final List<String> bannedPaymentMethods; private final List<String> bannedPaymentMethods;
// added in v0.6.0
@Nullable
private final List<String> arbitrators; private final List<String> arbitrators;
@Nullable
private final List<String> seedNodes; private final List<String> seedNodes;
@Nullable
private final List<String> priceRelayNodes; private final List<String> priceRelayNodes;
private final boolean preventPublicBtcNetwork; private final boolean preventPublicBtcNetwork;
// added in v0.6.2
@Nullable
private final List<String> btcNodes; private final List<String> 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<String> mediators;
private final List<String> refundAgents;
private final List<String> bannedAccountWitnessSignerPubKeys;
private final List<String> btcFeeReceiverAddresses;
private final long creationDate;
private final List<String> 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 // 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 // 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. // field in a class would break that hash and therefore break the storage mechanism.
@Nullable @Nullable
private Map<String, String> extraDataMap; private Map<String, String> extraDataMap;
private PublicKey ownerPubKey;
// added in v0.9.4 private transient PublicKey ownerPubKey;
private final boolean disableDao;
// added in v0.9.8 // added at v1.3.8
@Nullable
private final String disableDaoBelowVersion;
@Nullable
private final String disableTradeBelowVersion;
// added in v1.1.6
@Nullable
private final List<String> mediators;
// added in v1.2.0
@Nullable
private final List<String> refundAgents;
// added in v1.2.x
@Nullable
private final List<String> bannedSignerPubKeys;
// added in v1.3.2
@Nullable
private final List<String> btcFeeReceiverAddresses;
// added after v1.3.7
private final boolean disableAutoConf; private final boolean disableAutoConf;
public Filter(List<String> bannedOfferIds, // After we have created the signature from the filter data we clone it and apply the signature
List<String> bannedNodeAddress, static Filter cloneWithSig(Filter filter, String signatureAsBase64) {
List<PaymentAccountFilter> bannedPaymentAccounts, return new Filter(filter.getBannedOfferIds(),
@Nullable List<String> bannedCurrencies, filter.getBannedNodeAddress(),
@Nullable List<String> bannedPaymentMethods, filter.getBannedPaymentAccounts(),
@Nullable List<String> arbitrators, filter.getBannedCurrencies(),
@Nullable List<String> seedNodes, filter.getBannedPaymentMethods(),
@Nullable List<String> priceRelayNodes, filter.getArbitrators(),
boolean preventPublicBtcNetwork, filter.getSeedNodes(),
@Nullable List<String> btcNodes, filter.getPriceRelayNodes(),
boolean disableDao, filter.isPreventPublicBtcNetwork(),
@Nullable String disableDaoBelowVersion, filter.getBtcNodes(),
@Nullable String disableTradeBelowVersion, filter.isDisableDao(),
@Nullable List<String> mediators, filter.getDisableDaoBelowVersion(),
@Nullable List<String> refundAgents, filter.getDisableTradeBelowVersion(),
@Nullable List<String> bannedSignerPubKeys, filter.getMediators(),
@Nullable List<String> btcFeeReceiverAddresses, filter.getRefundAgents(),
boolean disableAutoConf) { filter.getBannedAccountWitnessSignerPubKeys(),
this.bannedOfferIds = bannedOfferIds; filter.getBtcFeeReceiverAddresses(),
this.bannedNodeAddress = bannedNodeAddress; filter.getOwnerPubKeyBytes(),
this.bannedPaymentAccounts = bannedPaymentAccounts; filter.getCreationDate(),
this.bannedCurrencies = bannedCurrencies; filter.getExtraDataMap(),
this.bannedPaymentMethods = bannedPaymentMethods; signatureAsBase64,
this.arbitrators = arbitrators; filter.getSignerPubKeyAsHex(),
this.seedNodes = seedNodes; filter.getBannedPrivilegedDevPubKeys(),
this.priceRelayNodes = priceRelayNodes; filter.isDisableAutoConf());
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;
} }
// 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<String> bannedOfferIds, public Filter(List<String> bannedOfferIds,
List<String> bannedNodeAddress, List<String> bannedNodeAddress,
List<PaymentAccountFilter> bannedPaymentAccounts, List<PaymentAccountFilter> bannedPaymentAccounts,
@Nullable List<String> bannedCurrencies, List<String> bannedCurrencies,
@Nullable List<String> bannedPaymentMethods, List<String> bannedPaymentMethods,
@Nullable List<String> arbitrators, List<String> arbitrators,
@Nullable List<String> seedNodes, List<String> seedNodes,
@Nullable List<String> priceRelayNodes, List<String> priceRelayNodes,
boolean preventPublicBtcNetwork, boolean preventPublicBtcNetwork,
@Nullable List<String> btcNodes, List<String> btcNodes,
boolean disableDao, boolean disableDao,
@Nullable String disableDaoBelowVersion, String disableDaoBelowVersion,
@Nullable String disableTradeBelowVersion, String disableTradeBelowVersion,
String signatureAsBase64, List<String> mediators,
byte[] ownerPubKeyBytes, List<String> refundAgents,
@Nullable Map<String, String> extraDataMap, List<String> bannedAccountWitnessSignerPubKeys,
@Nullable List<String> mediators, List<String> btcFeeReceiverAddresses,
@Nullable List<String> refundAgents, PublicKey ownerPubKey,
@Nullable List<String> bannedSignerPubKeys, String signerPubKeyAsHex,
@Nullable List<String> btcFeeReceiverAddresses, List<String> bannedPrivilegedDevPubKeys,
boolean disableAutoConf) { boolean disableAutoConf) {
this(bannedOfferIds, this(bannedOfferIds,
bannedNodeAddress, bannedNodeAddress,
@ -192,76 +182,146 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
disableTradeBelowVersion, disableTradeBelowVersion,
mediators, mediators,
refundAgents, refundAgents,
bannedSignerPubKeys, bannedAccountWitnessSignerPubKeys,
btcFeeReceiverAddresses, btcFeeReceiverAddresses,
Sig.getPublicKeyBytes(ownerPubKey),
System.currentTimeMillis(),
null,
null,
signerPubKeyAsHex,
bannedPrivilegedDevPubKeys,
disableAutoConf); disableAutoConf);
this.signatureAsBase64 = signatureAsBase64; }
this.ownerPubKeyBytes = ownerPubKeyBytes;
this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap);
ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes);
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
public Filter(List<String> bannedOfferIds,
List<String> bannedNodeAddress,
List<PaymentAccountFilter> bannedPaymentAccounts,
List<String> bannedCurrencies,
List<String> bannedPaymentMethods,
List<String> arbitrators,
List<String> seedNodes,
List<String> priceRelayNodes,
boolean preventPublicBtcNetwork,
List<String> btcNodes,
boolean disableDao,
String disableDaoBelowVersion,
String disableTradeBelowVersion,
List<String> mediators,
List<String> refundAgents,
List<String> bannedAccountWitnessSignerPubKeys,
List<String> btcFeeReceiverAddresses,
byte[] ownerPubKeyBytes,
long creationDate,
@Nullable Map<String, String> extraDataMap,
@Nullable String signatureAsBase64,
String signerPubKeyAsHex,
List<String> 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 @Override
public protobuf.StoragePayload toProtoMessage() { public protobuf.StoragePayload toProtoMessage() {
checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null");
checkNotNull(ownerPubKeyBytes, "ownerPubKeyBytes must not be null");
List<protobuf.PaymentAccountFilter> paymentAccountFilterList = bannedPaymentAccounts.stream() List<protobuf.PaymentAccountFilter> paymentAccountFilterList = bannedPaymentAccounts.stream()
.map(PaymentAccountFilter::toProtoMessage) .map(PaymentAccountFilter::toProtoMessage)
.collect(Collectors.toList()); .collect(Collectors.toList());
final protobuf.Filter.Builder builder = protobuf.Filter.newBuilder()
.addAllBannedOfferIds(bannedOfferIds) protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds)
.addAllBannedNodeAddress(bannedNodeAddress) .addAllBannedNodeAddress(bannedNodeAddress)
.addAllBannedPaymentAccounts(paymentAccountFilterList) .addAllBannedPaymentAccounts(paymentAccountFilterList)
.setSignatureAsBase64(signatureAsBase64) .addAllBannedCurrencies(bannedCurrencies)
.setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .addAllBannedPaymentMethods(bannedPaymentMethods)
.addAllArbitrators(arbitrators)
.addAllSeedNodes(seedNodes)
.addAllPriceRelayNodes(priceRelayNodes)
.setPreventPublicBtcNetwork(preventPublicBtcNetwork) .setPreventPublicBtcNetwork(preventPublicBtcNetwork)
.addAllBtcNodes(btcNodes)
.setDisableDao(disableDao) .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); .setDisableAutoConf(disableAutoConf);
Optional.ofNullable(bannedCurrencies).ifPresent(builder::addAllBannedCurrencies); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64);
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(extraDataMap).ifPresent(builder::putAllExtraData); 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(); return protobuf.StoragePayload.newBuilder().setFilter(builder).build();
} }
public static Filter fromProto(protobuf.Filter proto) { public static Filter fromProto(protobuf.Filter proto) {
return new Filter(new ArrayList<>(proto.getBannedOfferIdsList()), List<PaymentAccountFilter> bannedPaymentAccountsList = proto.getBannedPaymentAccountsList().stream()
new ArrayList<>(proto.getBannedNodeAddressList()), .map(PaymentAccountFilter::fromProto)
proto.getBannedPaymentAccountsList().stream() .collect(Collectors.toList());
.map(PaymentAccountFilter::fromProto)
.collect(Collectors.toList()),
CollectionUtils.isEmpty(proto.getBannedCurrenciesList()) ? null : new ArrayList<>(proto.getBannedCurrenciesList()), return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()),
CollectionUtils.isEmpty(proto.getBannedPaymentMethodsList()) ? null : new ArrayList<>(proto.getBannedPaymentMethodsList()), ProtoUtil.protocolStringListToList(proto.getBannedNodeAddressList()),
CollectionUtils.isEmpty(proto.getArbitratorsList()) ? null : new ArrayList<>(proto.getArbitratorsList()), bannedPaymentAccountsList,
CollectionUtils.isEmpty(proto.getSeedNodesList()) ? null : new ArrayList<>(proto.getSeedNodesList()), ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()),
CollectionUtils.isEmpty(proto.getPriceRelayNodesList()) ? null : new ArrayList<>(proto.getPriceRelayNodesList()), ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()),
ProtoUtil.protocolStringListToList(proto.getArbitratorsList()),
ProtoUtil.protocolStringListToList(proto.getSeedNodesList()),
ProtoUtil.protocolStringListToList(proto.getPriceRelayNodesList()),
proto.getPreventPublicBtcNetwork(), proto.getPreventPublicBtcNetwork(),
CollectionUtils.isEmpty(proto.getBtcNodesList()) ? null : new ArrayList<>(proto.getBtcNodesList()), ProtoUtil.protocolStringListToList(proto.getBtcNodesList()),
proto.getDisableDao(), proto.getDisableDao(),
proto.getDisableDaoBelowVersion().isEmpty() ? null : proto.getDisableDaoBelowVersion(), proto.getDisableDaoBelowVersion(),
proto.getDisableTradeBelowVersion().isEmpty() ? null : proto.getDisableTradeBelowVersion(), proto.getDisableTradeBelowVersion(),
proto.getSignatureAsBase64(), ProtoUtil.protocolStringListToList(proto.getMediatorsList()),
ProtoUtil.protocolStringListToList(proto.getRefundAgentsList()),
ProtoUtil.protocolStringListToList(proto.getBannedSignerPubKeysList()),
ProtoUtil.protocolStringListToList(proto.getBtcFeeReceiverAddressesList()),
proto.getOwnerPubKeyBytes().toByteArray(), proto.getOwnerPubKeyBytes().toByteArray(),
proto.getCreationDate(),
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(),
CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList()), proto.getSignatureAsBase64(),
CollectionUtils.isEmpty(proto.getRefundAgentsList()) ? null : new ArrayList<>(proto.getRefundAgentsList()), proto.getSignerPubKeyAsHex(),
CollectionUtils.isEmpty(proto.getBannedSignerPubKeysList()) ? ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()),
null : new ArrayList<>(proto.getBannedSignerPubKeysList()), proto.getDisableAutoConf()
CollectionUtils.isEmpty(proto.getBtcFeeReceiverAddressesList()) ? null : );
new ArrayList<>(proto.getBtcFeeReceiverAddressesList()),
proto.getDisableAutoConf());
} }
@ -274,13 +334,6 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
return TimeUnit.DAYS.toMillis(180); return TimeUnit.DAYS.toMillis(180);
} }
void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) {
this.signatureAsBase64 = signatureAsBase64;
this.ownerPubKey = ownerPubKey;
ownerPubKeyBytes = Sig.getPublicKeyBytes(this.ownerPubKey);
}
@Override @Override
public String toString() { public String toString() {
return "Filter{" + return "Filter{" +
@ -294,15 +347,20 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
",\n priceRelayNodes=" + priceRelayNodes + ",\n priceRelayNodes=" + priceRelayNodes +
",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork + ",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork +
",\n btcNodes=" + btcNodes + ",\n btcNodes=" + btcNodes +
",\n extraDataMap=" + extraDataMap + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' +
",\n signerPubKeyAsHex='" + signerPubKeyAsHex + '\'' +
",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) +
",\n disableDao=" + disableDao + ",\n disableDao=" + disableDao +
",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' + ",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' +
",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' +
",\n mediators=" + mediators + ",\n mediators=" + mediators +
",\n refundAgents=" + refundAgents + ",\n refundAgents=" + refundAgents +
",\n bannedSignerPubKeys=" + bannedSignerPubKeys + ",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys +
",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys +
",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses +
",\n disableAutoConf=" + disableAutoConf + ",\n disableAutoConf=" + disableAutoConf +
",\n creationDate=" + creationDate +
",\n extraDataMap=" + extraDataMap +
"\n}"; "\n}";
} }
} }

View File

@ -29,9 +29,7 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.P2PServiceListener; import bisq.network.p2p.P2PServiceListener;
import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.HashMapChangedListener;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry; 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.DevEnv;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.config.Config; import bisq.common.config.Config;
@ -39,7 +37,7 @@ import bisq.common.config.ConfigFileEditor;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import org.bitcoinj.core.ECKey; import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Utils; import org.bitcoinj.core.Sha256Hash;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -47,33 +45,36 @@ import javax.inject.Named;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; 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.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.bitcoinj.core.Utils.HEX; 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 { public class FilterManager {
private static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes";
private static final Logger log = LoggerFactory.getLogger(FilterManager.class); private static final String BANNED_SEED_NODES = "bannedSeedNodes";
private static final String BANNED_BTC_NODES = "bannedBtcNodes";
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";
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -90,16 +91,15 @@ public class FilterManager {
private final Preferences preferences; private final Preferences preferences;
private final ConfigFileEditor configFileEditor; private final ConfigFileEditor configFileEditor;
private final ProvidersRepository providersRepository; private final ProvidersRepository providersRepository;
private boolean ignoreDevMsg; private final boolean ignoreDevMsg;
private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>(); private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private final List<String> publicKeys;
private final String pubKeyAsHex;
private ECKey filterSigningKey; private ECKey filterSigningKey;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, Initialization // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Inject @Inject
@ -118,48 +118,53 @@ public class FilterManager {
this.configFileEditor = new ConfigFileEditor(config.configFile); this.configFileEditor = new ConfigFileEditor(config.configFile);
this.providersRepository = providersRepository; this.providersRepository = providersRepository;
this.ignoreDevMsg = ignoreDevMsg; this.ignoreDevMsg = ignoreDevMsg;
pubKeyAsHex = useDevPrivilegeKeys ?
DevEnv.DEV_PRIVILEGE_PUB_KEY : publicKeys = useDevPrivilegeKeys ?
"022ac7b7766b0aedff82962522c2c14fb8d1961dabef6e5cfd10edc679456a32f1"; Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) :
List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27",
"029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f",
"034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9");
} }
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
if (!ignoreDevMsg) { if (ignoreDevMsg) {
return;
final List<ProtectedStorageEntry> 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<ProtectedStorageEntry> 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<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) {
Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload();
if (verifySignature(filter) && getFilter().equals(filter))
resetFilters();
}
});
}
});
} }
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<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.stream()
.filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter)
.forEach(protectedStorageEntry -> {
Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload();
onFilterAddedFromNetwork(filter);
});
}
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.stream()
.filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter)
.forEach(protectedStorageEntry -> {
Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload();
onFilterRemovedFromNetwork(filter);
});
}
});
p2PService.addP2PServiceListener(new P2PServiceListener() { p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override @Override
public void onDataReceived() { public void onDataReceived() {
@ -175,12 +180,12 @@ public class FilterManager {
@Override @Override
public void onUpdatedDataReceived() { public void onUpdatedDataReceived() {
// We should have received all data at that point and if the filers were not set we // We should have received all data at that point and if the filters were not set we
// clean up as it might be that we missed the filter remove message if we have not been online. // clean up the persisted banned nodes in the options file as it might be that we missed the filter
UserThread.runAfter(() -> { // remove message if we have not been online.
if (filterProperty.get() == null) if (filterProperty.get() == null) {
resetFilters(); clearBannedNodes();
}, 1); }
} }
@Override @Override
@ -201,54 +206,94 @@ public class FilterManager {
}); });
} }
private void resetFilters() { public boolean isPrivilegedDevPubKeyBanned(String pubKeyAsHex) {
saveBannedNodes(BANNED_BTC_NODES, null); Filter filter = getFilter();
saveBannedNodes(BANNED_SEED_NODES, null); if (filter == 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<String> 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 {
return false; return false;
} }
return filter.getBannedPrivilegedDevPubKeys().contains(pubKeyAsHex);
} }
private void saveBannedNodes(String optionName, List<String> bannedNodes) { public boolean canAddDevFilter(String privKeyString) {
if (bannedNodes != null) if (privKeyString == null || privKeyString.isEmpty()) {
configFileEditor.setOption(optionName, String.join(",", bannedNodes)); return false;
else }
configFileEditor.clearOption(optionName); 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);
}
/////////////////////////////////////////////////////////////////////////////////////////// public void addDevFilter(Filter filterWithoutSig, String privKeyString) {
// API 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) { public void addListener(Listener listener) {
listeners.add(listener); listeners.add(listener);
@ -263,85 +308,15 @@ public class FilterManager {
return filterProperty.get(); 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 @Nullable
public Filter getDevelopersFilter() { public Filter getDevFilter() {
return user.getDevelopersFilter(); return user.getDevelopersFilter();
} }
public PublicKey getOwnerPubKey() {
return keyRing.getSignatureKeyPair().getPublic();
}
public boolean isCurrencyBanned(String currencyCode) { public boolean isCurrencyBanned(String currencyCode) {
return getFilter() != null && return getFilter() != null &&
getFilter().getBannedCurrencies() != null && getFilter().getBannedCurrencies() != null &&
@ -396,8 +371,8 @@ public class FilterManager {
return requireUpdateToNewVersion; return requireUpdateToNewVersion;
} }
public boolean isPeersPaymentAccountDataAreBanned(PaymentAccountPayload paymentAccountPayload, public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload,
PaymentAccountFilter[] appliedPaymentAccountFilter) { PaymentAccountFilter[] appliedPaymentAccountFilter) {
return getFilter() != null && return getFilter() != null &&
getFilter().getBannedPaymentAccounts().stream() getFilter().getBannedPaymentAccounts().stream()
.anyMatch(paymentAccountFilter -> { .anyMatch(paymentAccountFilter -> {
@ -419,11 +394,183 @@ public class FilterManager {
}); });
} }
public boolean isSignerPubKeyBanned(String signerPubKeyAsHex) { public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) {
return getFilter() != null && return getFilter() != null &&
getFilter().getBannedSignerPubKeys() != null && getFilter().getBannedAccountWitnessSignerPubKeys() != null &&
getFilter().getBannedSignerPubKeys().stream() getFilter().getBannedAccountWitnessSignerPubKeys().stream()
.anyMatch(e -> e.equals(signerPubKeyAsHex)); .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<String> 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<String> 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());
}
} }

View File

@ -129,7 +129,9 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
} }
public String toFriendlyString() { 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() { public String toPlainString() {

View File

@ -48,6 +48,9 @@ import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; 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 @EqualsAndHashCode
@Getter @Getter
@Slf4j @Slf4j

View File

@ -36,11 +36,15 @@ public final class RevolutAccount extends PaymentAccount {
return new RevolutAccountPayload(paymentMethod.getId(), id); return new RevolutAccountPayload(paymentMethod.getId(), id);
} }
public void setAccountId(String accountId) { public void setUserName(String userName) {
((RevolutAccountPayload) paymentAccountPayload).setAccountId(accountId); ((RevolutAccountPayload) paymentAccountPayload).setUserName(userName);
} }
public String getAccountId() { public String getUserName() {
return ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); return ((RevolutAccountPayload) paymentAccountPayload).getUserName();
}
public boolean userNameNotSet() {
return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet();
} }
} }

View File

@ -19,27 +19,37 @@ package bisq.core.payment.payload;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.common.proto.ProtoUtil;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString @ToString
@Setter
@Getter
@Slf4j @Slf4j
public final class RevolutAccountPayload extends PaymentAccountPayload { 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 = ""; 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) { public RevolutAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id); super(paymentMethod, id);
} }
@ -52,6 +62,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
private RevolutAccountPayload(String paymentMethod, private RevolutAccountPayload(String paymentMethod,
String id, String id,
String accountId, String accountId,
@Nullable String userName,
long maxTradePeriod, long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) { Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod, super(paymentMethod,
@ -60,20 +71,24 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
excludeFromJsonDataMap); excludeFromJsonDataMap);
this.accountId = accountId; this.accountId = accountId;
this.userName = userName;
} }
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder() protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder()
.setRevolutAccountPayload(protobuf.RevolutAccountPayload.newBuilder() .setAccountId(accountId);
.setAccountId(accountId)) Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName);
.build(); return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build();
} }
public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload();
return new RevolutAccountPayload(proto.getPaymentMethodId(), return new RevolutAccountPayload(proto.getPaymentMethodId(),
proto.getId(), proto.getId(),
proto.getRevolutAccountPayload().getAccountId(), revolutAccountPayload.getAccountId(),
ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()),
proto.getMaxTradePeriod(), proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap())); new HashMap<>(proto.getExcludeFromJsonDataMap()));
} }
@ -85,7 +100,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
@Override @Override
public String getPaymentDetails() { public String getPaymentDetails() {
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + getUserName();
} }
@Override @Override
@ -95,6 +110,24 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
@Override @Override
public byte[] getAgeWitnessInputData() { public byte[] getAgeWitnessInputData() {
// getAgeWitnessInputData is called at new account creation when accountId is empty string.
return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); 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;
}
} }

View File

@ -84,7 +84,6 @@ public class PriceFeedService {
private final StringProperty currencyCodeProperty = new SimpleStringProperty(); private final StringProperty currencyCodeProperty = new SimpleStringProperty();
private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); private final IntegerProperty updateCounter = new SimpleIntegerProperty(0);
private long epochInMillisAtLastRequest; private long epochInMillisAtLastRequest;
private Map<String, Long> timeStampMap = new HashMap<>();
private long retryDelay = 1; private long retryDelay = 1;
private long requestTs; private long requestTs;
@Nullable @Nullable
@ -126,6 +125,10 @@ public class PriceFeedService {
request(false); request(false);
} }
public boolean hasPrices() {
return !cache.isEmpty();
}
public void requestPriceFeed(Consumer<Double> resultHandler, FaultHandler faultHandler) { public void requestPriceFeed(Consumer<Double> resultHandler, FaultHandler faultHandler) {
this.priceConsumer = resultHandler; this.priceConsumer = resultHandler;
this.faultHandler = faultHandler; 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. // At applyPriceToConsumer we also check if price is not exceeding max. age for price data.
boolean success = applyPriceToConsumer(); boolean success = applyPriceToConsumer();
if (success) { if (success) {
final MarketPrice marketPrice = cache.get(currencyCode); MarketPrice marketPrice = cache.get(currencyCode);
if (marketPrice != null) if (marketPrice != null)
log.debug("Received new {} from provider {} after {} sec.", log.debug("Received new {} from provider {} after {} sec.",
marketPrice, marketPrice,
@ -326,7 +329,7 @@ public class PriceFeedService {
boolean result = false; boolean result = false;
String errorMessage = null; String errorMessage = null;
if (currencyCode != null) { if (currencyCode != null) {
final String baseUrl = priceProvider.getBaseUrl(); String baseUrl = priceProvider.getBaseUrl();
if (cache.containsKey(currencyCode)) { if (cache.containsKey(currencyCode)) {
try { try {
MarketPrice marketPrice = cache.get(currencyCode); MarketPrice marketPrice = cache.get(currencyCode);
@ -383,14 +386,12 @@ public class PriceFeedService {
public void onSuccess(@Nullable Tuple2<Map<String, Long>, Map<String, MarketPrice>> result) { public void onSuccess(@Nullable Tuple2<Map<String, Long>, Map<String, MarketPrice>> result) {
UserThread.execute(() -> { UserThread.execute(() -> {
checkNotNull(result, "Result must not be null at requestAllPrices"); checkNotNull(result, "Result must not be null at requestAllPrices");
timeStampMap = result.first;
// Each currency rate has a different timestamp, depending on when // Each currency rate has a different timestamp, depending on when
// the pricenode aggregate rate was calculated // the pricenode aggregate rate was calculated
// However, the request timestamp is when the pricenode was queried // However, the request timestamp is when the pricenode was queried
epochInMillisAtLastRequest = System.currentTimeMillis(); epochInMillisAtLastRequest = System.currentTimeMillis();
final Map<String, MarketPrice> priceMap = result.second; Map<String, MarketPrice> priceMap = result.second;
cache.putAll(priceMap); cache.putAll(priceMap);

View File

@ -70,7 +70,24 @@ public class PriceProvider extends HttpClientProvider {
final double price = (Double) treeMap.get("price"); final double price = (Double) treeMap.get("price");
// json uses double for our timestampSec long value... // json uses double for our timestampSec long value...
final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); 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) { } catch (Throwable t) {
log.error(t.toString()); log.error(t.toString());
t.printStackTrace(); t.printStackTrace();
@ -80,6 +97,13 @@ public class PriceProvider extends HttpClientProvider {
return new Tuple2<>(tsMap, marketPriceMap); return new Tuple2<>(tsMap, marketPriceMap);
} }
private void addPrice(Map<String, MarketPrice> marketPriceMap,
String currencyCode,
double price,
long timestampSec) {
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
}
public String getBaseUrl() { public String getBaseUrl() {
return httpClient.getBaseUrl(); return httpClient.getBaseUrl();
} }

View File

@ -62,10 +62,14 @@ public abstract class SupportManager {
// We get first the message handler called then the onBootstrapped // We get first the message handler called then the onBootstrapped
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
// already stored
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
tryApplyMessages(); tryApplyMessages();
}); });
p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
// As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
// already stored
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
tryApplyMessages(); tryApplyMessages();
}); });

View File

@ -19,9 +19,16 @@ package bisq.core.support.dispute;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; 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.offer.OpenOfferManager;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportManager; import bisq.core.support.SupportManager;
import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.DisputeResultMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage; 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.P2PService;
import bisq.network.p2p.SendMailboxMessageListener; import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.FaultHandler; import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.common.storage.Storage; import bisq.common.storage.Storage;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -51,6 +63,7 @@ import javafx.collections.ObservableList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -68,6 +81,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
protected final OpenOfferManager openOfferManager; protected final OpenOfferManager openOfferManager;
protected final PubKeyRing pubKeyRing; protected final PubKeyRing pubKeyRing;
protected final DisputeListService<T> disputeListService; protected final DisputeListService<T> disputeListService;
private final PriceFeedService priceFeedService;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -82,7 +96,8 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
DisputeListService<T> disputeListService) { DisputeListService<T> disputeListService,
PriceFeedService priceFeedService) {
super(p2PService, walletsSetup); super(p2PService, walletsSetup);
this.tradeWalletService = tradeWalletService; this.tradeWalletService = tradeWalletService;
@ -92,6 +107,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.pubKeyRing = pubKeyRing; this.pubKeyRing = pubKeyRing;
this.disputeListService = disputeListService; this.disputeListService = disputeListService;
this.priceFeedService = priceFeedService;
} }
@ -255,19 +271,20 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
String errorMessage = null; String errorMessage = null;
Dispute dispute = openNewDisputeMessage.getDispute(); Dispute dispute = openNewDisputeMessage.getDispute();
dispute.setStorage(disputeListService.getStorage());
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before // Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
dispute.setSupportType(openNewDisputeMessage.getSupportType()); dispute.setSupportType(openNewDisputeMessage.getSupportType());
dispute.setStorage(disputeListService.getStorage()); Contract contract = dispute.getContract();
Contract contractFromOpener = dispute.getContract(); addPriceInfoMessage(dispute, 0);
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
if (isAgent(dispute)) { if (isAgent(dispute)) {
if (!disputeList.contains(dispute)) { if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) { if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute); disputeList.add(dispute);
errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing); sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing);
} else { } else {
// valid case if both have opened a dispute and agent was not online. // 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 = {}", log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
@ -286,23 +303,11 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
ObservableList<ChatMessage> messages = dispute.getChatMessages(); ObservableList<ChatMessage> messages = dispute.getChatMessages();
if (!messages.isEmpty()) { if (!messages.isEmpty()) {
ChatMessage chatMessage = messages.get(0); 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); sendAckMessage(chatMessage, sendersPubKeyRing, errorMessage == null, errorMessage);
} }
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. addMediationResultMessage(dispute);
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);
}
} }
// not dispute requester receives that from dispute agent // not dispute requester receives that from dispute agent
@ -468,14 +473,27 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
} }
} }
// dispute agent sends that to trading peer when he received openDispute request // Dispute agent sends that to trading peer when he received openDispute request
private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
Contract contractFromOpener,
PubKeyRing pubKeyRing) {
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
// message and not skip the system message of the peer as it would be the case if we have created the system msg
// from the code below.
UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener,
contractFromOpener,
pubKeyRing),
100, TimeUnit.MILLISECONDS);
}
private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
Contract contractFromOpener, Contract contractFromOpener,
PubKeyRing pubKeyRing) { PubKeyRing pubKeyRing) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");
return null; return;
} }
Dispute dispute = new Dispute(disputeListService.getStorage(), Dispute dispute = new Dispute(disputeListService.getStorage(),
@ -500,91 +518,94 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> 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! // Valid case if both have opened a dispute and agent was not online.
Contract contract = dispute.getContract(); if (storedDisputeOptional.isPresent()) {
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); log.info("We got a dispute already open for that trade and trading peer. TradeId = {}",
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 = {}",
dispute.getTradeId()); 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 // dispute agent send result to trader
@ -731,4 +752,112 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
.filter(e -> e.getTradeId().equals(tradeId)) .filter(e -> e.getTradeId().equals(tradeId))
.findAny(); .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;
}
}
} }

View File

@ -28,6 +28,7 @@ import bisq.core.btc.wallet.WalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
@ -88,9 +89,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
ArbitrationDisputeListService arbitrationDisputeListService) { ArbitrationDisputeListService arbitrationDisputeListService,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, arbitrationDisputeListService); openOfferManager, pubKeyRing, arbitrationDisputeListService, priceFeedService);
} }
@ -163,6 +165,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
} }
@Override
protected void addPriceInfoMessage(Dispute dispute, int counter) {
// Arbitrator is not used anymore.
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Message handler // Message handler
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,6 +23,7 @@ import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
@ -80,9 +81,10 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
MediationDisputeListService mediationDisputeListService) { MediationDisputeListService mediationDisputeListService,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, mediationDisputeListService); openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,6 +23,7 @@ import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
@ -74,9 +75,10 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
RefundDisputeListService refundDisputeListService) { RefundDisputeListService refundDisputeListService,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, refundDisputeListService); openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -140,6 +142,14 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); 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 // Message handler
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -26,6 +26,8 @@ import bisq.common.app.DevEnv;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.common.annotations.VisibleForTesting;
import java.util.Date; import java.util.Date;
import lombok.Value; import lombok.Value;
@ -51,7 +53,7 @@ public class XmrTxProofModel {
private final int confirmsRequired; private final int confirmsRequired;
private final String serviceAddress; private final String serviceAddress;
public XmrTxProofModel(Trade trade, String serviceAddress, int confirmsRequired) { XmrTxProofModel(Trade trade, String serviceAddress, int confirmsRequired) {
this.serviceAddress = serviceAddress; this.serviceAddress = serviceAddress;
this.confirmsRequired = confirmsRequired; this.confirmsRequired = confirmsRequired;
Coin tradeAmount = trade.getTradeAmount(); Coin tradeAmount = trade.getTradeAmount();
@ -68,4 +70,25 @@ public class XmrTxProofModel {
tradeDate = trade.getDate(); tradeDate = trade.getDate();
tradeId = trade.getId(); 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;
}
} }

View File

@ -59,7 +59,7 @@ public class ApplyFilter extends TradeTask {
} else if (filterManager.isPaymentMethodBanned(trade.getOffer().getPaymentMethod())) { } else if (filterManager.isPaymentMethodBanned(trade.getOffer().getPaymentMethod())) {
failed("Payment method is banned.\n" + failed("Payment method is banned.\n" +
"Payment method=" + trade.getOffer().getPaymentMethod().getId()); "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" + failed("Other trader is banned by their trading account data.\n" +
"paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails() + "\n" + "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails() + "\n" +
"banFilter=" + appliedPaymentAccountFilter[0].toString()); "banFilter=" + appliedPaymentAccountFilter[0].toString());

View File

@ -24,6 +24,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.market.MarketAlertFilter;
import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.dispute.refund.refundagent.RefundAgent;
@ -50,6 +51,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -126,6 +128,16 @@ public class User implements PersistedDataHost {
// API // API
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized(@Nullable Consumer<List<RevolutAccount>> resultHandler) {
if (resultHandler != null) {
resultHandler.accept(paymentAccountsAsObservable.stream()
.filter(paymentAccount -> paymentAccount instanceof RevolutAccount)
.map(paymentAccount -> (RevolutAccount) paymentAccount)
.filter(RevolutAccount::userNameNotSet)
.collect(Collectors.toList()));
}
}
@Nullable @Nullable
public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) {
final List<Arbitrator> acceptedArbitrators = userPayload.getAcceptedArbitrators(); final List<Arbitrator> acceptedArbitrators = userPayload.getAcceptedArbitrators();

View File

@ -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.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.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.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} 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.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.contribute=Contribute
setting.about.providers=Data providers 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.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.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices.
setting.about.pricesProvided=Market prices provided by setting.about.pricesProvided=Market prices provided by
setting.about.feeEstimation.label=Mining fee estimation provided by setting.about.feeEstimation.label=Mining fee estimation provided by
setting.about.versionDetails=Version details 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.seller=Seller's payout amount
disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.payoutAmount.invert=Use loser as publisher
disputeSummaryWindow.reason=Reason of dispute disputeSummaryWindow.reason=Reason of dispute
disputeSummaryWindow.reason.bug=Bug
disputeSummaryWindow.reason.usability=Usability # dynamic values are not recognized by IntelliJ
disputeSummaryWindow.reason.protocolViolation=Protocol violation # suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.noReply=No reply disputeSummaryWindow.reason.BUG=Bug
disputeSummaryWindow.reason.scam=Scam # suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.other=Other disputeSummaryWindow.reason.USABILITY=Usability
disputeSummaryWindow.reason.bank=Bank # suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.optionTrade=Option trade disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protocol violation
disputeSummaryWindow.reason.sellerNotResponding=Seller not responding # suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.wrongSenderAccount=Wrong sender account disputeSummaryWindow.reason.NO_REPLY=No reply
disputeSummaryWindow.reason.peerWasLate=Peer was late # suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.tradeAlreadySettled=Trade already settled 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.summaryNotes=Summary notes
disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes
@ -2418,7 +2432,8 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\
Summary:\n\ Summary:\n\
Payout amount for BTC buyer: {1}\n\ Payout amount for BTC buyer: {1}\n\
Payout amount for BTC seller: {2}\n\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\ disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\
Open trade and accept or reject suggestion from mediator Open trade and accept or reject suggestion from mediator
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\n\ 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.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value]
filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedCurrencies=Filtered currency codes (comma sep.)
filterWindow.bannedPaymentMethods=Filtered payment method IDs (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.arbitrators=Filtered arbitrators (comma sep. onion addresses)
filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.mediators=Filtered mediators (comma sep. onion addresses)
filterWindow.refundAgents=Filtered refund agents (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=Account
payment.account.no=Account no. payment.account.no=Account no.
payment.account.name=Account name payment.account.name=Account name
payment.account.userName=User name
payment.account.owner=Account owner full name payment.account.owner=Account owner full name
payment.account.fullName=Full name (first, middle, last) payment.account.fullName=Full name (first, middle, last)
payment.account.state=State/Province/Region payment.account.state=State/Province/Region
@ -3048,8 +3065,6 @@ payment.cashApp.cashTag=$Cashtag
payment.moneyBeam.accountId=Email or phone no. payment.moneyBeam.accountId=Email or phone no.
payment.venmo.venmoUserName=Venmo username payment.venmo.venmoUserName=Venmo username
payment.popmoney.accountId=Email or phone no. 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.promptPay.promptPayId=Citizen ID/Tax ID or phone no.
payment.supportedCurrencies=Supported currencies payment.supportedCurrencies=Supported currencies
payment.limitations=Limitations 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. \ 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. 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 \ 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.
otherwise the BTC buyer cannot send you the funds. 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\ payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\
\n\ \n\

View File

@ -397,14 +397,14 @@ public class SignedWitnessServiceTest {
signedWitnessService.addToMap(sw3); signedWitnessService.addToMap(sw3);
// Second account is banned, first account is still a signer but the other two are no longer signers // 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)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3));
// First account is banned, no accounts in the tree below it are signers // First account is banned, no accounts in the tree below it are signers
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true);
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false);
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3));
@ -434,14 +434,14 @@ public class SignedWitnessServiceTest {
signedWitnessService.addToMap(sw3); signedWitnessService.addToMap(sw3);
// Only second account is banned, first account is still a signer but the other two are no longer signers // 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)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3));
// Only first account is banned, account2 and account3 are still signers // Only first account is banned, account2 and account3 are still signers
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true);
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false);
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3));
@ -484,21 +484,21 @@ public class SignedWitnessServiceTest {
signedWitnessService.addToMap(sw3p); signedWitnessService.addToMap(sw3p);
// First account is banned, the other two are still signers // 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)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3));
// Second account is banned, the other two are still signers // Second account is banned, the other two are still signers
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false);
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true);
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3));
// First and second account is banned, the third is no longer a signer // First and second account is banned, the third is no longer a signer
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true);
when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true);
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2));
assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3));

View File

@ -218,8 +218,8 @@ public class AccountAgeWitnessServiceTest {
when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isNodeAddressBanned(any())).thenReturn(false);
when(filterManager.isCurrencyBanned(any())).thenReturn(false); when(filterManager.isCurrencyBanned(any())).thenReturn(false);
when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); when(filterManager.isPaymentMethodBanned(any())).thenReturn(false);
when(filterManager.isPeersPaymentAccountDataAreBanned(any(), any())).thenReturn(false); when(filterManager.arePeersPaymentAccountDataBanned(any(), any())).thenReturn(false);
when(filterManager.isSignerPubKeyBanned(any())).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false);
when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true);

View File

@ -27,14 +27,14 @@ public class PriceTest {
Price result = Price.parse("USD", "0.1"); Price result = Price.parse("USD", "0.1");
Assert.assertEquals( Assert.assertEquals(
"Fiat value should be formatted with two decimals.", "Fiat value should be formatted with two decimals.",
"0.10 USD", "0.10 BTC/USD",
result.toFriendlyString() result.toFriendlyString()
); );
result = Price.parse("EUR", "0.1234"); result = Price.parse("EUR", "0.1234");
Assert.assertEquals( Assert.assertEquals(
"Fiat value should be given two decimals", "Fiat value should be given two decimals",
"0.1234 EUR", "0.1234 BTC/EUR",
result.toFriendlyString() result.toFriendlyString()
); );
@ -57,19 +57,19 @@ public class PriceTest {
Assert.assertEquals( Assert.assertEquals(
"Comma (',') as decimal separator should be converted to period ('.')", "Comma (',') as decimal separator should be converted to period ('.')",
"0.0001 USD", "0.0001 BTC/USD",
Price.parse("USD", "0,0001").toFriendlyString() Price.parse("USD", "0,0001").toFriendlyString()
); );
Assert.assertEquals( Assert.assertEquals(
"Too many decimals should get rounded up properly.", "Too many decimals should get rounded up properly.",
"10000.2346 LTC", "10000.2346 LTC/BTC",
Price.parse("LTC", "10000,23456789").toFriendlyString() Price.parse("LTC", "10000,23456789").toFriendlyString()
); );
Assert.assertEquals( Assert.assertEquals(
"Too many decimals should get rounded down properly.", "Too many decimals should get rounded down properly.",
"10000.2345 LTC", "10000.2345 LTC/BTC",
Price.parse("LTC", "10000,23454999").toFriendlyString() Price.parse("LTC", "10000,23454999").toFriendlyString()
); );
@ -95,14 +95,14 @@ public class PriceTest {
Price result = Price.valueOf("USD", 1); Price result = Price.valueOf("USD", 1);
Assert.assertEquals( Assert.assertEquals(
"Fiat value should have four decimals.", "Fiat value should have four decimals.",
"0.0001 USD", "0.0001 BTC/USD",
result.toFriendlyString() result.toFriendlyString()
); );
result = Price.valueOf("EUR", 1234); result = Price.valueOf("EUR", 1234);
Assert.assertEquals( Assert.assertEquals(
"Fiat value should be given two decimals", "Fiat value should be given two decimals",
"0.1234 EUR", "0.1234 BTC/EUR",
result.toFriendlyString() result.toFriendlyString()
); );
@ -114,13 +114,13 @@ public class PriceTest {
Assert.assertEquals( Assert.assertEquals(
"Too many decimals should get rounded up properly.", "Too many decimals should get rounded up properly.",
"10000.2346 LTC", "10000.2346 LTC/BTC",
Price.valueOf("LTC", 1000023456789L).toFriendlyString() Price.valueOf("LTC", 1000023456789L).toFriendlyString()
); );
Assert.assertEquals( Assert.assertEquals(
"Too many decimals should get rounded down properly.", "Too many decimals should get rounded down properly.",
"10000.2345 LTC", "10000.2345 LTC/BTC",
Price.valueOf("LTC", 1000023454999L).toFriendlyString() Price.valueOf("LTC", 1000023454999L).toFriendlyString()
); );

View File

@ -51,23 +51,23 @@ public class XmrTxProofParserTest {
public void testJsonTopLevel() { public void testJsonTopLevel() {
// testing the top level fields: data and status // testing the top level fields: data and status
assertTrue(XmrTxProofParser.parse(xmrTxProofModel, assertTrue(XmrTxProofParser.parse(xmrTxProofModel,
"{'data':{'title':''},'status':'fail'}" ) "{'data':{'title':''},'status':'fail'}")
.getDetail() == XmrTxProofRequest.Detail.TX_NOT_FOUND); .getDetail() == XmrTxProofRequest.Detail.TX_NOT_FOUND);
assertTrue(XmrTxProofParser.parse(xmrTxProofModel, assertTrue(XmrTxProofParser.parse(xmrTxProofModel,
"{'data':{'title':''},'missingstatus':'success'}" ) "{'data':{'title':''},'missingstatus':'success'}")
.getDetail() == XmrTxProofRequest.Detail.API_INVALID); .getDetail() == XmrTxProofRequest.Detail.API_INVALID);
assertTrue(XmrTxProofParser.parse(xmrTxProofModel, assertTrue(XmrTxProofParser.parse(xmrTxProofModel,
"{'missingdata':{'title':''},'status':'success'}" ) "{'missingdata':{'title':''},'status':'success'}")
.getDetail() == XmrTxProofRequest.Detail.API_INVALID); .getDetail() == XmrTxProofRequest.Detail.API_INVALID);
} }
@Test @Test
public void testJsonAddress() { public void testJsonAddress() {
assertTrue(XmrTxProofParser.parse(xmrTxProofModel, assertTrue(XmrTxProofParser.parse(xmrTxProofModel,
"{'data':{'missingaddress':'irrelevant'},'status':'success'}" ) "{'data':{'missingaddress':'irrelevant'},'status':'success'}")
.getDetail() == XmrTxProofRequest.Detail.API_INVALID); .getDetail() == XmrTxProofRequest.Detail.API_INVALID);
assertTrue(XmrTxProofParser.parse(xmrTxProofModel, assertTrue(XmrTxProofParser.parse(xmrTxProofModel,
"{'data':{'address':'e957dac7'},'status':'success'}" ) "{'data':{'address':'e957dac7'},'status':'success'}")
.getDetail() == XmrTxProofRequest.Detail.ADDRESS_INVALID); .getDetail() == XmrTxProofRequest.Detail.ADDRESS_INVALID);
} }

View File

@ -53,14 +53,18 @@ public class UserPayloadModelVOTest {
false, false,
null, null,
null, null,
"string", Lists.newArrayList(),
new byte[]{10, 0, 0}, Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
null,
0,
null,
null,
null,
null, null,
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
false)); false));
vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock());
vo.setRegisteredMediator(MediatorTest.getMediatorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock());
vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock()));

View File

@ -22,6 +22,7 @@ import bisq.core.dao.governance.param.Param;
import bisq.core.filter.Filter; import bisq.core.filter.Filter;
import bisq.core.filter.FilterManager; import bisq.core.filter.FilterManager;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import java.util.HashMap; import java.util.HashMap;
@ -98,10 +99,29 @@ public class FeeReceiverSelectorTest {
} }
private static Filter filterWithReceivers(List<String> btcFeeReceiverAddresses) { private static Filter filterWithReceivers(List<String> btcFeeReceiverAddresses) {
return new Filter(null, null, null, null, return new Filter(Lists.newArrayList(),
null, null, null, null, Lists.newArrayList(),
false, null, false, null, Lists.newArrayList(),
null, null, null, null, Lists.newArrayList(),
btcFeeReceiverAddresses, false); 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);
} }
} }

View File

@ -23,8 +23,6 @@ import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.RevolutValidator; import bisq.desktop.util.validation.RevolutValidator;
import bisq.core.account.witness.AccountAgeWitnessService; 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.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.payment.PaymentAccount; 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.coin.CoinFormatter;
import bisq.core.util.validation.InputValidator; import bisq.core.util.validation.InputValidator;
import com.jfoenix.controls.JFXComboBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; 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.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane;
import static bisq.desktop.util.FormBuilder.addTopLabelTextField; import static bisq.desktop.util.FormBuilder.addTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
public class RevolutForm extends PaymentMethodForm { public class RevolutForm extends PaymentMethodForm {
private final RevolutAccount account; private final RevolutAccount account;
private RevolutValidator validator; private RevolutValidator validator;
private InputTextField accountIdInputTextField; private InputTextField userNameInputTextField;
private Country selectedCountry;
public static int addFormForBuyer(GridPane gridPane, int gridRow, public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) { PaymentAccountPayload paymentAccountPayload) {
String accountId = ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, getTitle(accountId), accountId); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName);
return gridRow; 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, public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService,
RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane, RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter) { int gridRow, CoinFormatter formatter) {
@ -86,63 +66,16 @@ public class RevolutForm extends PaymentMethodForm {
public void addFormForAddAccount() { public void addFormForAddAccount() {
gridRowFrom = gridRow + 1; gridRowFrom = gridRow + 1;
// country selection is added only to prevent anymore email id input and userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName"));
// solely to validate the given phone number userNameInputTextField.setValidator(validator);
ComboBox<Country> countryComboBox = addCountrySelection(); userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
setCountryComboBoxAction(countryComboBox); account.setUserName(newValue.trim());
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());
updateFromInputs(); updateFromInputs();
}); });
addCurrenciesGrid(true); addCurrenciesGrid(true);
addLimitations(false); addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton(); addAccountNameTextFieldWithAutoFillToggleButton();
//set default country as selected
selectedCountry = CountryUtil.getDefaultCountry();
if (CountryUtil.getAllRevolutCountries().contains(selectedCountry)) {
countryComboBox.getSelectionModel().select(selectedCountry);
}
}
ComboBox<Country> addCountrySelection() {
HBox hBox = new HBox();
hBox.setSpacing(5);
ComboBox<Country> 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<Country> countryComboBox) {
countryComboBox.setOnAction(e -> {
selectedCountry = countryComboBox.getSelectionModel().getSelectedItem();
updateFromInputs();
accountIdInputTextField.resetValidation();
accountIdInputTextField.validate();
accountIdInputTextField.requestFocus();
countryComboBox.requestFocus();
});
} }
private void addCurrenciesGrid(boolean isEditable) { private void addCurrenciesGrid(boolean isEditable) {
@ -161,18 +94,18 @@ public class RevolutForm extends PaymentMethodForm {
@Override @Override
protected void autoFillNameTextField() { protected void autoFillNameTextField() {
setAccountNameWithString(accountIdInputTextField.getText()); setAccountNameWithString(userNameInputTextField.getText());
} }
@Override @Override
public void addFormForDisplayAccount() { public void addFormForDisplayAccount() {
gridRowFrom = gridRow; 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); account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(account.getPaymentMethod().getId())); Res.get(account.getPaymentMethod().getId()));
String accountId = account.getAccountId(); String userName = account.getUserName();
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, getTitle(accountId), accountId).second; TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second;
field.setMouseTransparent(false); field.setMouseTransparent(false);
addLimitations(true); addLimitations(true);
addCurrenciesGrid(false); addCurrenciesGrid(false);
@ -181,7 +114,7 @@ public class RevolutForm extends PaymentMethodForm {
@Override @Override
public void updateAllInputsValid() { public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid() allInputsValid.set(isAccountNameValid()
&& validator.validate(account.getAccountId(), selectedCountry.code).isValid && validator.validate(account.getUserName()).isValid
&& account.getTradeCurrencies().size() > 0); && account.getTradeCurrencies().size() > 0);
} }
} }

View File

@ -28,12 +28,13 @@ import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow;
import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow; import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow;
import bisq.desktop.main.overlays.windows.TacWindow; import bisq.desktop.main.overlays.windows.TacWindow;
import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; 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.WalletPasswordWindow;
import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow;
import bisq.desktop.main.presentation.AccountPresentation; import bisq.desktop.main.presentation.AccountPresentation;
import bisq.desktop.main.presentation.SettingsPresentation;
import bisq.desktop.main.presentation.DaoPresentation; import bisq.desktop.main.presentation.DaoPresentation;
import bisq.desktop.main.presentation.MarketPricePresentation; import bisq.desktop.main.presentation.MarketPricePresentation;
import bisq.desktop.main.presentation.SettingsPresentation;
import bisq.desktop.main.shared.PriceFeedComboBoxItem; import bisq.desktop.main.shared.PriceFeedComboBoxItem;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil; import bisq.desktop.util.GUIUtil;
@ -50,6 +51,7 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.payment.AliPayAccount; import bisq.core.payment.AliPayAccount;
import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.CryptoCurrencyAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.presentation.BalancePresentation; import bisq.core.presentation.BalancePresentation;
import bisq.core.presentation.SupportTicketsPresentation; import bisq.core.presentation.SupportTicketsPresentation;
import bisq.core.presentation.TradePresentation; import bisq.core.presentation.TradePresentation;
@ -88,12 +90,15 @@ import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.PriorityQueue; import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -306,10 +311,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
.useReportBugButton() .useReportBugButton()
.show())); .show()));
bisqSetup.setDisplayTorNetworkSettingsHandler(show -> { bisqSetup.setDisplayTorNetworkSettingsHandler(show -> {
if (show) if (show) {
torNetworkSettingsWindow.show(); torNetworkSettingsWindow.show();
else } else if (torNetworkSettingsWindow.isDisplayed()) {
torNetworkSettingsWindow.hide(); torNetworkSettingsWindow.hide();
}
}); });
bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg)
.actionButtonText(Res.get("settings.net.reSyncSPVChainButton")) .actionButtonText(Res.get("settings.net.reSyncSPVChainButton"))
@ -380,6 +386,12 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); 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() corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> new Popup()
.warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir))
.useShutDownButton() .useShutDownButton()
@ -409,6 +421,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show());
} }
private void showRevolutAccountUpdateWindow(List<RevolutAccount> 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() { private void setupP2PNumPeersWatcher() {
p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> {
int numPeers = (int) newValue; int numPeers = (int) newValue;

View File

@ -50,7 +50,6 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
@ -127,14 +126,12 @@ class OfferBookChartViewModel extends ActivatableViewModel {
fillTradeCurrencies(); fillTradeCurrencies();
}; };
currenciesUpdatedListener = new ChangeListener<>() { currenciesUpdatedListener = (observable, oldValue, newValue) -> {
@Override if (!isAnyPriceAbsent()) {
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { offerBook.fillOfferBookListItems();
if (!isAnyPricePresent()) { updateChartData();
offerBook.fillOfferBookListItems(); var self = this;
updateChartData(); priceFeedService.updateCounterProperty().removeListener(self.currenciesUpdatedListener);
priceFeedService.updateCounterProperty().removeListener(currenciesUpdatedListener);
}
} }
}; };
@ -163,7 +160,7 @@ class OfferBookChartViewModel extends ActivatableViewModel {
fillTradeCurrencies(); fillTradeCurrencies();
updateChartData(); updateChartData();
if (isAnyPricePresent()) if (isAnyPriceAbsent())
priceFeedService.updateCounterProperty().addListener(currenciesUpdatedListener); priceFeedService.updateCounterProperty().addListener(currenciesUpdatedListener);
syncPriceFeedCurrency(); syncPriceFeedCurrency();
@ -271,7 +268,7 @@ class OfferBookChartViewModel extends ActivatableViewModel {
priceFeedService.setCurrencyCode(getCurrencyCode()); priceFeedService.setCurrencyCode(getCurrencyCode());
} }
private boolean isAnyPricePresent() { private boolean isAnyPriceAbsent() {
return offerBookListItems.stream().anyMatch(item -> item.getOffer().getPrice() == null); return offerBookListItems.stream().anyMatch(item -> item.getOffer().getPrice() == null);
} }
@ -291,11 +288,11 @@ class OfferBookChartViewModel extends ActivatableViewModel {
Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed();
var buyOfferSortComparator = var buyOfferSortComparator =
offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low.
.thenComparing(offerAmountComparator); .thenComparing(offerAmountComparator);
var sellOfferSortComparator = var sellOfferSortComparator =
offerPriceComparator offerPriceComparator
.thenComparing(offerAmountComparator); .thenComparing(offerAmountComparator);
List<Offer> allBuyOffers = offerBookListItems.stream() List<Offer> allBuyOffers = offerBookListItems.stream()
.map(OfferBookListItem::getOffer) .map(OfferBookListItem::getOffer)

View File

@ -71,7 +71,6 @@ import javax.inject.Named;
import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.GlyphIcons;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas; import javafx.scene.canvas.Canvas;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
@ -731,6 +730,12 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
return column; return column;
} }
private ObservableValue<OfferBookListItem> asPriceDependentObservable(OfferBookListItem item) {
return item.getOffer().isUseMarketBasedPrice()
? EasyBind.map(model.priceFeedService.updateCounterProperty(), n -> item)
: new ReadOnlyObjectWrapper<>(item);
}
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getPriceColumn() { private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getPriceColumn() {
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>("") { AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>("") {
{ {
@ -738,59 +743,20 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
} }
}; };
column.getStyleClass().add("number-column"); column.getStyleClass().add("number-column");
column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue()));
column.setCellFactory( column.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
public TableCell<OfferBookListItem, OfferBookListItem> call( public TableCell<OfferBookListItem, OfferBookListItem> call(
TableColumn<OfferBookListItem, OfferBookListItem> column) { TableColumn<OfferBookListItem, OfferBookListItem> column) {
return new TableCell<>() { return new TableCell<>() {
private OfferBookListItem offerBookListItem;
private ChangeListener<Number> priceChangedListener;
ChangeListener<Scene> sceneChangeListener;
@Override @Override
public void updateItem(final OfferBookListItem item, boolean empty) { public void updateItem(final OfferBookListItem item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) { if (item != null && !empty) {
if (getTableView().getScene() != null && sceneChangeListener == null) { setGraphic(getPriceLabel(model.getPrice(item), item));
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));
} else { } 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); setGraphic(null);
} }
} }
@ -845,35 +811,19 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
} }
}; };
column.getStyleClass().add("number-column"); column.getStyleClass().add("number-column");
column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue()));
column.setCellFactory( column.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
public TableCell<OfferBookListItem, OfferBookListItem> call( public TableCell<OfferBookListItem, OfferBookListItem> call(
TableColumn<OfferBookListItem, OfferBookListItem> column) { TableColumn<OfferBookListItem, OfferBookListItem> column) {
return new TableCell<>() { return new TableCell<>() {
private OfferBookListItem offerBookListItem;
final ChangeListener<Number> listener = new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends Number> 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 @Override
public void updateItem(final OfferBookListItem item, boolean empty) { public void updateItem(final OfferBookListItem item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) { if (item != null && !empty) {
if (item.getOffer().getPrice() == null) { if (item.getOffer().getPrice() == null) {
this.offerBookListItem = item;
model.priceFeedService.updateCounterProperty().addListener(listener);
setText(Res.get("shared.na")); setText(Res.get("shared.na"));
setGraphic(null); setGraphic(null);
} else { } else {
@ -882,8 +832,6 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
model.getNumberOfDecimalsForVolume(item))); model.getNumberOfDecimalsForVolume(item)));
} }
} else { } else {
model.priceFeedService.updateCounterProperty().removeListener(listener);
this.offerBookListItem = null;
setText(""); setText("");
setGraphic(null); setGraphic(null);
} }
@ -1015,7 +963,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
public void updateItem(final OfferBookListItem newItem, boolean empty) { public void updateItem(final OfferBookListItem newItem, boolean empty) {
super.updateItem(newItem, empty); super.updateItem(newItem, empty);
TableRow tableRow = getTableRow(); TableRow<OfferBookListItem> tableRow = getTableRow();
if (newItem != null && !empty) { if (newItem != null && !empty) {
final Offer offer = newItem.getOffer(); final Offer offer = newItem.getOffer();
boolean myOffer = model.isMyOffer(offer); boolean myOffer = model.isMyOffer(offer);

View File

@ -486,18 +486,18 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
private void addReasonControls() { private void addReasonControls() {
reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bug")); reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BUG.name()));
reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.usability")); reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.USABILITY.name()));
reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.protocolViolation")); reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PROTOCOL_VIOLATION.name()));
reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.noReply")); reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.NO_REPLY.name()));
reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.scam")); reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SCAM.name()));
reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bank")); reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BANK_PROBLEMS.name()));
reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.other")); reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OTHER.name()));
reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.optionTrade")); reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OPTION_TRADE.name()));
reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.sellerNotResponding")); reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SELLER_NOT_RESPONDING.name()));
reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.wrongSenderAccount")); reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.WRONG_SENDER_ACCOUNT.name()));
reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.peerWasLate")); reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PEER_WAS_LATE.name()));
reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.tradeAlreadySettled")); reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.TRADE_ALREADY_SETTLED.name()));
HBox feeRadioButtonPane = new HBox(); HBox feeRadioButtonPane = new HBox();
feeRadioButtonPane.setSpacing(20); feeRadioButtonPane.setSpacing(20);
@ -745,12 +745,20 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);
dispute.setIsClosed(true); dispute.setIsClosed(true);
DisputeResult.Reason reason = disputeResult.getReason();
String text = Res.get("disputeSummaryWindow.close.msg", String text = Res.get("disputeSummaryWindow.close.msg",
DisplayUtils.formatDateTime(disputeResult.getCloseDate()), DisplayUtils.formatDateTime(disputeResult.getCloseDate()),
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()),
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()),
Res.get("disputeSummaryWindow.reason." + reason.name()),
disputeResult.summaryNotesProperty().get()); 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) { if (dispute.getSupportType() == SupportType.MEDIATION) {
text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); text += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
} else if (dispute.getSupportType() == SupportType.REFUND) { } else if (dispute.getSupportType() == SupportType.REFUND) {

View File

@ -79,7 +79,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
if (headLine == null) if (headLine == null)
headLine = Res.get("filterWindow.headline"); headLine = Res.get("filterWindow.headline");
width = 968; width = 1000;
createGridPane(); createGridPane();
@ -87,7 +87,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
scrollPane.setContent(gridPane); scrollPane.setContent(gridPane);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true); scrollPane.setFitToHeight(true);
scrollPane.setMaxHeight(1000); scrollPane.setMaxHeight(700);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
addHeadLine(); addHeadLine();
@ -112,93 +112,127 @@ public class FilterWindow extends Overlay<FilterWindow> {
gridPane.getColumnConstraints().remove(1); gridPane.getColumnConstraints().remove(1);
gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT);
InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); InputTextField keyTF = addInputTextField(gridPane, ++rowIndex,
if (useDevPrivilegeKeys) Res.get("shared.unlock"), 10);
keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); if (useDevPrivilegeKeys) {
keyTF.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY);
}
InputTextField offerIdsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); InputTextField offerIdsTF = addInputTextField(gridPane, ++rowIndex,
InputTextField nodesInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; Res.get("filterWindow.offers"));
nodesInputTextField.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate InputTextField nodesTF = addTopLabelInputTextField(gridPane, ++rowIndex,
InputTextField paymentAccountFilterInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.accounts")).second; Res.get("filterWindow.onions")).second;
GridPane.setHalignment(paymentAccountFilterInputTextField, HPos.RIGHT); nodesTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate
paymentAccountFilterInputTextField.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate InputTextField paymentAccountFilterTF = addTopLabelInputTextField(gridPane, ++rowIndex,
InputTextField bannedCurrenciesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedCurrencies")); Res.get("filterWindow.accounts")).second;
InputTextField bannedPaymentMethodsInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; GridPane.setHalignment(paymentAccountFilterTF, HPos.RIGHT);
bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate paymentAccountFilterTF.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate
InputTextField bannedSignerPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedSignerPubKeys")).second; InputTextField bannedCurrenciesTF = addInputTextField(gridPane, ++rowIndex,
bannedSignerPubKeysInputTextField.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate Res.get("filterWindow.bannedCurrencies"));
InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); InputTextField bannedPaymentMethodsTF = addTopLabelInputTextField(gridPane, ++rowIndex,
InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); Res.get("filterWindow.bannedPaymentMethods")).second;
InputTextField refundAgentsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); bannedPaymentMethodsTF.setPromptText("E.g. PERFECT_MONEY"); // Do not translate
InputTextField btcFeeReceiverAddressesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcFeeReceiverAddresses")); InputTextField bannedAccountWitnessSignerPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex,
InputTextField seedNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second;
InputTextField priceRelayNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); bannedAccountWitnessSignerPubKeysTF.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate
InputTextField btcNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcNode")); InputTextField arbitratorsTF = addInputTextField(gridPane, ++rowIndex,
CheckBox preventPublicBtcNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.preventPublicBtcNetwork")); Res.get("filterWindow.arbitrators"));
CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableDao")); InputTextField mediatorsTF = addInputTextField(gridPane, ++rowIndex,
CheckBox disableAutoConfCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableAutoConf")); Res.get("filterWindow.mediators"));
InputTextField disableDaoBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableDaoBelowVersion")); InputTextField refundAgentsTF = addInputTextField(gridPane, ++rowIndex,
InputTextField disableTradeBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); 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) { if (filter != null) {
setupFieldFromList(offerIdsInputTextField, filter.getBannedOfferIds()); setupFieldFromList(offerIdsTF, filter.getBannedOfferIds());
setupFieldFromList(nodesInputTextField, filter.getBannedNodeAddress()); setupFieldFromList(nodesTF, filter.getBannedNodeAddress());
setupFieldFromPaymentAccountFiltersList(paymentAccountFilterInputTextField, filter.getBannedPaymentAccounts()); setupFieldFromPaymentAccountFiltersList(paymentAccountFilterTF, filter.getBannedPaymentAccounts());
setupFieldFromList(bannedCurrenciesInputTextField, filter.getBannedCurrencies()); setupFieldFromList(bannedCurrenciesTF, filter.getBannedCurrencies());
setupFieldFromList(bannedPaymentMethodsInputTextField, filter.getBannedPaymentMethods()); setupFieldFromList(bannedPaymentMethodsTF, filter.getBannedPaymentMethods());
setupFieldFromList(bannedSignerPubKeysInputTextField, filter.getBannedSignerPubKeys()); setupFieldFromList(bannedAccountWitnessSignerPubKeysTF, filter.getBannedAccountWitnessSignerPubKeys());
setupFieldFromList(arbitratorsInputTextField, filter.getArbitrators()); setupFieldFromList(arbitratorsTF, filter.getArbitrators());
setupFieldFromList(mediatorsInputTextField, filter.getMediators()); setupFieldFromList(mediatorsTF, filter.getMediators());
setupFieldFromList(refundAgentsInputTextField, filter.getRefundAgents()); setupFieldFromList(refundAgentsTF, filter.getRefundAgents());
setupFieldFromList(btcFeeReceiverAddressesInputTextField, filter.getBtcFeeReceiverAddresses()); setupFieldFromList(btcFeeReceiverAddressesTF, filter.getBtcFeeReceiverAddresses());
setupFieldFromList(seedNodesInputTextField, filter.getSeedNodes()); setupFieldFromList(seedNodesTF, filter.getSeedNodes());
setupFieldFromList(priceRelayNodesInputTextField, filter.getPriceRelayNodes()); setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes());
setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); setupFieldFromList(btcNodesTF, filter.getBtcNodes());
setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys());
preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork());
disableDaoCheckBox.setSelected(filter.isDisableDao()); disableDaoCheckBox.setSelected(filter.isDisableDao());
disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf()); disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf());
disableDaoBelowVersionInputTextField.setText(filter.getDisableDaoBelowVersion()); disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion());
disableTradeBelowVersionInputTextField.setText(filter.getDisableTradeBelowVersion()); 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")); 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 -> { removeFilterMessageButton.setOnAction(e -> {
if (keyInputTextField.getText().length() > 0) { String privKeyString = keyTF.getText();
if (filterManager.removeFilterMessageIfKeyIsValid(keyInputTextField.getText())) if (filterManager.canRemoveDevFilter(privKeyString)) {
hide(); filterManager.removeDevFilter(privKeyString);
else hide();
new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); } else {
new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show();
} }
}); });
@ -218,13 +252,13 @@ public class FilterWindow extends Overlay<FilterWindow> {
private void setupFieldFromList(InputTextField field, List<String> values) { private void setupFieldFromList(InputTextField field, List<String> values) {
if (values != null) if (values != null)
field.setText(values.stream().collect(Collectors.joining(", "))); field.setText(String.join(", ", values));
} }
private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List<PaymentAccountFilter> values) { private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List<PaymentAccountFilter> values) {
if (values != null) { if (values != null) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
values.stream().forEach(e -> { values.forEach(e -> {
if (e != null && e.getPaymentMethodId() != null) { if (e != null && e.getPaymentMethodId() != null) {
sb sb
.append(e.getPaymentMethodId()) .append(e.getPaymentMethodId())

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<UpdateRevolutAccountWindow> {
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);
}
}

View File

@ -17,10 +17,17 @@
package bisq.desktop.util.validation; 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) { public ValidationResult validate(String input) {
super.setIsoCountryCode(code);
return super.validate(input); return super.validate(input);
} }

View File

@ -42,7 +42,7 @@ dependencyVerification {
'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729',
'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9',
'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2',
'com.jfoenix:jfoenix:4739e37a05e67c3bc9d5b391a1b93717b5a48fa872992616b0964d3f827f8fe6', 'com.jfoenix:jfoenix:8060235fec5eb49617ec8d81d379e8c945f6cc722d0645e97190045100de2084',
'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf', 'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf',

View File

@ -17,6 +17,7 @@
package bisq.network.p2p; package bisq.network.p2p;
import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
import bisq.common.app.Capabilities; import bisq.common.app.Capabilities;
@ -36,7 +37,7 @@ import lombok.Value;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Value
public final class BundleOfEnvelopes extends NetworkEnvelope implements ExtendedDataSizePermission, CapabilityRequiringPayload { public final class BundleOfEnvelopes extends BroadcastMessage implements ExtendedDataSizePermission, CapabilityRequiringPayload {
private final List<NetworkEnvelope> envelopes; private final List<NetworkEnvelope> envelopes;
@ -44,6 +45,10 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended
this(new ArrayList<>(), Version.getP2PMessageVersion()); this(new ArrayList<>(), Version.getP2PMessageVersion());
} }
public BundleOfEnvelopes(List<NetworkEnvelope> envelopes) {
this(envelopes, Version.getP2PMessageVersion());
}
public void add(NetworkEnvelope networkEnvelope) { public void add(NetworkEnvelope networkEnvelope) {
envelopes.add(networkEnvelope); envelopes.add(networkEnvelope);
} }
@ -67,7 +72,9 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended
.build(); .build();
} }
public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, NetworkProtoResolver resolver, int messageVersion) { public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto,
NetworkProtoResolver resolver,
int messageVersion) {
List<NetworkEnvelope> envelopes = proto.getEnvelopesList() List<NetworkEnvelope> envelopes = proto.getEnvelopesList()
.stream() .stream()
.map(envelope -> { .map(envelope -> {

View File

@ -37,7 +37,6 @@ import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.HashMapChangedListener;
import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.messages.AddDataMessage; 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.messages.RefreshOfferMessage;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
import bisq.network.p2p.storage.payload.MailboxStoragePayload; 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.ProtobufferException;
import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.util.Utilities;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -77,6 +75,7 @@ import java.security.PublicKey;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -122,9 +121,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty();
private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0);
private volatile boolean shutDownInProgress;
@Getter
private boolean shutDownComplete;
private final Subscription networkReadySubscription; private final Subscription networkReadySubscription;
private boolean isBootstrapped; private boolean isBootstrapped;
private final KeepAliveManager keepAliveManager; private final KeepAliveManager keepAliveManager;
@ -212,48 +208,48 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
} }
public void shutDown(Runnable shutDownCompleteHandler) { public void shutDown(Runnable shutDownCompleteHandler) {
if (!shutDownInProgress) { shutDownResultHandlers.add(shutDownCompleteHandler);
shutDownInProgress = true;
shutDownResultHandlers.add(shutDownCompleteHandler); // We need to make sure queued up messages are flushed out before we continue shut down other network
// services
if (p2PDataStorage != null) if (broadcaster != null) {
p2PDataStorage.shutDown(); broadcaster.shutDown(this::doShutDown);
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;
}
} else { } else {
log.debug("shutDown already in progress"); doShutDown();
if (shutDownComplete) { }
shutDownCompleteHandler.run(); }
} else {
shutDownResultHandlers.add(shutDownCompleteHandler); 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 @Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) { public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
// not handled // not used
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -672,43 +668,21 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { BroadcastHandler.Listener listener = new BroadcastHandler.Listener() {
@Override @Override
public void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts) { public void onSufficientlyBroadcast(List<Broadcaster.BroadcastRequest> 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 @Override
public void onBroadcastedToFirstPeer(BroadcastMessage message) { public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) {
// The reason for that check was to separate different callback for different send calls. sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" +
// We only want to notify our sendMailboxMessageListener for the calls he is interested in. "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" +
if (message instanceof AddDataMessage && "numOfFailedBroadcast=" + numOfFailedBroadcast);
((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
} }
}; };
boolean result = p2PDataStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener); 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."); log.error("Unexpected state: adding mailbox message that already exists.");
} }
} catch (CryptoException e) { } catch (CryptoException e) {
log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); log.error("Signing at getMailboxDataWithSignedSeqNr failed.");
} }
} else { } else {
sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " +

View File

@ -225,7 +225,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
// Called from various threads // Called from various threads
public void sendMessage(NetworkEnvelope networkEnvelope) { 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 (!stopped) {
if (noCapabilityRequiredOrCapabilityIsSupported(networkEnvelope)) { 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) { public boolean noCapabilityRequiredOrCapabilityIsSupported(Proto msg) {
boolean result; boolean result;
if (msg instanceof AddDataMessage) { if (msg instanceof AddDataMessage) {
@ -408,12 +410,13 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
checkArgument(connection.equals(this)); checkArgument(connection.equals(this));
if (networkEnvelope instanceof BundleOfEnvelopes) if (networkEnvelope instanceof BundleOfEnvelopes) {
for (NetworkEnvelope current : ((BundleOfEnvelopes) networkEnvelope).getEnvelopes()) { for (NetworkEnvelope current : ((BundleOfEnvelopes) networkEnvelope).getEnvelopes()) {
UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(current, connection))); UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(current, connection)));
} }
else } else {
UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection))); UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)));
}
} }

View File

@ -17,6 +17,7 @@
package bisq.network.p2p.peers; package bisq.network.p2p.peers;
import bisq.network.p2p.BundleOfEnvelopes;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.network.NetworkNode;
@ -24,7 +25,6 @@ import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.util.Utilities;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -41,11 +40,10 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Slf4j @Slf4j
public class BroadcastHandler implements PeerManager.Listener { 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 { interface ResultHandler {
void onCompleted(BroadcastHandler broadcastHandler); void onCompleted(BroadcastHandler broadcastHandler);
void onFault(BroadcastHandler broadcastHandler);
} }
public interface Listener { public interface Listener {
@SuppressWarnings({"EmptyMethod", "UnusedParameters"}) void onSufficientlyBroadcast(List<Broadcaster.BroadcastRequest> broadcastRequests);
void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts);
void onBroadcastedToFirstPeer(BroadcastMessage message); void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast);
void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts);
@SuppressWarnings({"EmptyMethod", "UnusedParameters"})
void onBroadcastFailed(String errorMessage);
} }
@ -76,16 +66,12 @@ public class BroadcastHandler implements PeerManager.Listener {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode; private final NetworkNode networkNode;
public final String uid;
private final PeerManager peerManager; private final PeerManager peerManager;
private boolean stopped = false; private final ResultHandler resultHandler;
private int numOfCompletedBroadcasts = 0; private final String uid;
private int numOfFailedBroadcasts = 0;
private BroadcastMessage message; private boolean stopped, timeoutTriggered;
private ResultHandler resultHandler; private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeersForBroadcast;
@Nullable
private Listener listener;
private int numPeers;
private Timer timeoutTimer; private Timer timeoutTimer;
@ -93,16 +79,13 @@ public class BroadcastHandler implements PeerManager.Listener {
// Constructor // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) { BroadcastHandler(NetworkNode networkNode, PeerManager peerManager, ResultHandler resultHandler) {
this.networkNode = networkNode; this.networkNode = networkNode;
this.peerManager = peerManager; this.peerManager = peerManager;
peerManager.addListener(this); this.resultHandler = resultHandler;
uid = UUID.randomUUID().toString(); uid = UUID.randomUUID().toString();
}
public void cancel() { peerManager.addListener(this);
stopped = true;
onFault("Broadcast canceled.", false);
} }
@ -110,110 +93,73 @@ public class BroadcastHandler implements PeerManager.Listener {
// API // API
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, ResultHandler resultHandler, public void broadcast(List<Broadcaster.BroadcastRequest> broadcastRequests, boolean shutDownRequested) {
@Nullable Listener listener) { List<Connection> confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections());
this.message = message; Collections.shuffle(confirmedConnections);
this.resultHandler = resultHandler;
this.listener = listener;
Set<Connection> connectedPeersSet = networkNode.getConfirmedConnections() int delay;
.stream() if (shutDownRequested) {
.filter(connection -> !connection.getPeersNodeAddressOptional().get().equals(sender)) delay = 1;
.collect(Collectors.toSet()); // We sent to all peers as in case we had offers we want that it gets removed with higher reliability
if (!connectedPeersSet.isEmpty()) { numPeersForBroadcast = confirmedConnections.size();
numOfCompletedBroadcasts = 0; } else {
if (requestsContainOwnMessage(broadcastRequests)) {
List<Connection> connectedPeersList = new ArrayList<>(connectedPeersSet); // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and
Collections.shuffle(connectedPeersList); // with shorter delay
numPeers = connectedPeersList.size(); numPeersForBroadcast = confirmedConnections.size();
int delay = 50; delay = 50;
} else {
boolean isDataOwner = (sender != null) && sender.equals(networkNode.getNodeAddress()); // Relay nodes only send to max 7 peers and with longer delay
if (!isDataOwner) { numPeersForBroadcast = Math.min(7, confirmedConnections.size());
// for not data owner (relay nodes) we send to max. 7 nodes and use a longer delay
numPeers = Math.min(7, connectedPeersList.size());
delay = 100; delay = 100;
} }
}
long timeoutDelay = TIMEOUT + delay * numPeers; setupTimeoutHandler(broadcastRequests, delay, shutDownRequested);
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec.";
log.debug(errorMessage + "\n\t" + int iterations = numPeersForBroadcast;
"numOfPeers=" + numPeers + "\n\t" + for (int i = 0; i < iterations; i++) {
"numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" + long minDelay = (i + 1) * delay;
"numOfFailedBroadcasts=" + numOfFailedBroadcasts); long maxDelay = (i + 2) * delay;
onFault(errorMessage, false); Connection connection = confirmedConnections.get(i);
}, timeoutDelay); UserThread.runAfterRandomDelay(() -> {
if (stopped) {
return;
}
log.debug("Broadcast message to {} peers out of {} total connected peers.", numPeers, connectedPeersSet.size()); // We use broadcastRequests which have excluded the requests for messages the connection has
for (int i = 0; i < numPeers; i++) { // originated to avoid sending back the message we received. We also remove messages not satisfying
if (stopped) // capability checks.
break; // do not continue sending after a timeout or a cancellation List<Broadcaster.BroadcastRequest> broadcastRequestsForConnection = getBroadcastRequestsForConnection(connection, broadcastRequests);
final long minDelay = (i + 1) * delay; // Could be empty list...
final long maxDelay = (i + 2) * delay; if (broadcastRequestsForConnection.isEmpty()) {
final Connection connection = connectedPeersList.get(i); // We decrease numPeers in that case for making completion checks correct.
UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS); if (numPeersForBroadcast > 0) {
} numPeersForBroadcast--;
} else { }
onFault("Message not broadcasted because we have no available peers yet.\n\t" + checkForCompletion();
"message = " + Utilities.toTruncatedString(message), false); 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) { public void cancel() {
String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" + stopped = true;
"message = " + Utilities.toTruncatedString(message); cleanup();
if (!stopped) {
if (!connection.isStopped()) {
if (connection.noCapabilityRequiredOrCapabilityIsSupported(message)) {
NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get();
SettableFuture<Connection> future = networkNode.sendMessage(connection, message);
Futures.addCallback(future, new FutureCallback<Connection>() {
@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);
}
} }
@ -223,7 +169,7 @@ public class BroadcastHandler implements PeerManager.Listener {
@Override @Override
public void onAllConnectionsLost() { public void onAllConnectionsLost() {
onFault("All connections lost", false); cleanup();
} }
@Override @Override
@ -239,37 +185,142 @@ public class BroadcastHandler implements PeerManager.Listener {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Check if we have at least one message originated by ourselves
private boolean requestsContainOwnMessage(List<Broadcaster.BroadcastRequest> broadcastRequests) {
NodeAddress myAddress = networkNode.getNodeAddress();
if (myAddress == null)
return false;
return broadcastRequests.stream().anyMatch(e -> myAddress.equals(e.getSender()));
}
private void setupTimeoutHandler(List<Broadcaster.BroadcastRequest> 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<Broadcaster.BroadcastRequest> getBroadcastRequestsForConnection(Connection connection,
List<Broadcaster.BroadcastRequest> 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<Broadcaster.BroadcastRequest> broadcastRequestsForConnection) {
// Can be BundleOfEnvelopes or a single BroadcastMessage
BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection);
SettableFuture<Connection> 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<Broadcaster.BroadcastRequest> 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<Broadcaster.BroadcastRequest> 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() { private void cleanup() {
stopped = true; stopped = true;
peerManager.removeListener(this);
if (timeoutTimer != null) { if (timeoutTimer != null) {
timeoutTimer.stop(); timeoutTimer.stop();
timeoutTimer = null; 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -277,11 +328,11 @@ public class BroadcastHandler implements PeerManager.Listener {
BroadcastHandler that = (BroadcastHandler) o; BroadcastHandler that = (BroadcastHandler) o;
return !(uid != null ? !uid.equals(that.uid) : that.uid != null); return uid.equals(that.uid);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return uid != null ? uid.hashCode() : 0; return uid.hashCode();
} }
} }

View File

@ -21,18 +21,34 @@ import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.common.Timer;
import bisq.common.UserThread;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; 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; import org.jetbrains.annotations.Nullable;
@Slf4j
public class Broadcaster implements BroadcastHandler.ResultHandler { public class Broadcaster implements BroadcastHandler.ResultHandler {
private static final long BROADCAST_INTERVAL_MS = 2000;
private final NetworkNode networkNode; private final NetworkNode networkNode;
private final PeerManager peerManager; private final PeerManager peerManager;
private final Set<BroadcastHandler> broadcastHandlers = new CopyOnWriteArraySet<>(); private final Set<BroadcastHandler> broadcastHandlers = new CopyOnWriteArraySet<>();
private final List<BroadcastRequest> 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; this.peerManager = peerManager;
} }
public void shutDown() { public void shutDown(Runnable resultHandler) {
broadcastHandlers.stream().forEach(BroadcastHandler::cancel); shutDownRequested = true;
broadcastHandlers.clear(); 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 // 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) { @Nullable BroadcastHandler.Listener listener) {
BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager); broadcastRequests.add(new BroadcastRequest(message, sender, listener));
broadcastHandler.broadcast(message, sender, this, listener); // Keep that log on INFO for better debugging if the feature works as expected. Later it can
broadcastHandlers.add(broadcastHandler); // 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 @Override
public void onCompleted(BroadcastHandler broadcastHandler) { public void onCompleted(BroadcastHandler broadcastHandler) {
broadcastHandlers.remove(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;
}
} }
} }

View File

@ -49,8 +49,8 @@ import org.jetbrains.annotations.NotNull;
public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener { public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener {
private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class); private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class);
private static final int INTERVAL_SEC = new Random().nextInt(5) + 30; private static final int INTERVAL_SEC = new Random().nextInt(30) + 30;
private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC / 2; private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC * 1000 / 2;
private final NetworkNode networkNode; private final NetworkNode networkNode;
private final PeerManager peerManager; private final PeerManager peerManager;

View File

@ -529,7 +529,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
// Broadcast the payload if requested by caller // Broadcast the payload if requested by caller
if (allowBroadcast) if (allowBroadcast)
broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender, null); broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender);
return true; return true;
} }
@ -675,7 +675,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000); sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000);
// Always broadcast refreshes // Always broadcast refreshes
broadcaster.broadcast(refreshTTLMessage, sender, null); broadcaster.broadcast(refreshTTLMessage, sender);
return true; return true;
} }
@ -725,9 +725,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
printData("after remove"); printData("after remove");
if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) {
broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null); broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender);
} else { } else {
broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender, null); broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender);
} }
return true; return true;

View File

@ -33,8 +33,6 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.mockito.ArgumentMatchers.any; 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.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -69,7 +67,7 @@ public class P2PDataStorageOnMessageHandlerTest {
this.testState.mockedStorage.onMessage(envelope, mockedConnection); this.testState.mockedStorage.onMessage(envelope, mockedConnection);
verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); 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 @Test
@ -82,7 +80,7 @@ public class P2PDataStorageOnMessageHandlerTest {
this.testState.mockedStorage.onMessage(envelope, mockedConnection); this.testState.mockedStorage.onMessage(envelope, mockedConnection);
verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); 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 @Test
@ -96,6 +94,6 @@ public class P2PDataStorageOnMessageHandlerTest {
this.testState.mockedStorage.onMessage(envelope, mockedConnection); this.testState.mockedStorage.onMessage(envelope, mockedConnection);
verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); 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));
} }
} }

View File

@ -19,7 +19,6 @@ package bisq.network.p2p.storage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.BroadcastHandler;
import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.storage.messages.AddDataMessage; import bisq.network.p2p.storage.messages.AddDataMessage;
import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage;
@ -51,10 +50,10 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.Assert;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.junit.Assert;
import static org.mockito.Mockito.*; 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. * 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, private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, int sequenceNumber) {
int sequenceNumber) {
final ArgumentCaptor<SequenceNumberMap> captor = ArgumentCaptor.forClass(SequenceNumberMap.class); final ArgumentCaptor<SequenceNumberMap> captor = ArgumentCaptor.forClass(SequenceNumberMap.class);
verify(this.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong()); verify(this.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong());
@ -187,10 +185,9 @@ public class TestState {
verify(this.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); verify(this.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload);
if (expectedBroadcast) if (expectedBroadcast)
verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), nullable(NodeAddress.class));
nullable(NodeAddress.class), isNull());
else 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, void verifyProtectedStorageAdd(SavedTestState beforeState,
@ -219,13 +216,17 @@ public class TestState {
if (expectedBroadcast) { if (expectedBroadcast) {
final ArgumentCaptor<BroadcastMessage> captor = ArgumentCaptor.forClass(BroadcastMessage.class); final ArgumentCaptor<BroadcastMessage> 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()); verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull());
BroadcastMessage broadcastMessage = captor.getValue(); BroadcastMessage broadcastMessage = captor.getValue();
Assert.assertTrue(broadcastMessage instanceof AddDataMessage); Assert.assertTrue(broadcastMessage instanceof AddDataMessage);
Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry());
} else { } 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) { if (expectedSequenceNrMapWrite) {
@ -275,7 +276,7 @@ public class TestState {
verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong());
if (!expectedBroadcast) 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 -> { protectedStorageEntries.forEach(protectedStorageEntry -> {
@ -287,9 +288,9 @@ public class TestState {
if (expectedBroadcast) { if (expectedBroadcast) {
if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) 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 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); Assert.assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate);
final ArgumentCaptor<BroadcastMessage> captor = ArgumentCaptor.forClass(BroadcastMessage.class); final ArgumentCaptor<BroadcastMessage> 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(); BroadcastMessage broadcastMessage = captor.getValue();
Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage); Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage);
@ -336,7 +337,7 @@ public class TestState {
Assert.assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); 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()); verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong());
} }
} }

View File

@ -639,7 +639,10 @@ message Filter {
repeated string refundAgents = 18; repeated string refundAgents = 18;
repeated string bannedSignerPubKeys = 19; repeated string bannedSignerPubKeys = 19;
repeated string btc_fee_receiver_addresses = 20; 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 // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older
@ -1087,6 +1090,7 @@ message PopmoneyAccountPayload {
message RevolutAccountPayload { message RevolutAccountPayload {
string account_id = 1; string account_id = 1;
string user_name = 2;
} }
message PerfectMoneyAccountPayload { message PerfectMoneyAccountPayload {