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