Merge master

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

View File

@ -1,11 +1,11 @@
#!/usr/bin/env bats
#
# 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 ]
}

View File

@ -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;

View File

@ -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;

View File

@ -30,58 +30,64 @@ import bisq.daemon.app.BisqDaemonMain;
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a>
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/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" +
'}';
}
}

View File

@ -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() {

View File

@ -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() {

View File

@ -1,109 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import java.util.concurrent.Executor;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
import static io.grpc.Status.UNAUTHENTICATED;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.config.BisqAppConfig;
public class GrpcStubs {
public final CallCredentials credentials;
public final String host;
public final int port;
public GetVersionGrpc.GetVersionBlockingStub versionService;
public OffersGrpc.OffersBlockingStub offersService;
public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
public WalletsGrpc.WalletsBlockingStub walletsService;
public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
this.credentials = new PasswordCallCredentials(config.apiPassword);
this.host = "localhost";
this.port = bisqAppConfig.apiPort;
}
public GrpcStubs init() {
var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
channel.shutdown().awaitTermination(1, SECONDS);
} catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}));
this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
return this;
}
static class PasswordCallCredentials extends CallCredentials {
public static final String PASSWORD_KEY = "password";
private final String passwordValue;
public PasswordCallCredentials(String passwordValue) {
if (passwordValue == null)
throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY));
this.passwordValue = passwordValue;
}
@Override
public void applyRequestMetadata(RequestInfo requestInfo,
Executor appExecutor,
MetadataApplier metadataApplier) {
appExecutor.execute(() -> {
try {
var headers = new Metadata();
var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER);
headers.put(passwordKey, passwordValue);
metadataApplier.apply(headers);
} catch (Throwable ex) {
metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
}
});
}
@Override
public void thisUsesUnstableApi() {
// An experimental api. A noop but never called; tries to make it clearer to
// implementors that they may break in the future.
}
}
}

View File

@ -0,0 +1,9 @@
package bisq.asset;
public class LiquidBitcoinAddressValidator extends RegexAddressValidator {
static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$";
public LiquidBitcoinAddressValidator() {
super(REGEX, "validation.altcoin.liquidBitcoin.invalidAddress");
}
}

View File

@ -19,12 +19,12 @@ package bisq.asset.coins;
import bisq.asset.AltCoinAccountDisclaimer;
import bisq.asset.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());
}
}

View File

@ -0,0 +1,12 @@
package bisq.asset.coins;
import bisq.asset.Coin;
import bisq.asset.LiquidBitcoinAddressValidator;
public class TetherUSDLiquid extends Coin {
public TetherUSDLiquid() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (Liquid Bitcoin)", "L-USDT", new LiquidBitcoinAddressValidator());
}
}

View File

@ -0,0 +1,12 @@
package bisq.asset.coins;
import bisq.asset.Base58BitcoinAddressValidator;
import bisq.asset.Coin;
public class TetherUSDOmni extends Coin {
public TetherUSDOmni() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (Omni)", "USDT-O", new Base58BitcoinAddressValidator());
}
}

View File

@ -0,0 +1,11 @@
package bisq.asset.tokens;
import bisq.asset.Erc20Token;
public class TetherUSDERC20 extends Erc20Token {
public TetherUSDERC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (ERC20)", "USDT-E");
}
}

View File

@ -104,6 +104,8 @@ bisq.asset.coins.Spectrecoin
bisq.asset.coins.Starwels
bisq.asset.coins.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

View File

@ -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'

View File

@ -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) {

View File

@ -0,0 +1,54 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannelBuilder;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GrpcStubs {
public final GetVersionGrpc.GetVersionBlockingStub versionService;
public final OffersGrpc.OffersBlockingStub offersService;
public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
public final WalletsGrpc.WalletsBlockingStub walletsService;
public GrpcStubs(String apiHost, int apiPort, String apiPassword) {
CallCredentials credentials = new PasswordCallCredentials(apiPassword);
var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
channel.shutdown().awaitTermination(1, SECONDS);
} catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}));
this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
}
}

View File

@ -18,12 +18,15 @@
package bisq.common.proto;
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);
}
}

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -238,7 +238,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
log.debug("We received a new message from peer {} and broadcast it to our peers. extBlockId={}",
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);

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
///////////////////////////////////////////////////////////////////////////////////////////

View File

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

View File

@ -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
///////////////////////////////////////////////////////////////////////////////////////////

View File

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

View File

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

View File

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

View File

@ -1065,7 +1065,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver
support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1}
support.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\

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,12 +28,13 @@ import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow;
import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow;
import bisq.desktop.main.overlays.windows.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;

View File

@ -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)

View File

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

View File

@ -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) {

View File

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

View File

@ -0,0 +1,96 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.RevolutValidator;
import bisq.core.locale.Res;
import bisq.core.payment.RevolutAccount;
import bisq.core.user.User;
import javafx.scene.Scene;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addLabel;
public class UpdateRevolutAccountWindow extends Overlay<UpdateRevolutAccountWindow> {
private final RevolutValidator revolutValidator;
private final RevolutAccount revolutAccount;
private final User user;
private InputTextField userNameInputTextField;
public UpdateRevolutAccountWindow(RevolutAccount revolutAccount, User user) {
super();
this.revolutAccount = revolutAccount;
this.user = user;
type = Type.Attention;
hideCloseButton = true;
revolutValidator = new RevolutValidator();
actionButtonText = Res.get("shared.save");
}
@Override
protected void setupKeyHandler(Scene scene) {
// We do not support enter or escape here
}
@Override
public void show() {
if (headLine == null)
headLine = Res.get("payment.revolut.addUserNameInfo.headLine");
width = 868;
createGridPane();
addHeadLine();
addContent();
addButtons();
applyStyles();
display();
}
private void addContent() {
addLabel(gridPane, ++rowIndex, Res.get("payment.account.revolut.addUserNameInfo", Res.get("payment.revolut.info"), revolutAccount.getAccountName()));
userNameInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("payment.account.userName"), Layout.COMPACT_FIRST_ROW_DISTANCE);
userNameInputTextField.setValidator(revolutValidator);
userNameInputTextField.textProperty().addListener((observable, oldValue, newValue) ->
actionButton.setDisable(!revolutValidator.validate(newValue).isValid));
}
@Override
protected void addButtons() {
super.addButtons();
// We do not allow close in case the userName is not correctly added so we
// overwrote the default handler
actionButton.setOnAction(event -> {
String userName = userNameInputTextField.getText();
if (revolutValidator.validate(userName).isValid) {
revolutAccount.setUserName(userName);
user.persist();
closeHandlerOptional.ifPresent(Runnable::run);
hide();
}
});
actionButton.setDisable(true);
}
}

View File

@ -17,10 +17,17 @@
package bisq.desktop.util.validation;
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);
}

View File

@ -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',

View File

@ -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 -> {

View File

@ -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. " +

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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 {