This commit is contained in:
woodser 2021-05-04 20:20:01 -04:00
commit 8a38081c04
2800 changed files with 344130 additions and 0 deletions

View file

@ -0,0 +1,33 @@
/*
* 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.common;
public class BisqException extends RuntimeException {
public BisqException(Throwable cause) {
super(cause);
}
public BisqException(String format, Object... args) {
super(String.format(format, args));
}
public BisqException(Throwable cause, String format, Object... args) {
super(String.format(format, args), cause);
}
}

View file

@ -0,0 +1,95 @@
/*
* 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.common;
import javax.inject.Singleton;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
// Helps configure listener objects that are run by the `UserThread` each second
// and can do per second, per minute and delayed second actions. Also detects when we were in standby, and logs it.
@Slf4j
@Singleton
public class ClockWatcher {
public static final int IDLE_TOLERANCE_MS = 20000;
public interface Listener {
void onSecondTick();
void onMinuteTick();
default void onMissedSecondTick(long missedMs) {
}
default void onAwakeFromStandby(long missedMs) {
}
}
private Timer timer;
private final List<Listener> listeners = new LinkedList<>();
private long counter = 0;
private long lastSecondTick;
public ClockWatcher() {
}
public void start() {
if (timer == null) {
lastSecondTick = System.currentTimeMillis();
timer = UserThread.runPeriodically(() -> {
listeners.forEach(Listener::onSecondTick);
counter++;
if (counter >= 60) {
counter = 0;
listeners.forEach(Listener::onMinuteTick);
}
long currentTimeMillis = System.currentTimeMillis();
long diff = currentTimeMillis - lastSecondTick;
if (diff > 1000) {
long missedMs = diff - 1000;
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
log.info("We have been in standby mode for {} sec", missedMs / 1000);
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
}
}
lastSecondTick = currentTimeMillis;
}, 1, TimeUnit.SECONDS);
}
}
public void stop() {
timer.stop();
timer = null;
counter = 0;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.common;
/**
* Interface for the outside envelope object sent over the network or persisted to disk.
*/
public interface Envelope extends Proto {
}

View file

@ -0,0 +1,105 @@
/*
* 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.common;
import java.time.Duration;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* We simulate a global frame rate timer similar to FXTimer to avoid creation of threads for each timer call.
* Used only in headless apps like the seed node.
*/
public class FrameRateTimer implements Timer, Runnable {
private final Logger log = LoggerFactory.getLogger(FrameRateTimer.class);
private long interval;
private Runnable runnable;
private long startTs;
private boolean isPeriodically;
private final String uid = UUID.randomUUID().toString();
private volatile boolean stopped;
public FrameRateTimer() {
}
@Override
public void run() {
if (!stopped) {
try {
long currentTimeMillis = System.currentTimeMillis();
if ((currentTimeMillis - startTs) >= interval) {
runnable.run();
if (isPeriodically)
startTs = currentTimeMillis;
else
stop();
}
} catch (Throwable t) {
log.error("exception in FrameRateTimer", t);
stop();
throw t;
}
}
}
@Override
public Timer runLater(Duration delay, Runnable runnable) {
this.interval = delay.toMillis();
this.runnable = runnable;
startTs = System.currentTimeMillis();
MasterTimer.addListener(this);
return this;
}
@Override
public Timer runPeriodically(Duration interval, Runnable runnable) {
this.interval = interval.toMillis();
isPeriodically = true;
this.runnable = runnable;
startTs = System.currentTimeMillis();
MasterTimer.addListener(this);
return this;
}
@Override
public void stop() {
stopped = true;
MasterTimer.removeListener(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FrameRateTimer)) return false;
FrameRateTimer that = (FrameRateTimer) o;
return !(uid != null ? !uid.equals(that.uid) : that.uid != null);
}
@Override
public int hashCode() {
return uid != null ? uid.hashCode() : 0;
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.common;
import java.util.Set;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArraySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Runs all listener objects periodically in a short interval.
public class MasterTimer {
private final static Logger log = LoggerFactory.getLogger(MasterTimer.class);
private static final java.util.Timer timer = new java.util.Timer();
// frame rate of 60 fps is about 16 ms but we don't need such a short interval, 100 ms should be good enough
public static final long FRAME_INTERVAL_MS = 100;
static {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
UserThread.execute(() -> listeners.forEach(Runnable::run));
}
}, FRAME_INTERVAL_MS, FRAME_INTERVAL_MS);
}
private static final Set<Runnable> listeners = new CopyOnWriteArraySet<>();
public static void addListener(Runnable runnable) {
listeners.add(runnable);
}
public static void removeListener(Runnable runnable) {
listeners.remove(runnable);
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.common;
/**
* Interface for objects used inside an Envelope or other Payloads.
*/
public interface Payload extends Proto {
}

View file

@ -0,0 +1,27 @@
/*
* 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.common;
import com.google.protobuf.Message;
/**
* Base interface for Envelope and Payload.
*/
public interface Proto {
Message toProtoMessage();
}

View file

@ -0,0 +1,28 @@
/*
* 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.common;
import java.time.Duration;
public interface Timer {
Timer runLater(java.time.Duration delay, Runnable action);
Timer runPeriodically(Duration interval, Runnable runnable);
void stop();
}

View file

@ -0,0 +1,100 @@
/*
* 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.common;
import com.google.common.util.concurrent.MoreExecutors;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.lang.reflect.InvocationTargetException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* Defines which thread is used as user thread. The user thread is the the main thread in the single threaded context.
* For JavaFX it is usually the Platform::RunLater executor, for a headless application it is any single threaded
* executor.
* Additionally sets a timer class so JavaFX and headless applications can set different timers (UITimer for JavaFX
* otherwise we use the default FrameRateTimer).
* <p>
* Provides also methods for delayed and periodic executions.
*/
@Slf4j
public class UserThread {
private static Class<? extends Timer> timerClass;
@Getter
@Setter
private static Executor executor;
public static void setTimerClass(Class<? extends Timer> timerClass) {
UserThread.timerClass = timerClass;
}
static {
// If not defined we use same thread as caller thread
executor = MoreExecutors.directExecutor();
timerClass = FrameRateTimer.class;
}
public static void execute(Runnable command) {
UserThread.executor.execute(command);
}
// Prefer FxTimer if a delay is needed in a JavaFx class (gui module)
public static Timer runAfterRandomDelay(Runnable runnable, long minDelayInSec, long maxDelayInSec) {
return UserThread.runAfterRandomDelay(runnable, minDelayInSec, maxDelayInSec, TimeUnit.SECONDS);
}
@SuppressWarnings("WeakerAccess")
public static Timer runAfterRandomDelay(Runnable runnable, long minDelay, long maxDelay, TimeUnit timeUnit) {
return UserThread.runAfter(runnable, new Random().nextInt((int) (maxDelay - minDelay)) + minDelay, timeUnit);
}
public static Timer runAfter(Runnable runnable, long delayInSec) {
return UserThread.runAfter(runnable, delayInSec, TimeUnit.SECONDS);
}
public static Timer runAfter(Runnable runnable, long delay, TimeUnit timeUnit) {
return getTimer().runLater(Duration.ofMillis(timeUnit.toMillis(delay)), runnable);
}
public static Timer runPeriodically(Runnable runnable, long intervalInSec) {
return UserThread.runPeriodically(runnable, intervalInSec, TimeUnit.SECONDS);
}
public static Timer runPeriodically(Runnable runnable, long interval, TimeUnit timeUnit) {
return getTimer().runPeriodically(Duration.ofMillis(timeUnit.toMillis(interval)), runnable);
}
private static Timer getTimer() {
try {
return timerClass.getDeclaredConstructor().newInstance();
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
String message = "Could not instantiate timer bsTimerClass=" + timerClass;
log.error(message, e);
throw new RuntimeException(message);
}
}
}

View file

@ -0,0 +1,66 @@
/*
* 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.common.app;
import bisq.common.config.Config;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import java.util.ArrayList;
import java.util.List;
public abstract class AppModule extends AbstractModule {
protected final Config config;
private final List<AppModule> modules = new ArrayList<>();
protected AppModule(Config config) {
this.config = config;
}
protected void install(AppModule module) {
super.install(module);
modules.add(module);
}
/**
* Close any instances this module is responsible for and recursively close any
* sub-modules installed via {@link #install(AppModule)}. This method
* must be called manually, e.g. at the end of a main() method or in the stop() method
* of a JavaFX Application; alternatively it may be registered as a JVM shutdown hook.
*
* @param injector the Injector originally initialized with this module
* @see #doClose(com.google.inject.Injector)
*/
public final void close(Injector injector) {
modules.forEach(module -> module.close(injector));
doClose(injector);
}
/**
* Actually perform closing of any instances this module is responsible for. Called by
* {@link #close(Injector)}.
*
* @param injector the Injector originally initialized with this module
*/
@SuppressWarnings({"WeakerAccess", "EmptyMethod", "UnusedParameters"})
protected void doClose(Injector injector) {
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.common.app;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AsciiLogo {
public static void showAsciiLogo() {
String ls = System.lineSeparator();
log.info(ls + ls +
" ........ ...... " + ls +
" .............. ...... " + ls +
" ................. ...... " + ls +
" ...... .......... .. ...... " + ls +
" ...... ...... ...... ............... ..... ......... .......... " + ls +
" ....... ........ .................. ..... ............. ............... " + ls +
" ...... ........ .......... ....... ..... ...... ... ........ ....... " + ls +
" ...... ..... ....... ..... ..... ..... ..... ...... " + ls +
" ...... ... ... ...... ...... ..... ........... ...... ...... " + ls +
" ...... ..... .... ...... ...... ..... ............ ..... ...... " + ls +
" ...... ..... ...... ..... ........ ...... ...... " + ls +
" ...... .... ... ...... ...... ..... .. ...... ...... ........ " + ls +
" ........ .. ....... ................. ..... .............. ................... " + ls +
" .......... ......... ............. ..... ............ ................. " + ls +
" ...................... ..... .... .... ...... " + ls +
" ................ ...... " + ls +
" .... ...... " + ls +
" ...... " + ls +
ls + ls);
}
}

View file

@ -0,0 +1,201 @@
/*
* 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.common.app;
import com.google.common.base.Joiner;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
/**
* hold a set of capabilities and offers appropriate comparison methods.
*
* @author Florian Reimair
*/
@EqualsAndHashCode
@Slf4j
public class Capabilities {
/**
* The global set of capabilities, i.e. the capabilities if the local app.
*/
public static final Capabilities app = new Capabilities();
// Defines which most recent capability any node need to support.
// This helps to clean network from very old inactive but still running nodes.
@SuppressWarnings("deprecation")
private static final Capability MANDATORY_CAPABILITY = Capability.DAO_STATE;
protected final Set<Capability> capabilities = new HashSet<>();
public Capabilities(Capability... capabilities) {
this(Arrays.asList(capabilities));
}
public Capabilities(Capabilities capabilities) {
this(capabilities.capabilities);
}
public Capabilities(Collection<Capability> capabilities) {
this.capabilities.addAll(capabilities);
}
public void set(Capability... capabilities) {
set(Arrays.asList(capabilities));
}
public void set(Capabilities capabilities) {
set(capabilities.capabilities);
}
public void set(Collection<Capability> capabilities) {
this.capabilities.clear();
this.capabilities.addAll(capabilities);
}
public void addAll(Capability... capabilities) {
this.capabilities.addAll(Arrays.asList(capabilities));
}
public void addAll(Capabilities capabilities) {
if (capabilities != null)
this.capabilities.addAll(capabilities.capabilities);
}
public boolean containsAll(final Set<Capability> requiredItems) {
return capabilities.containsAll(requiredItems);
}
public boolean containsAll(final Capabilities capabilities) {
return containsAll(capabilities.capabilities);
}
public boolean containsAll(Capability... capabilities) {
return this.capabilities.containsAll(Arrays.asList(capabilities));
}
public boolean contains(Capability capability) {
return this.capabilities.contains(capability);
}
public boolean isEmpty() {
return capabilities.isEmpty();
}
/**
* helper for protobuffer stuff
*
* @param capabilities
* @return int list of Capability ordinals
*/
public static List<Integer> toIntList(Capabilities capabilities) {
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
}
/**
* helper for protobuffer stuff
*
* @param capabilities a list of Capability ordinals
* @return a {@link Capabilities} object
*/
public static Capabilities fromIntList(List<Integer> capabilities) {
return new Capabilities(capabilities.stream()
.filter(integer -> integer < Capability.values().length)
.filter(integer -> integer >= 0)
.map(integer -> Capability.values()[integer])
.collect(Collectors.toSet()));
}
/**
*
* @param list Comma separated list of Capability ordinals.
* @return Capabilities
*/
public static Capabilities fromStringList(String list) {
if (list == null || list.isEmpty())
return new Capabilities();
List<String> entries = List.of(list.replace(" ", "").split(","));
List<Integer> capabilitiesList = entries.stream()
.map(c -> {
try {
return Integer.parseInt(c);
} catch (Throwable e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
return Capabilities.fromIntList(capabilitiesList);
}
/**
* @return Converts capabilities to list of ordinals as comma separated strings
*/
public String toStringList() {
return Joiner.on(", ").join(Capabilities.toIntList(this));
}
public static boolean hasMandatoryCapability(Capabilities capabilities) {
return hasMandatoryCapability(capabilities, MANDATORY_CAPABILITY);
}
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
}
@Override
public String toString() {
return Arrays.toString(Capabilities.toIntList(this).toArray());
}
public String prettyPrint() {
return capabilities.stream()
.sorted(Comparator.comparingInt(Enum::ordinal))
.map(e -> e.name() + " [" + e.ordinal() + "]")
.collect(Collectors.joining(", "));
}
public int size() {
return capabilities.size();
}
// We return true if our capabilities have less capabilities than the parameter value
public boolean hasLess(Capabilities other) {
return findHighestCapability(this) < findHighestCapability(other);
}
// We use the sum of all capabilities. Alternatively we could use the highest entry.
// Neither would support removal of past capabilities, a use case we never had so far and which might have
// backward compatibility issues, so we should treat capabilities as an append-only data structure.
public int findHighestCapability(Capabilities capabilities) {
return (int) capabilities.capabilities.stream()
.mapToLong(e -> (long) e.ordinal())
.sum();
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.common.app;
// We can define here special features the client is supporting.
// Useful for updates to new versions where a new data type would break backwards compatibility or to
// limit a node to certain behaviour and roles like the seed nodes.
// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility.
// We use the ordinal integer instead.
// Sequence in the enum must not be changed (append only).
public enum Capability {
@Deprecated TRADE_STATISTICS, // Not required anymore as no old clients out there not having that support
@Deprecated TRADE_STATISTICS_2, // Not required anymore as no old clients out there not having that support
@Deprecated ACCOUNT_AGE_WITNESS, // Not required anymore as no old clients out there not having that support
SEED_NODE, // Node is a seed node
DAO_FULL_NODE, // DAO full node can deliver BSQ blocks
@Deprecated PROPOSAL, // Not required anymore as no old clients out there not having that support
@Deprecated BLIND_VOTE, // Not required anymore as no old clients out there not having that support
@Deprecated ACK_MSG, // Not required anymore as no old clients out there not having that support
RECEIVE_BSQ_BLOCK, // Signaling that node which wants to receive BSQ blocks (DAO lite node)
@Deprecated DAO_STATE, // Not required anymore as no old clients out there not having that support
@Deprecated BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval
SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature
MEDIATION, // Supports mediation feature
REFUND_AGENT, // Supports refund agents
TRADE_STATISTICS_HASH_UPDATE, // We changed the hash method in 1.2.0 and that requires update to 1.2.2 for handling it correctly, otherwise the seed nodes have to process too much data.
NO_ADDRESS_PRE_FIX, // At 1.4.0 we removed the prefix filter for mailbox messages. If a peer has that capability we do not sent the prefix.
TRADE_STATISTICS_3 // We used a new reduced trade statistics model from v1.4.0 on
}

View file

@ -0,0 +1,71 @@
/*
* 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.common.app;
import bisq.common.config.Config;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DevEnv {
// The UI got set the private dev key so the developer does not need to do anything and can test those features.
// Features: Arbitration registration (alt+R at account), Alert/Update (alt+m), private message to a
// peer (click user icon and alt+r), filter/block offers by various data like offer ID (cmd + f).
// The user can set a program argument to ignore all of those privileged network_messages. They are intended for
// emergency cases only (beside update message and arbitrator registration).
public static final String DEV_PRIVILEGE_PUB_KEY = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee";
public static final String DEV_PRIVILEGE_PRIV_KEY = "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a";
public static void setup(Config config) {
DevEnv.setDevMode(config.useDevMode);
DevEnv.setDaoActivated(config.daoActivated);
}
// If set to true we ignore several UI behavior like confirmation popups as well dummy accounts are created and
// offers are filled with default values. Intended to make dev testing faster.
private static boolean devMode = false;
public static boolean isDevMode() {
return devMode;
}
public static void setDevMode(boolean devMode) {
DevEnv.devMode = devMode;
}
private static boolean daoActivated = true;
public static boolean isDaoActivated() {
return daoActivated;
}
public static void setDaoActivated(boolean daoActivated) {
DevEnv.daoActivated = daoActivated;
}
public static void logErrorAndThrowIfDevMode(String msg) {
log.error(msg);
if (devMode)
throw new RuntimeException(msg);
}
public static boolean isDaoTradingActivated() {
return true;
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common.app;
/**
* Holds a set of {@link Capabilities}.
*
* @author Florian Reimair
*/
public interface HasCapabilities {
Capabilities getCapabilities();
}

View file

@ -0,0 +1,92 @@
/*
* 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.common.app;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
import ch.qos.logback.core.util.FileSize;
public class Log {
private static Logger logbackLogger;
public static void setLevel(Level logLevel) {
logbackLogger.setLevel(logLevel);
}
public static void setup(String fileName) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
appender.setContext(loggerContext);
appender.setFile(fileName + ".log");
FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(loggerContext);
rollingPolicy.setParent(appender);
rollingPolicy.setFileNamePattern(fileName + "_%i.log");
rollingPolicy.setMinIndex(1);
rollingPolicy.setMaxIndex(20);
rollingPolicy.start();
SizeBasedTriggeringPolicy<ILoggingEvent> triggeringPolicy = new SizeBasedTriggeringPolicy<>();
triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB"));
triggeringPolicy.start();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n");
encoder.start();
appender.setEncoder(encoder);
appender.setRollingPolicy(rollingPolicy);
appender.setTriggeringPolicy(triggeringPolicy);
appender.start();
logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logbackLogger.addAppender(appender);
logbackLogger.setLevel(Level.INFO);
// log errors in separate file
// not working as expected still.... damn logback...
/* FileAppender errorAppender = new FileAppender();
errorAppender.setEncoder(encoder);
errorAppender.setName("Error");
errorAppender.setContext(loggerContext);
errorAppender.setFile(fileName + "_error.log");
LevelFilter levelFilter = new LevelFilter();
levelFilter.setLevel(Level.ERROR);
levelFilter.setOnMatch(FilterReply.ACCEPT);
levelFilter.setOnMismatch(FilterReply.DENY);
levelFilter.start();
errorAppender.addFilter(levelFilter);
errorAppender.start();
logbackLogger.addAppender(errorAppender);*/
}
public static void setCustomLogLevel(String pattern, Level logLevel) {
((Logger) LoggerFactory.getLogger(pattern)).setLevel(logLevel);
}
}

View file

@ -0,0 +1,145 @@
/*
* 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.common.app;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class Version {
// The application versions
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
// Therefore all sub versions start again with 1
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.6.2";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
* This must not contain each version but only those where we add new version-tagged resource files for
* historical data stores.
*/
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0", "1.5.2",
"1.5.5", "1.5.7", "1.6.0");
public static int getMajorVersion(String version) {
return getSubVersion(version, 0);
}
public static int getMinorVersion(String version) {
return getSubVersion(version, 1);
}
public static int getPatchVersion(String version) {
return getSubVersion(version, 2);
}
public static boolean isNewVersion(String newVersion) {
return isNewVersion(newVersion, VERSION);
}
public static boolean isNewVersion(String newVersion, String currentVersion) {
if (newVersion.equals(currentVersion))
return false;
else if (getMajorVersion(newVersion) > getMajorVersion(currentVersion))
return true;
else if (getMajorVersion(newVersion) < getMajorVersion(currentVersion))
return false;
else if (getMinorVersion(newVersion) > getMinorVersion(currentVersion))
return true;
else if (getMinorVersion(newVersion) < getMinorVersion(currentVersion))
return false;
else if (getPatchVersion(newVersion) > getPatchVersion(currentVersion))
return true;
else if (getPatchVersion(newVersion) < getPatchVersion(currentVersion))
return false;
else
return false;
}
private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
return Integer.parseInt(split[index]);
}
// The version no. for the objects sent over the network. A change will break the serialization of old objects.
// If objects are used for both network and database the network version is applied.
// VERSION = 0.5.0 -> P2P_NETWORK_VERSION = 1
// With version 1.2.2 we change to version 2 (new trade protocol)
public static final int P2P_NETWORK_VERSION = 1;
// The version no. of the serialized data stored to disc. A change will break the serialization of old objects.
// VERSION = 0.5.0 -> LOCAL_DB_VERSION = 1
public static final int LOCAL_DB_VERSION = 1;
// The version no. of the current protocol. The offer holds that version.
// A taker will check the version of the offers to see if his version is compatible.
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Bisq app.
// VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1
// Version 1.2.2 -> TRADE_PROTOCOL_VERSION = 2
// Version 1.5.0 -> TRADE_PROTOCOL_VERSION = 3
public static final int TRADE_PROTOCOL_VERSION = 3;
private static int p2pMessageVersion;
public static final String BSQ_TX_VERSION = "1";
public static int getP2PMessageVersion() {
return p2pMessageVersion;
}
// The version for the crypto network (BTC_Mainnet = 0, BTC_TestNet = 1, BTC_Regtest = 2, ...)
private static int BASE_CURRENCY_NETWORK;
public static void setBaseCryptoNetworkId(int baseCryptoNetworkId) {
BASE_CURRENCY_NETWORK = baseCryptoNetworkId;
// CRYPTO_NETWORK_ID is ordinal of enum. We use for changes at NETWORK_PROTOCOL_VERSION a multiplication with 10
// to not mix up networks:
p2pMessageVersion = BASE_CURRENCY_NETWORK + 10 * P2P_NETWORK_VERSION;
}
public static int getBaseCurrencyNetwork() {
return BASE_CURRENCY_NETWORK;
}
public static void printVersion() {
log.info("Version{" +
"VERSION=" + VERSION +
", P2P_NETWORK_VERSION=" + P2P_NETWORK_VERSION +
", LOCAL_DB_VERSION=" + LOCAL_DB_VERSION +
", TRADE_PROTOCOL_VERSION=" + TRADE_PROTOCOL_VERSION +
", BASE_CURRENCY_NETWORK=" + BASE_CURRENCY_NETWORK +
", getP2PNetworkId()=" + getP2PMessageVersion() +
'}');
}
public static final byte COMPENSATION_REQUEST = (byte) 0x01;
public static final byte REIMBURSEMENT_REQUEST = (byte) 0x01;
public static final byte PROPOSAL = (byte) 0x01;
public static final byte BLIND_VOTE = (byte) 0x01;
public static final byte VOTE_REVEAL = (byte) 0x01;
public static final byte LOCKUP = (byte) 0x01;
public static final byte ASSET_LISTING_FEE = (byte) 0x01;
public static final byte PROOF_OF_BURN = (byte) 0x01;
}

View file

@ -0,0 +1,78 @@
/*
* 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.common.config;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import lombok.Getter;
public enum BaseCurrencyNetwork {
BTC_MAINNET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"),
BTC_TESTNET(TestNet3Params.get(), "BTC", "TESTNET", "Bitcoin"),
BTC_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"),
BTC_DAO_TESTNET(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), // server side regtest until v0.9.5
BTC_DAO_BETANET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"), // mainnet test genesis
BTC_DAO_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"); // server side regtest after v0.9.5, had breaking code changes so we started over again
@Getter
private final NetworkParameters parameters;
@Getter
private final String currencyCode;
@Getter
private final String network;
@Getter
private final String currencyName;
BaseCurrencyNetwork(NetworkParameters parameters, String currencyCode, String network, String currencyName) {
this.parameters = parameters;
this.currencyCode = currencyCode;
this.network = network;
this.currencyName = currencyName;
}
public boolean isMainnet() {
return "BTC_MAINNET".equals(name());
}
public boolean isTestnet() {
return "BTC_TESTNET".equals(name());
}
public boolean isDaoTestNet() {
return "BTC_DAO_TESTNET".equals(name());
}
public boolean isDaoRegTest() {
return "BTC_DAO_REGTEST".equals(name());
}
public boolean isDaoBetaNet() {
return "BTC_DAO_BETANET".equals(name());
}
public boolean isRegtest() {
return "BTC_REGTEST".equals(name());
}
public long getDefaultMinFeePerVbyte() {
return 15; // 2021-02-22 due to mempool congestion, increased from 2
}
}

View file

@ -0,0 +1,131 @@
/*
* 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.common.config;
import joptsimple.HelpFormatter;
import joptsimple.OptionDescriptor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class BisqHelpFormatter implements HelpFormatter {
private final String fullName;
private final String scriptName;
private final String version;
public BisqHelpFormatter(String fullName, String scriptName, String version) {
this.fullName = fullName;
this.scriptName = scriptName;
this.version = version;
}
public String format(Map<String, ? extends OptionDescriptor> descriptors) {
StringBuilder output = new StringBuilder();
output.append(String.format("%s version %s\n\n", fullName, version));
output.append(String.format("Usage: %s [options]\n\n", scriptName));
output.append("Options:\n\n");
for (Map.Entry<String, ? extends OptionDescriptor> entry : descriptors.entrySet()) {
String optionName = entry.getKey();
OptionDescriptor optionDesc = entry.getValue();
if (optionDesc.representsNonOptions())
continue;
output.append(String.format("%s\n", formatOptionSyntax(optionName, optionDesc)));
output.append(String.format("%s\n", formatOptionDescription(optionDesc)));
}
return output.toString();
}
private String formatOptionSyntax(String optionName, OptionDescriptor optionDesc) {
StringBuilder result = new StringBuilder(String.format(" --%s", optionName));
if (optionDesc.acceptsArguments())
result.append(String.format("=<%s>", formatArgDescription(optionDesc)));
List<?> defaultValues = optionDesc.defaultValues();
if (defaultValues.size() > 0)
result.append(String.format(" (default: %s)", formatDefaultValues(defaultValues)));
return result.toString();
}
private String formatArgDescription(OptionDescriptor optionDesc) {
String argDescription = optionDesc.argumentDescription();
if (argDescription.length() > 0)
return argDescription;
String typeIndicator = optionDesc.argumentTypeIndicator();
if (typeIndicator == null)
return "value";
try {
Class<?> type = Class.forName(typeIndicator);
return type.isEnum() ?
Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(Collectors.joining("|")) :
typeIndicator.substring(typeIndicator.lastIndexOf('.') + 1);
} catch (ClassNotFoundException ex) {
// typeIndicator is something other than a class name, which can occur
// in certain cases e.g. where OptionParser.withValuesConvertedBy is used.
return typeIndicator;
}
}
private Object formatDefaultValues(List<?> defaultValues) {
return defaultValues.size() == 1 ?
defaultValues.get(0) :
defaultValues.toString();
}
private String formatOptionDescription(OptionDescriptor optionDesc) {
StringBuilder output = new StringBuilder();
String remainder = optionDesc.description().trim();
// Wrap description text at 80 characters with 8 spaces of indentation and a
// maximum of 72 chars of text, wrapping on spaces. Strings longer than 72 chars
// without any spaces (e.g. a URL) are allowed to overflow the 80-char margin.
while (remainder.length() > 72) {
int idxFirstSpace = remainder.indexOf(' ');
int chunkLen = idxFirstSpace == -1 ? remainder.length() : Math.max(idxFirstSpace, 73);
String chunk = remainder.substring(0, chunkLen);
int idxLastSpace = chunk.lastIndexOf(' ');
int idxBreak = idxLastSpace > 0 ? idxLastSpace : chunk.length();
String line = remainder.substring(0, idxBreak);
output.append(formatLine(line));
remainder = remainder.substring(chunk.length() - (chunk.length() - idxBreak)).trim();
}
if (remainder.length() > 0)
output.append(formatLine(remainder));
return output.toString();
}
private String formatLine(String line) {
return String.format(" %s\n", line.trim());
}
}

View file

@ -0,0 +1,58 @@
package bisq.common.config;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/**
* Composes multiple JOptSimple {@link OptionSet} instances such that calls to
* {@link #valueOf(OptionSpec)} and co will search all instances in the order they were
* added and return any value explicitly set, otherwise returning the default value for
* the given option or null if no default has been set. The API found here loosely
* emulates the {@link OptionSet} API without going through the unnecessary work of
* actually extending it. In practice, this class is used to compose options provided at
* the command line with those provided via config file, such that those provided at the
* command line take precedence over those provided in the config file.
*/
@VisibleForTesting
public class CompositeOptionSet {
private final List<OptionSet> optionSets = new ArrayList<>();
public void addOptionSet(OptionSet optionSet) {
optionSets.add(optionSet);
}
public boolean has(OptionSpec<?> option) {
for (OptionSet optionSet : optionSets)
if (optionSet.has(option))
return true;
return false;
}
public <V> V valueOf(OptionSpec<V> option) {
for (OptionSet optionSet : optionSets)
if (optionSet.has(option))
return optionSet.valueOf(option);
// None of the provided option sets specified the given option so fall back to
// the default value (if any) provided by the first specified OptionSet
return optionSets.get(0).valueOf(option);
}
public List<String> valuesOf(ArgumentAcceptingOptionSpec<String> option) {
for (OptionSet optionSet : optionSets)
if (optionSet.has(option))
return optionSet.valuesOf(option);
// None of the provided option sets specified the given option so fall back to
// the default value (if any) provided by the first specified OptionSet
return optionSets.get(0).valuesOf(option);
}
}

View file

@ -0,0 +1,944 @@
package bisq.common.config;
import org.bitcoinj.core.NetworkParameters;
import joptsimple.AbstractOptionSpec;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.HelpFormatter;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.OptionSpecBuilder;
import joptsimple.util.PathConverter;
import joptsimple.util.PathProperties;
import joptsimple.util.RegexMatcher;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Optional;
import ch.qos.logback.classic.Level;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
/**
* Parses and provides access to all Bisq configuration options specified at the command
* line and/or via the {@value DEFAULT_CONFIG_FILE_NAME} config file, including any
* default values. Constructing a {@link Config} instance is generally side-effect free,
* with one key exception being that {@value APP_DATA_DIR} and its subdirectories will
* be created if they do not already exist. Care is taken to avoid inadvertent creation or
* modification of the actual system user data directory and/or the production Bisq
* application data directory. Calling code must explicitly specify these values; they are
* never assumed.
* <p/>
* Note that this class deviates from typical JavaBean conventions in that fields
* representing configuration options are public and have no corresponding accessor
* ("getter") method. This is because all such fields are final and therefore not subject
* to modification by calling code and because eliminating the accessor methods means
* eliminating hundreds of lines of boilerplate code and one less touchpoint to deal with
* when adding or modifying options. Furthermore, while accessor methods are often useful
* when mocking an object in a testing context, this class is designed for testability
* without needing to be mocked. See {@code ConfigTests} for examples.
* @see #Config(String...)
* @see #Config(String, File, String...)
*/
public class Config {
// Option name constants
public static final String HELP = "help";
public static final String APP_NAME = "appName";
public static final String USER_DATA_DIR = "userDataDir";
public static final String APP_DATA_DIR = "appDataDir";
public static final String CONFIG_FILE = "configFile";
public static final String MAX_MEMORY = "maxMemory";
public static final String LOG_LEVEL = "logLevel";
public static final String BANNED_BTC_NODES = "bannedBtcNodes";
public static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes";
public static final String BANNED_SEED_NODES = "bannedSeedNodes";
public static final String BASE_CURRENCY_NETWORK = "baseCurrencyNetwork";
public static final String REFERRAL_ID = "referralId";
public static final String USE_DEV_MODE = "useDevMode";
public static final String USE_DEV_MODE_HEADER = "useDevModeHeader";
public static final String TOR_DIR = "torDir";
public static final String STORAGE_DIR = "storageDir";
public static final String KEY_STORAGE_DIR = "keyStorageDir";
public static final String WALLET_DIR = "walletDir";
public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys";
public static final String DUMP_STATISTICS = "dumpStatistics";
public static final String IGNORE_DEV_MSG = "ignoreDevMsg";
public static final String PROVIDERS = "providers";
public static final String SEED_NODES = "seedNodes";
public static final String BAN_LIST = "banList";
public static final String NODE_PORT = "nodePort";
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
public static final String MAX_CONNECTIONS = "maxConnections";
public static final String SOCKS_5_PROXY_BTC_ADDRESS = "socks5ProxyBtcAddress";
public static final String SOCKS_5_PROXY_HTTP_ADDRESS = "socks5ProxyHttpAddress";
public static final String USE_TOR_FOR_BTC = "useTorForBtc";
public static final String TORRC_FILE = "torrcFile";
public static final String TORRC_OPTIONS = "torrcOptions";
public static final String TOR_CONTROL_PORT = "torControlPort";
public static final String TOR_CONTROL_PASSWORD = "torControlPassword";
public static final String TOR_CONTROL_COOKIE_FILE = "torControlCookieFile";
public static final String TOR_CONTROL_USE_SAFE_COOKIE_AUTH = "torControlUseSafeCookieAuth";
public static final String TOR_STREAM_ISOLATION = "torStreamIsolation";
public static final String MSG_THROTTLE_PER_SEC = "msgThrottlePerSec";
public static final String MSG_THROTTLE_PER_10_SEC = "msgThrottlePer10Sec";
public static final String SEND_MSG_THROTTLE_TRIGGER = "sendMsgThrottleTrigger";
public static final String SEND_MSG_THROTTLE_SLEEP = "sendMsgThrottleSleep";
public static final String IGNORE_LOCAL_BTC_NODE = "ignoreLocalBtcNode";
public static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost";
public static final String BTC_NODES = "btcNodes";
public static final String SOCKS5_DISCOVER_MODE = "socks5DiscoverMode";
public static final String USE_ALL_PROVIDED_NODES = "useAllProvidedNodes";
public static final String USER_AGENT = "userAgent";
public static final String NUM_CONNECTIONS_FOR_BTC = "numConnectionsForBtc";
public static final String RPC_USER = "rpcUser";
public static final String RPC_PASSWORD = "rpcPassword";
public static final String RPC_HOST = "rpcHost";
public static final String RPC_PORT = "rpcPort";
public static final String RPC_BLOCK_NOTIFICATION_PORT = "rpcBlockNotificationPort";
public static final String RPC_BLOCK_NOTIFICATION_HOST = "rpcBlockNotificationHost";
public static final String DUMP_BLOCKCHAIN_DATA = "dumpBlockchainData";
public static final String FULL_DAO_NODE = "fullDaoNode";
public static final String GENESIS_TX_ID = "genesisTxId";
public static final String GENESIS_BLOCK_HEIGHT = "genesisBlockHeight";
public static final String GENESIS_TOTAL_SUPPLY = "genesisTotalSupply";
public static final String DAO_ACTIVATED = "daoActivated";
public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs";
public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs";
public static final String API_PASSWORD = "apiPassword";
public static final String API_PORT = "apiPort";
public static final String PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE = "preventPeriodicShutdownAtSeedNode";
public static final String REPUBLISH_MAILBOX_ENTRIES = "republishMailboxEntries";
public static final String BTC_TX_FEE = "btcTxFee";
public static final String BTC_MIN_TX_FEE = "btcMinTxFee";
public static final String BTC_FEES_TS = "bitcoinFeesTs";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
public static final String DEFAULT_REGTEST_HOST = "localhost";
public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12
public static final boolean DEFAULT_FULL_DAO_NODE = false;
static final String DEFAULT_CONFIG_FILE_NAME = "bisq.properties";
// Static fields that provide access to Config properties in locations where injecting
// a Config instance is not feasible. See Javadoc for corresponding static accessors.
private static File APP_DATA_DIR_VALUE;
private static BaseCurrencyNetwork BASE_CURRENCY_NETWORK_VALUE = BaseCurrencyNetwork.BTC_MAINNET;
// Default "data dir properties", i.e. properties that can determine the location of
// Bisq's application data directory (appDataDir)
public final String defaultAppName;
public final File defaultUserDataDir;
public final File defaultAppDataDir;
public final File defaultConfigFile;
// Options supported only at the command-line interface (cli)
public final boolean helpRequested;
public final File configFile;
// Options supported on cmd line and in the config file
public final String appName;
public final File userDataDir;
public final File appDataDir;
public final int nodePort;
public final int maxMemory;
public final String logLevel;
public final List<String> bannedBtcNodes;
public final List<String> bannedPriceRelayNodes;
public final List<String> bannedSeedNodes;
public final BaseCurrencyNetwork baseCurrencyNetwork;
public final NetworkParameters networkParameters;
public final boolean ignoreLocalBtcNode;
public final String bitcoinRegtestHost;
public final boolean daoActivated;
public final String referralId;
public final boolean useDevMode;
public final boolean useDevModeHeader;
public final boolean useDevPrivilegeKeys;
public final boolean dumpStatistics;
public final boolean ignoreDevMsg;
public final List<String> providers;
public final List<String> seedNodes;
public final List<String> banList;
public final boolean useLocalhostForP2P;
public final int maxConnections;
public final String socks5ProxyBtcAddress;
public final String socks5ProxyHttpAddress;
public final File torrcFile;
public final String torrcOptions;
public final int torControlPort;
public final String torControlPassword;
public final File torControlCookieFile;
public final boolean useTorControlSafeCookieAuth;
public final boolean torStreamIsolation;
public final int msgThrottlePerSec;
public final int msgThrottlePer10Sec;
public final int sendMsgThrottleTrigger;
public final int sendMsgThrottleSleep;
public final String btcNodes;
public final boolean useTorForBtc;
public final boolean useTorForBtcOptionSetExplicitly;
public final String socks5DiscoverMode;
public final boolean useAllProvidedNodes;
public final String userAgent;
public final int numConnectionsForBtc;
public final String rpcUser;
public final String rpcPassword;
public final String rpcHost;
public final int rpcPort;
public final int rpcBlockNotificationPort;
public final String rpcBlockNotificationHost;
public final boolean dumpBlockchainData;
public final boolean fullDaoNode;
public final boolean fullDaoNodeOptionSetExplicitly;
public final String genesisTxId;
public final int genesisBlockHeight;
public final long genesisTotalSupply;
public final boolean dumpDelayedPayoutTxs;
public final boolean allowFaultyDelayedTxs;
public final String apiPassword;
public final int apiPort;
public final boolean preventPeriodicShutdownAtSeedNode;
public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation;
// Properties derived from options but not exposed as options themselves
public final File torDir;
public final File walletDir;
public final File storageDir;
public final File keyStorageDir;
// The parser that will be used to parse both cmd line and config file options
private final OptionParser parser = new OptionParser();
/**
* Create a new {@link Config} instance using a randomly-generated default
* {@value APP_NAME} and a newly-created temporary directory as the default
* {@value USER_DATA_DIR} along with any command line arguments. This constructor is
* primarily useful in test code, where no references or modifications should be made
* to the actual system user data directory and/or real Bisq application data
* directory. Most production use cases will favor calling the
* {@link #Config(String, File, String...)} constructor directly.
* @param args zero or more command line arguments in the form "--optName=optValue"
* @throws ConfigException if any problems are encountered during option parsing
* @see #Config(String, File, String...)
*/
public Config(String... args) {
this(randomAppName(), tempUserDataDir(), args);
}
/**
* Create a new {@link Config} instance with the given default {@value APP_NAME} and
* {@value USER_DATA_DIR} values along with any command line arguments, typically
* those supplied via a Bisq application's main() method.
* <p/>
* This constructor performs all parsing of command line options and config file
* options, assuming the default config file exists or a custom config file has been
* specified via the {@value CONFIG_FILE} option and exists. For any options that
* are present both at the command line and in the config file, the command line value
* will take precedence. Note that the {@value HELP} and {@value CONFIG_FILE} options
* are supported only at the command line and are disallowed within the config file
* itself.
* @param defaultAppName typically "Bisq" or similar
* @param defaultUserDataDir typically the OS-specific user data directory location
* @param args zero or more command line arguments in the form "--optName=optValue"
* @throws ConfigException if any problems are encountered during option parsing
*/
public Config(String defaultAppName, File defaultUserDataDir, String... args) {
this.defaultAppName = defaultAppName;
this.defaultUserDataDir = defaultUserDataDir;
this.defaultAppDataDir = new File(defaultUserDataDir, defaultAppName);
this.defaultConfigFile = absoluteConfigFile(defaultAppDataDir, DEFAULT_CONFIG_FILE_NAME);
AbstractOptionSpec<Void> helpOpt =
parser.accepts(HELP, "Print this help text")
.forHelp();
ArgumentAcceptingOptionSpec<String> configFileOpt =
parser.accepts(CONFIG_FILE, format("Specify configuration file. " +
"Relative paths will be prefixed by %s location.", APP_DATA_DIR))
.withRequiredArg()
.ofType(String.class)
.defaultsTo(DEFAULT_CONFIG_FILE_NAME);
ArgumentAcceptingOptionSpec<String> appNameOpt =
parser.accepts(APP_NAME, "Application name")
.withRequiredArg()
.ofType(String.class)
.defaultsTo(this.defaultAppName);
ArgumentAcceptingOptionSpec<File> userDataDirOpt =
parser.accepts(USER_DATA_DIR, "User data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(this.defaultUserDataDir);
ArgumentAcceptingOptionSpec<File> appDataDirOpt =
parser.accepts(APP_DATA_DIR, "Application data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(defaultAppDataDir);
ArgumentAcceptingOptionSpec<Integer> nodePortOpt =
parser.accepts(NODE_PORT, "Port to listen on")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(9999);
ArgumentAcceptingOptionSpec<Integer> maxMemoryOpt =
parser.accepts(MAX_MEMORY, "Max. permitted memory (used only by headless versions)")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(1200);
ArgumentAcceptingOptionSpec<String> logLevelOpt =
parser.accepts(LOG_LEVEL, "Set logging level")
.withRequiredArg()
.ofType(String.class)
.describedAs("OFF|ALL|ERROR|WARN|INFO|DEBUG|TRACE")
.defaultsTo(Level.INFO.levelStr);
ArgumentAcceptingOptionSpec<String> bannedBtcNodesOpt =
parser.accepts(BANNED_BTC_NODES, "List Bitcoin nodes to ban")
.withRequiredArg()
.ofType(String.class)
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
ArgumentAcceptingOptionSpec<String> bannedPriceRelayNodesOpt =
parser.accepts(BANNED_PRICE_RELAY_NODES, "List Bisq price nodes to ban")
.withRequiredArg()
.ofType(String.class)
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
ArgumentAcceptingOptionSpec<String> bannedSeedNodesOpt =
parser.accepts(BANNED_SEED_NODES, "List Bisq seed nodes to ban")
.withRequiredArg()
.ofType(String.class)
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
//noinspection rawtypes
ArgumentAcceptingOptionSpec<Enum> baseCurrencyNetworkOpt =
parser.accepts(BASE_CURRENCY_NETWORK, "Base currency network")
.withRequiredArg()
.ofType(BaseCurrencyNetwork.class)
.withValuesConvertedBy(new EnumValueConverter(BaseCurrencyNetwork.class))
.defaultsTo(BaseCurrencyNetwork.BTC_MAINNET);
ArgumentAcceptingOptionSpec<Boolean> ignoreLocalBtcNodeOpt =
parser.accepts(IGNORE_LOCAL_BTC_NODE,
"If set to true a Bitcoin Core node running locally will be ignored")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> bitcoinRegtestHostOpt =
parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core node when using BTC_REGTEST network")
.withRequiredArg()
.ofType(String.class)
.describedAs("host[:port]")
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> referralIdOpt =
parser.accepts(REFERRAL_ID, "Optional Referral ID (e.g. for API users or pro market makers)")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> useDevModeOpt =
parser.accepts(USE_DEV_MODE,
"Enables dev mode which is used for convenience for developer testing")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> useDevModeHeaderOpt =
parser.accepts(USE_DEV_MODE_HEADER,
"Use dev mode css scheme to distinguish dev instances.")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> useDevPrivilegeKeysOpt =
parser.accepts(USE_DEV_PRIVILEGE_KEYS, "If set to true all privileged features requiring a private " +
"key to be enabled are overridden by a dev key pair (This is for developers only!)")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> dumpStatisticsOpt =
parser.accepts(DUMP_STATISTICS, "If set to true dump trade statistics to a json file in appDataDir")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> ignoreDevMsgOpt =
parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " +
"network_messages from bisq developers are ignored (Global " +
"alert, Version update alert, Filters for offers, nodes or " +
"trading account data)")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> providersOpt =
parser.accepts(PROVIDERS, "List custom pricenodes")
.withRequiredArg()
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
ArgumentAcceptingOptionSpec<String> seedNodesOpt =
parser.accepts(SEED_NODES, "Override hard coded seed nodes as comma separated list e.g. " +
"'rxdkppp3vicnbgqt.onion:8002,mfla72c4igh5ta2t.onion:8002'")
.withRequiredArg()
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
ArgumentAcceptingOptionSpec<String> banListOpt =
parser.accepts(BAN_LIST, "Nodes to exclude from network connections.")
.withRequiredArg()
.withValuesSeparatedBy(',')
.describedAs("host:port[,...]");
ArgumentAcceptingOptionSpec<Boolean> useLocalhostForP2POpt =
parser.accepts(USE_LOCALHOST_FOR_P2P, "Use localhost P2P network for development. Only available for non-BTC_MAINNET configuration.")
.availableIf(BASE_CURRENCY_NETWORK)
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Integer> maxConnectionsOpt =
parser.accepts(MAX_CONNECTIONS, "Max. connections a peer will try to keep")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(12);
ArgumentAcceptingOptionSpec<String> socks5ProxyBtcAddressOpt =
parser.accepts(SOCKS_5_PROXY_BTC_ADDRESS, "A proxy address to be used for Bitcoin network.")
.withRequiredArg()
.describedAs("host:port")
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> socks5ProxyHttpAddressOpt =
parser.accepts(SOCKS_5_PROXY_HTTP_ADDRESS,
"A proxy address to be used for Http requests (should be non-Tor)")
.withRequiredArg()
.describedAs("host:port")
.defaultsTo("");
ArgumentAcceptingOptionSpec<Path> torrcFileOpt =
parser.accepts(TORRC_FILE, "An existing torrc-file to be sourced for Tor. Note that torrc-entries, " +
"which are critical to Bisq's correct operation, cannot be overwritten.")
.withRequiredArg()
.describedAs("File")
.withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE));
ArgumentAcceptingOptionSpec<String> torrcOptionsOpt =
parser.accepts(TORRC_OPTIONS, "A list of torrc-entries to amend to Bisq's torrc. Note that " +
"torrc-entries, which are critical to Bisq's flawless operation, cannot be overwritten. " +
"[torrc options line, torrc option, ...]")
.withRequiredArg()
.withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$"))
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> torControlPortOpt =
parser.accepts(TOR_CONTROL_PORT,
"The control port of an already running Tor service to be used by Bisq.")
.availableUnless(TORRC_FILE, TORRC_OPTIONS)
.withRequiredArg()
.ofType(int.class)
.describedAs("port")
.defaultsTo(UNSPECIFIED_PORT);
ArgumentAcceptingOptionSpec<String> torControlPasswordOpt =
parser.accepts(TOR_CONTROL_PASSWORD, "The password for controlling the already running Tor service.")
.availableIf(TOR_CONTROL_PORT)
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<Path> torControlCookieFileOpt =
parser.accepts(TOR_CONTROL_COOKIE_FILE, "The cookie file for authenticating against the already " +
"running Tor service. Use in conjunction with --" + TOR_CONTROL_USE_SAFE_COOKIE_AUTH)
.availableIf(TOR_CONTROL_PORT)
.availableUnless(TOR_CONTROL_PASSWORD)
.withRequiredArg()
.describedAs("File")
.withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE));
OptionSpecBuilder torControlUseSafeCookieAuthOpt =
parser.accepts(TOR_CONTROL_USE_SAFE_COOKIE_AUTH,
"Use the SafeCookie method when authenticating to the already running Tor service.")
.availableIf(TOR_CONTROL_COOKIE_FILE);
OptionSpecBuilder torStreamIsolationOpt =
parser.accepts(TOR_STREAM_ISOLATION, "Use stream isolation for Tor [experimental!].");
ArgumentAcceptingOptionSpec<Integer> msgThrottlePerSecOpt =
parser.accepts(MSG_THROTTLE_PER_SEC, "Message throttle per sec for connection class")
.withRequiredArg()
.ofType(int.class)
// With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 40MB/sec or 5 mbit/sec
.defaultsTo(200);
ArgumentAcceptingOptionSpec<Integer> msgThrottlePer10SecOpt =
parser.accepts(MSG_THROTTLE_PER_10_SEC, "Message throttle per 10 sec for connection class")
.withRequiredArg()
.ofType(int.class)
// With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 20MB/sec or 2.5 mbit/sec
.defaultsTo(1000);
ArgumentAcceptingOptionSpec<Integer> sendMsgThrottleTriggerOpt =
parser.accepts(SEND_MSG_THROTTLE_TRIGGER, "Time in ms when we trigger a sleep if 2 messages are sent")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(20); // Time in ms when we trigger a sleep if 2 messages are sent
ArgumentAcceptingOptionSpec<Integer> sendMsgThrottleSleepOpt =
parser.accepts(SEND_MSG_THROTTLE_SLEEP, "Pause in ms to sleep if we get too many messages to send")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(50); // Pause in ms to sleep if we get too many messages to send
ArgumentAcceptingOptionSpec<String> btcNodesOpt =
parser.accepts(BTC_NODES, "Custom nodes used for BitcoinJ as comma separated IP addresses.")
.withRequiredArg()
.describedAs("ip[,...]")
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> useTorForBtcOpt =
parser.accepts(USE_TOR_FOR_BTC, "If set to true BitcoinJ is routed over tor (socks 5 proxy).")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> socks5DiscoverModeOpt =
parser.accepts(SOCKS5_DISCOVER_MODE, "Specify discovery mode for Bitcoin nodes. " +
"One or more of: [ADDR, DNS, ONION, ALL] (comma separated, they get OR'd together).")
.withRequiredArg()
.describedAs("mode[,...]")
.defaultsTo("ALL");
ArgumentAcceptingOptionSpec<Boolean> useAllProvidedNodesOpt =
parser.accepts(USE_ALL_PROVIDED_NODES,
"Set to true if connection of bitcoin nodes should include clear net nodes")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> userAgentOpt =
parser.accepts(USER_AGENT,
"User agent at btc node connections")
.withRequiredArg()
.defaultsTo("Bisq");
ArgumentAcceptingOptionSpec<Integer> numConnectionsForBtcOpt =
parser.accepts(NUM_CONNECTIONS_FOR_BTC, "Number of connections to the Bitcoin network")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(DEFAULT_NUM_CONNECTIONS_FOR_BTC);
ArgumentAcceptingOptionSpec<String> rpcUserOpt =
parser.accepts(RPC_USER, "Bitcoind rpc username")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> rpcPasswordOpt =
parser.accepts(RPC_PASSWORD, "Bitcoind rpc password")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> rpcHostOpt =
parser.accepts(RPC_HOST, "Bitcoind rpc host")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> rpcPortOpt =
parser.accepts(RPC_PORT, "Bitcoind rpc port")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(UNSPECIFIED_PORT);
ArgumentAcceptingOptionSpec<Integer> rpcBlockNotificationPortOpt =
parser.accepts(RPC_BLOCK_NOTIFICATION_PORT, "Bitcoind rpc port for block notifications")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(UNSPECIFIED_PORT);
ArgumentAcceptingOptionSpec<String> rpcBlockNotificationHostOpt =
parser.accepts(RPC_BLOCK_NOTIFICATION_HOST,
"Bitcoind rpc accepted incoming host for block notifications")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> dumpBlockchainDataOpt =
parser.accepts(DUMP_BLOCKCHAIN_DATA, "If set to true the blockchain data " +
"from RPC requests to Bitcoin Core are stored as json file in the data dir.")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> fullDaoNodeOpt =
parser.accepts(FULL_DAO_NODE, "If set to true the node requests the blockchain data via RPC requests " +
"from Bitcoin Core and provide the validated BSQ txs to the network. It requires that the " +
"other RPC properties are set as well.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(DEFAULT_FULL_DAO_NODE);
ArgumentAcceptingOptionSpec<String> genesisTxIdOpt =
parser.accepts(GENESIS_TX_ID, "Genesis transaction ID when not using the hard coded one")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> genesisBlockHeightOpt =
parser.accepts(GENESIS_BLOCK_HEIGHT,
"Genesis transaction block height when not using the hard coded one")
.withRequiredArg()
.ofType(int.class)
.defaultsTo(-1);
ArgumentAcceptingOptionSpec<Long> genesisTotalSupplyOpt =
parser.accepts(GENESIS_TOTAL_SUPPLY, "Genesis total supply when not using the hard coded one")
.withRequiredArg()
.ofType(long.class)
.defaultsTo(-1L);
ArgumentAcceptingOptionSpec<Boolean> daoActivatedOpt =
parser.accepts(DAO_ACTIVATED, "Developer flag. If true it enables dao phase 2 features.")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(true);
ArgumentAcceptingOptionSpec<Boolean> dumpDelayedPayoutTxsOpt =
parser.accepts(DUMP_DELAYED_PAYOUT_TXS, "Dump delayed payout transactions to file")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> allowFaultyDelayedTxsOpt =
parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " +
"payout transactions")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> apiPasswordOpt =
parser.accepts(API_PASSWORD, "gRPC API password")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> apiPortOpt =
parser.accepts(API_PORT, "gRPC API port")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(9998);
ArgumentAcceptingOptionSpec<Boolean> preventPeriodicShutdownAtSeedNodeOpt =
parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE,
"Prevents periodic shutdown at seed nodes")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> republishMailboxEntriesOpt =
parser.accepts(REPUBLISH_MAILBOX_ENTRIES,
"Republish mailbox messages at startup")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> bypassMempoolValidationOpt =
parser.accepts(BYPASS_MEMPOOL_VALIDATION,
"Prevents mempool check of trade parameters")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
try {
CompositeOptionSet options = new CompositeOptionSet();
// Parse command line options
OptionSet cliOpts = parser.parse(args);
options.addOptionSet(cliOpts);
// Option parsing is strict at the command line, but we relax it now for any
// subsequent config file processing. This is for compatibility with pre-1.2.6
// versions that allowed unrecognized options in the bisq.properties config
// file and because it follows suit with Bitcoin Core's config file behavior.
parser.allowsUnrecognizedOptions();
// Parse config file specified at the command line only if it was specified as
// an absolute path. Otherwise, the config file will be processed later below.
File configFile = null;
OptionSpec<?>[] disallowedOpts = new OptionSpec<?>[]{helpOpt, configFileOpt};
final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt);
boolean configFileHasBeenProcessed = false;
if (cliHasConfigFileOpt) {
configFile = new File(cliOpts.valueOf(configFileOpt));
if (configFile.isAbsolute()) {
Optional<OptionSet> configFileOpts = parseOptionsFrom(configFile, disallowedOpts);
if (configFileOpts.isPresent()) {
options.addOptionSet(configFileOpts.get());
configFileHasBeenProcessed = true;
}
}
}
// Assign values to the following "data dir properties". If a
// relatively-pathed config file was specified at the command line, any
// entries it has for these options will be ignored, as it has not been
// processed yet.
this.appName = options.valueOf(appNameOpt);
this.userDataDir = options.valueOf(userDataDirOpt);
this.appDataDir = mkAppDataDir(options.has(appDataDirOpt) ?
options.valueOf(appDataDirOpt) :
new File(userDataDir, appName));
// If the config file has not yet been processed, either because a relative
// path was provided at the command line, or because no value was provided at
// the command line, attempt to process the file now, falling back to the
// default config file location if none was specified at the command line.
if (!configFileHasBeenProcessed) {
configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ?
absoluteConfigFile(appDataDir, configFile.getPath()) :
absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME);
Optional<OptionSet> configFileOpts = parseOptionsFrom(configFile, disallowedOpts);
configFileOpts.ifPresent(options::addOptionSet);
}
// Assign all remaining properties, with command line options taking
// precedence over those provided in the config file (if any)
this.helpRequested = options.has(helpOpt);
this.configFile = configFile;
this.nodePort = options.valueOf(nodePortOpt);
this.maxMemory = options.valueOf(maxMemoryOpt);
this.logLevel = options.valueOf(logLevelOpt);
this.bannedBtcNodes = options.valuesOf(bannedBtcNodesOpt);
this.bannedPriceRelayNodes = options.valuesOf(bannedPriceRelayNodesOpt);
this.bannedSeedNodes = options.valuesOf(bannedSeedNodesOpt);
this.baseCurrencyNetwork = (BaseCurrencyNetwork) options.valueOf(baseCurrencyNetworkOpt);
this.networkParameters = baseCurrencyNetwork.getParameters();
this.ignoreLocalBtcNode = options.valueOf(ignoreLocalBtcNodeOpt);
this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt);
this.torrcFile = options.has(torrcFileOpt) ? options.valueOf(torrcFileOpt).toFile() : null;
this.torrcOptions = options.valueOf(torrcOptionsOpt);
this.torControlPort = options.valueOf(torControlPortOpt);
this.torControlPassword = options.valueOf(torControlPasswordOpt);
this.torControlCookieFile = options.has(torControlCookieFileOpt) ?
options.valueOf(torControlCookieFileOpt).toFile() : null;
this.useTorControlSafeCookieAuth = options.has(torControlUseSafeCookieAuthOpt);
this.torStreamIsolation = options.has(torStreamIsolationOpt);
this.referralId = options.valueOf(referralIdOpt);
this.useDevMode = options.valueOf(useDevModeOpt);
this.useDevModeHeader = options.valueOf(useDevModeHeaderOpt);
this.useDevPrivilegeKeys = options.valueOf(useDevPrivilegeKeysOpt);
this.dumpStatistics = options.valueOf(dumpStatisticsOpt);
this.ignoreDevMsg = options.valueOf(ignoreDevMsgOpt);
this.providers = options.valuesOf(providersOpt);
this.seedNodes = options.valuesOf(seedNodesOpt);
this.banList = options.valuesOf(banListOpt);
this.useLocalhostForP2P = !this.baseCurrencyNetwork.isMainnet() && options.valueOf(useLocalhostForP2POpt);
this.maxConnections = options.valueOf(maxConnectionsOpt);
this.socks5ProxyBtcAddress = options.valueOf(socks5ProxyBtcAddressOpt);
this.socks5ProxyHttpAddress = options.valueOf(socks5ProxyHttpAddressOpt);
this.msgThrottlePerSec = options.valueOf(msgThrottlePerSecOpt);
this.msgThrottlePer10Sec = options.valueOf(msgThrottlePer10SecOpt);
this.sendMsgThrottleTrigger = options.valueOf(sendMsgThrottleTriggerOpt);
this.sendMsgThrottleSleep = options.valueOf(sendMsgThrottleSleepOpt);
this.btcNodes = options.valueOf(btcNodesOpt);
this.useTorForBtc = options.valueOf(useTorForBtcOpt);
this.useTorForBtcOptionSetExplicitly = options.has(useTorForBtcOpt);
this.socks5DiscoverMode = options.valueOf(socks5DiscoverModeOpt);
this.useAllProvidedNodes = options.valueOf(useAllProvidedNodesOpt);
this.userAgent = options.valueOf(userAgentOpt);
this.numConnectionsForBtc = options.valueOf(numConnectionsForBtcOpt);
this.rpcUser = options.valueOf(rpcUserOpt);
this.rpcPassword = options.valueOf(rpcPasswordOpt);
this.rpcHost = options.valueOf(rpcHostOpt);
this.rpcPort = options.valueOf(rpcPortOpt);
this.rpcBlockNotificationPort = options.valueOf(rpcBlockNotificationPortOpt);
this.rpcBlockNotificationHost = options.valueOf(rpcBlockNotificationHostOpt);
this.dumpBlockchainData = options.valueOf(dumpBlockchainDataOpt);
this.fullDaoNode = options.valueOf(fullDaoNodeOpt);
this.fullDaoNodeOptionSetExplicitly = options.has(fullDaoNodeOpt);
this.genesisTxId = options.valueOf(genesisTxIdOpt);
this.genesisBlockHeight = options.valueOf(genesisBlockHeightOpt);
this.genesisTotalSupply = options.valueOf(genesisTotalSupplyOpt);
this.daoActivated = options.valueOf(daoActivatedOpt);
this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt);
this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt);
this.apiPassword = options.valueOf(apiPasswordOpt);
this.apiPort = options.valueOf(apiPortOpt);
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
ex.getCause() != null ?
ex.getCause().getMessage() :
ex.getMessage());
}
// Create all appDataDir subdirectories and assign to their respective properties
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(btcNetworkDir, "keys");
this.storageDir = mkdir(btcNetworkDir, "db");
this.torDir = mkdir(btcNetworkDir, "tor");
this.walletDir = mkdir(btcNetworkDir, "wallet");
// Assign values to special-case static fields
APP_DATA_DIR_VALUE = appDataDir;
BASE_CURRENCY_NETWORK_VALUE = baseCurrencyNetwork;
}
private static File absoluteConfigFile(File parentDir, String relativeConfigFilePath) {
return new File(parentDir, relativeConfigFilePath);
}
private Optional<OptionSet> parseOptionsFrom(File configFile, OptionSpec<?>[] disallowedOpts) {
if (!configFile.exists()) {
if (!configFile.equals(absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME)))
throw new ConfigException("The specified config file '%s' does not exist.", configFile);
return Optional.empty();
}
ConfigFileReader configFileReader = new ConfigFileReader(configFile);
String[] optionLines = configFileReader.getOptionLines().stream()
.map(o -> "--" + o) // prepend dashes expected by jopt parser below
.collect(toList())
.toArray(new String[]{});
OptionSet configFileOpts = parser.parse(optionLines);
for (OptionSpec<?> disallowedOpt : disallowedOpts)
if (configFileOpts.has(disallowedOpt))
throw new ConfigException("The '%s' option is disallowed in config files",
disallowedOpt.options().get(0));
return Optional.of(configFileOpts);
}
public void printHelp(OutputStream sink, HelpFormatter formatter) {
try {
parser.formatHelpWith(formatter);
parser.printHelpOn(sink);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
// == STATIC UTILS ===================================================================
private static String randomAppName() {
try {
File file = Files.createTempFile("Bisq", "Temp").toFile();
//noinspection ResultOfMethodCallIgnored
file.delete();
return file.toPath().getFileName().toString();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static File tempUserDataDir() {
try {
return Files.createTempDirectory("BisqTempUserData").toFile();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Creates {@value APP_DATA_DIR} including any nonexistent parent directories. Does
* nothing if the directory already exists.
* @return the given directory, now guaranteed to exist
*/
private static File mkAppDataDir(File dir) {
if (!dir.exists()) {
try {
Files.createDirectories(dir.toPath());
} catch (IOException ex) {
throw new UncheckedIOException(format("Application data directory '%s' could not be created", dir), ex);
}
}
return dir;
}
/**
* Creates child directory assuming parent directories already exist. Does nothing if
* the directory already exists.
* @return the child directory, now guaranteed to exist
*/
private static File mkdir(File parent, String child) {
File dir = new File(parent, child);
if (!dir.exists()) {
try {
Files.createDirectory(dir.toPath());
} catch (IOException ex) {
throw new UncheckedIOException(format("Directory '%s' could not be created", dir), ex);
}
}
return dir;
}
// == STATIC ACCESSORS ======================================================================
/**
* Static accessor that returns the same value as the non-static
* {@link #appDataDir} property. For use only in the {@code Overlay} class, where
* because of its large number of subclasses, injecting the Guice-managed
* {@link Config} class is not worth the effort. {@link #appDataDir} should be
* favored in all other cases.
* @throws NullPointerException if the static value has not yet been assigned, i.e. if
* the Guice-managed {@link Config} class has not yet been instantiated elsewhere.
* This should never be the case, as Guice wiring always happens before any
* {@code Overlay} class is instantiated.
*/
public static File appDataDir() {
return checkNotNull(APP_DATA_DIR_VALUE, "The static appDataDir has not yet " +
"been assigned. A Config instance must be instantiated (usually by " +
"Guice) before calling this method.");
}
/**
* Static accessor that returns either the default base currency network value of
* {@link BaseCurrencyNetwork#BTC_MAINNET} or the value assigned via the
* {@value BASE_CURRENCY_NETWORK} option. The non-static
* {@link #baseCurrencyNetwork} property should be favored whenever possible and
* this static accessor should be used only in code locations where it is infeasible
* or too cumbersome to inject the normal Guice-managed singleton {@link Config}
* instance.
*/
public static BaseCurrencyNetwork baseCurrencyNetwork() {
return BASE_CURRENCY_NETWORK_VALUE;
}
/**
* Static accessor that returns the value of
* {@code baseCurrencyNetwork().getParameters()} for convenience and to avoid violating
* the <a href="https://en.wikipedia.org/wiki/Law_of_Demeter">Law of Demeter</a>. The
* non-static {@link #baseCurrencyNetwork} property should be favored whenever
* possible.
* @see #baseCurrencyNetwork()
*/
public static NetworkParameters baseCurrencyNetworkParameters() {
return BASE_CURRENCY_NETWORK_VALUE.getParameters();
}
}

View file

@ -0,0 +1,10 @@
package bisq.common.config;
import bisq.common.BisqException;
public class ConfigException extends BisqException {
public ConfigException(String format, Object... args) {
super(format, args);
}
}

View file

@ -0,0 +1,87 @@
package bisq.common.config;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.util.List;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
public class ConfigFileEditor {
private static final Logger log = (Logger) LoggerFactory.getLogger(ConfigFileEditor.class);
private final File file;
private final ConfigFileReader reader;
public ConfigFileEditor(File file) {
this.file = file;
this.reader = new ConfigFileReader(file);
}
public void setOption(String name) {
setOption(name, null);
}
public void setOption(String name, String arg) {
tryCreate(file);
List<String> lines = reader.getLines();
try (PrintWriter writer = new PrintWriter(file)) {
boolean fileAlreadyContainsTargetOption = false;
for (String line : lines) {
if (ConfigFileOption.isOption(line)) {
ConfigFileOption existingOption = ConfigFileOption.parse(line);
if (existingOption.name.equals(name)) {
fileAlreadyContainsTargetOption = true;
if (!existingOption.arg.equals(arg)) {
ConfigFileOption newOption = new ConfigFileOption(name, arg);
writer.println(newOption);
log.warn("Overwrote existing config file option '{}' as '{}'", existingOption, newOption);
continue;
}
}
}
writer.println(line);
}
if (!fileAlreadyContainsTargetOption)
writer.println(new ConfigFileOption(name, arg));
} catch (FileNotFoundException ex) {
throw new UncheckedIOException(ex);
}
}
public void clearOption(String name) {
if (!file.exists())
return;
List<String> lines = reader.getLines();
try (PrintWriter writer = new PrintWriter(file)) {
for (String line : lines) {
if (ConfigFileOption.isOption(line)) {
ConfigFileOption option = ConfigFileOption.parse(line);
if (option.name.equals(name)) {
log.warn("Cleared existing config file option '{}'", option);
continue;
}
}
writer.println(line);
}
} catch (FileNotFoundException ex) {
throw new UncheckedIOException(ex);
}
}
private void tryCreate(File file) {
try {
if (file.createNewFile())
log.info("Created config file '{}'", file);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

View file

@ -0,0 +1,36 @@
package bisq.common.config;
class ConfigFileOption {
public final String name;
public final String arg;
public ConfigFileOption(String name, String arg) {
this.name = name;
this.arg = arg;
}
public static boolean isOption(String line) {
return !line.isEmpty() && !line.startsWith("#");
}
public static ConfigFileOption parse(String option) {
if (!option.contains("="))
return new ConfigFileOption(option, null);
String[] tokens = clean(option).split("=");
String name = tokens[0].trim();
String arg = tokens.length > 1 ? tokens[1].trim() : "";
return new ConfigFileOption(name, arg);
}
public String toString() {
return String.format("%s%s", name, arg != null ? ('=' + arg) : "");
}
public static String clean(String option) {
return option
.trim()
.replace("\\:", ":");
}
}

View file

@ -0,0 +1,46 @@
package bisq.common.config;
import java.nio.file.Files;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import static java.util.stream.Collectors.toList;
class ConfigFileReader {
private final File file;
public ConfigFileReader(File file) {
this.file = file;
}
public List<String> getLines() {
if (!file.exists())
throw new ConfigException("Config file %s does not exist", file);
if (!file.canRead())
throw new ConfigException("Config file %s is not readable", file);
try {
return Files.readAllLines(file.toPath()).stream()
.map(ConfigFileReader::cleanLine)
.collect(toList());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public List<String> getOptionLines() {
return getLines().stream()
.filter(ConfigFileOption::isOption)
.collect(toList());
}
private static String cleanLine(String line) {
return ConfigFileOption.isOption(line) ? ConfigFileOption.clean(line) : line;
}
}

View file

@ -0,0 +1,70 @@
/*
* 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.common.config;
import joptsimple.ValueConverter;
import com.google.common.base.Enums;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import java.util.Set;
/**
* A {@link joptsimple.ValueConverter} that supports case-insensitive conversion from
* String to an enum label. Useful in conjunction with
* {@link joptsimple.ArgumentAcceptingOptionSpec#ofType(Class)} when the type in question
* is an enum.
*/
class EnumValueConverter implements ValueConverter<Enum> {
private final Class<? extends Enum> enumType;
public EnumValueConverter(Class<? extends Enum> enumType) {
this.enumType = enumType;
}
/**
* Attempt to resolve an enum of the specified type by looking for a label with the
* given value, trying all case variations in the process.
*
* @return the matching enum label (if any)
* @throws ConfigException if no such label matching the given value is found.
*/
@Override
public Enum convert(String value) {
Set<String> candidates = Sets.newHashSet(value, value.toUpperCase(), value.toLowerCase());
for (String candidate : candidates) {
Optional<? extends Enum> result = Enums.getIfPresent(enumType, candidate);
if (result.isPresent())
return result.get();
}
throw new ConfigException("Enum label %s.{%s} does not exist",
enumType.getSimpleName(), String.join("|", candidates));
}
@Override
public Class<? extends Enum> valueType() {
return enumType;
}
@Override
public String valuePattern() {
return null;
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common.consensus;
/**
* Marker interface for classes which are used in the trade contract.
* Any change of the class fields would breaking backward compatibility.
* If a field needs to get added it needs to be annotated with @JsonExclude (thus excluded from the contract JSON).
* Better to use the excludeFromJsonDataMap (annotated with @JsonExclude; used in PaymentAccountPayload) to
* add a key/value pair.
*/
public interface UsedForTradeContractJson {
}

View file

@ -0,0 +1,32 @@
/*
* 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.common.crypto;
public class CryptoException extends Exception {
public CryptoException(String message) {
super(message);
}
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
public CryptoException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.common.crypto;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CryptoUtils {
public static String pubKeyToString(PublicKey publicKey) {
final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
return Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded());
}
public static byte[] getRandomBytes(int size) {
byte[] bytes = new byte[size];
new SecureRandom().nextBytes(bytes);
return bytes;
}
}

View file

@ -0,0 +1,250 @@
/*
* 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.common.crypto;
import bisq.common.util.Hex;
import bisq.common.util.Utilities;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Encryption {
private static final Logger log = LoggerFactory.getLogger(Encryption.class);
public static final String ASYM_KEY_ALGO = "RSA";
private static final String ASYM_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1PADDING";
private static final String SYM_KEY_ALGO = "AES";
private static final String SYM_CIPHER = "AES";
private static final String HMAC = "HmacSHA256";
public static KeyPair generateKeyPair() {
long ts = System.currentTimeMillis();
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ASYM_KEY_ALGO);
keyPairGenerator.initialize(2048);
return keyPairGenerator.genKeyPair();
} catch (Throwable e) {
log.error("Could not create key.", e);
throw new RuntimeException("Could not create key.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Symmetric
///////////////////////////////////////////////////////////////////////////////////////////
public static byte[] encrypt(byte[] payload, SecretKey secretKey) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(SYM_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(payload);
} catch (Throwable e) {
log.error("error in encrypt", e);
throw new CryptoException(e);
}
}
public static byte[] decrypt(byte[] encryptedPayload, SecretKey secretKey) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(SYM_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedPayload);
} catch (Throwable e) {
throw new CryptoException(e);
}
}
public static SecretKey getSecretKeyFromBytes(byte[] secretKeyBytes) {
return new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, SYM_KEY_ALGO);
}
public static byte[] getSecretKeyBytes(SecretKey secretKey) {
return secretKey.getEncoded();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Hmac
///////////////////////////////////////////////////////////////////////////////////////////
private static byte[] getPayloadWithHmac(byte[] payload, SecretKey secretKey) {
byte[] payloadWithHmac;
try {
ByteArrayOutputStream outputStream = null;
try {
byte[] hmac = getHmac(payload, secretKey);
outputStream = new ByteArrayOutputStream();
outputStream.write(payload);
outputStream.write(hmac);
outputStream.flush();
payloadWithHmac = outputStream.toByteArray().clone();
} catch (IOException | NoSuchProviderException e) {
log.error("Could not create hmac", e);
throw new RuntimeException("Could not create hmac");
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignored) {
}
}
}
} catch (Throwable e) {
log.error("Could not create hmac", e);
throw new RuntimeException("Could not create hmac");
}
return payloadWithHmac;
}
private static boolean verifyHmac(byte[] message, byte[] hmac, SecretKey secretKey) {
try {
byte[] hmacTest = getHmac(message, secretKey);
return Arrays.equals(hmacTest, hmac);
} catch (Throwable e) {
log.error("Could not create cipher", e);
throw new RuntimeException("Could not create cipher");
}
}
private static byte[] getHmac(byte[] payload, SecretKey secretKey) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
Mac mac = Mac.getInstance(HMAC);
mac.init(secretKey);
return mac.doFinal(payload);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Symmetric with Hmac
///////////////////////////////////////////////////////////////////////////////////////////
public static byte[] encryptPayloadWithHmac(byte[] payload, SecretKey secretKey) throws CryptoException {
return encrypt(getPayloadWithHmac(payload, secretKey), secretKey);
}
public static byte[] decryptPayloadWithHmac(byte[] encryptedPayloadWithHmac, SecretKey secretKey) throws CryptoException {
byte[] payloadWithHmac = decrypt(encryptedPayloadWithHmac, secretKey);
String payloadWithHmacAsHex = Hex.encode(payloadWithHmac);
// first part is raw message
int length = payloadWithHmacAsHex.length();
int sep = length - 64;
String payloadAsHex = payloadWithHmacAsHex.substring(0, sep);
// last 64 bytes is hmac
String hmacAsHex = payloadWithHmacAsHex.substring(sep, length);
if (verifyHmac(Hex.decode(payloadAsHex), Hex.decode(hmacAsHex), secretKey)) {
return Hex.decode(payloadAsHex);
} else {
throw new CryptoException("Hmac does not match.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Asymmetric
///////////////////////////////////////////////////////////////////////////////////////////
public static byte[] encryptSecretKey(SecretKey secretKey, PublicKey publicKey) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(ASYM_CIPHER);
OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1",
MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
cipher.init(Cipher.WRAP_MODE, publicKey, oaepParameterSpec);
return cipher.wrap(secretKey);
} catch (Throwable e) {
log.error("Couldn't encrypt payload", e);
throw new CryptoException("Couldn't encrypt payload");
}
}
public static SecretKey decryptSecretKey(byte[] encryptedSecretKey, PrivateKey privateKey) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(ASYM_CIPHER);
OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1",
MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
cipher.init(Cipher.UNWRAP_MODE, privateKey, oaepParameterSpec);
return (SecretKey) cipher.unwrap(encryptedSecretKey, "AES", Cipher.SECRET_KEY);
} catch (Throwable e) {
// errors when trying to decrypt foreign network_messages are normal
throw new CryptoException(e);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Hybrid with signature of asymmetric key
///////////////////////////////////////////////////////////////////////////////////////////
public static SecretKey generateSecretKey(int bits) {
try {
KeyGenerator keyPairGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO);
keyPairGenerator.init(bits);
return keyPairGenerator.generateKey();
} catch (Throwable e) {
log.error("Couldn't generate key", e);
throw new RuntimeException("Couldn't generate key");
}
}
public static byte[] getPublicKeyBytes(PublicKey encryptionPubKey) {
return new X509EncodedKeySpec(encryptionPubKey.getEncoded()).getEncoded();
}
/**
* @param encryptionPubKeyBytes
* @return
*/
public static PublicKey getPublicKeyFromBytes(byte[] encryptionPubKeyBytes) {
try {
return KeyFactory.getInstance(Encryption.ASYM_KEY_ALGO).generatePublic(new X509EncodedKeySpec(encryptionPubKeyBytes));
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(encryptionPubKeyBytes), e);
throw new KeyConversionException(e);
}
}
}

View file

@ -0,0 +1,85 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.crypto;
import org.bitcoinj.core.Utils;
import com.google.common.base.Charsets;
import org.bouncycastle.crypto.digests.RIPEMD160Digest;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.ByteBuffer;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Hash {
/**
* @param data Data as byte array
* @return Hash of data
*/
public static byte[] getSha256Hash(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(data, 0, data.length);
return digest.digest();
} catch (NoSuchAlgorithmException e) {
log.error("Could not create MessageDigest for hash. ", e);
throw new RuntimeException(e);
}
}
/**
* @param message UTF-8 encoded message
* @return Hash of data
*/
public static byte[] getSha256Hash(String message) {
return getSha256Hash(message.getBytes(Charsets.UTF_8));
}
/**
* @param data data as Integer
* @return Hash of data
*/
public static byte[] getSha256Hash(Integer data) {
return getSha256Hash(ByteBuffer.allocate(4).putInt(data).array());
}
/**
* Calculates RIPEMD160(SHA256(data)).
*/
public static byte[] getSha256Ripemd160hash(byte[] data) {
return Utils.sha256hash160(data);
}
/**
* Calculates RIPEMD160(data).
*/
public static byte[] getRipemd160hash(byte[] data) {
RIPEMD160Digest digest = new RIPEMD160Digest();
digest.update(data, 0, data.length);
byte[] out = new byte[20];
digest.doFinal(out, 0);
return out;
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common.crypto;
public class KeyConversionException extends RuntimeException {
public KeyConversionException(Throwable cause) {
super(cause);
}
public KeyConversionException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.common.crypto;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.security.KeyPair;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Getter
@EqualsAndHashCode
@Slf4j
@Singleton
public final class KeyRing {
private final KeyPair signatureKeyPair;
private final KeyPair encryptionKeyPair;
private final PubKeyRing pubKeyRing;
@Inject
public KeyRing(KeyStorage keyStorage) {
if (keyStorage.allKeyFilesExist()) {
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE);
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION);
} else {
// First time we create key pairs
signatureKeyPair = Sig.generateKeyPair();
encryptionKeyPair = Encryption.generateKeyPair();
keyStorage.saveKeyRing(this);
}
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
}
// Don't print keys for security reasons
@Override
public String toString() {
return "KeyRing{" +
"signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() +
", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() +
", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() +
'}';
}
}

View file

@ -0,0 +1,171 @@
/*
* 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.common.crypto;
import bisq.common.config.Config;
import bisq.common.file.FileUtil;
import com.google.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.NotNull;
import static bisq.common.util.Preconditions.checkDir;
@Singleton
public class KeyStorage {
private static final Logger log = LoggerFactory.getLogger(KeyStorage.class);
public enum KeyEntry {
MSG_SIGNATURE("sig", Sig.KEY_ALGO),
MSG_ENCRYPTION("enc", Encryption.ASYM_KEY_ALGO);
private final String fileName;
private final String algorithm;
KeyEntry(String fileName, String algorithm) {
this.fileName = fileName;
this.algorithm = algorithm;
}
public String getFileName() {
return fileName;
}
public String getAlgorithm() {
return algorithm;
}
@NotNull
@Override
public String toString() {
return "Key{" +
"fileName='" + fileName + '\'' +
", algorithm='" + algorithm + '\'' +
'}';
}
}
private final File storageDir;
@Inject
public KeyStorage(@Named(Config.KEY_STORAGE_DIR) File storageDir) {
this.storageDir = checkDir(storageDir);
}
public boolean allKeyFilesExist() {
return fileExists(KeyEntry.MSG_SIGNATURE) && fileExists(KeyEntry.MSG_ENCRYPTION);
}
private boolean fileExists(KeyEntry keyEntry) {
return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists();
}
public KeyPair loadKeyPair(KeyEntry keyEntry) {
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
// long now = System.currentTimeMillis();
try {
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
PublicKey publicKey;
PrivateKey privateKey;
File filePrivateKey = new File(storageDir + "/" + keyEntry.getFileName() + ".key");
try (FileInputStream fis = new FileInputStream(filePrivateKey.getPath())) {
byte[] encodedPrivateKey = new byte[(int) filePrivateKey.length()];
//noinspection ResultOfMethodCallIgnored
fis.read(encodedPrivateKey);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
privateKey = keyFactory.generatePrivate(privateKeySpec);
} catch (InvalidKeySpecException | IOException e) {
log.error("Could not load key " + keyEntry.toString(), e.getMessage());
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
}
if (privateKey instanceof RSAPrivateCrtKey) {
RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent());
publicKey = keyFactory.generatePublic(publicKeySpec);
} else if (privateKey instanceof DSAPrivateKey) {
DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) privateKey;
DSAParams dsaParams = dsaPrivateKey.getParams();
BigInteger p = dsaParams.getP();
BigInteger q = dsaParams.getQ();
BigInteger g = dsaParams.getG();
BigInteger y = g.modPow(dsaPrivateKey.getX(), p);
KeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g);
publicKey = keyFactory.generatePublic(publicKeySpec);
} else {
throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm());
}
log.debug("load completed in {} msec", System.currentTimeMillis() - new Date().getTime());
return new KeyPair(publicKey, privateKey);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("Could not load key " + keyEntry.toString(), e);
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
}
}
public void saveKeyRing(KeyRing keyRing) {
savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName());
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName());
}
private void savePrivateKey(PrivateKey privateKey, String name) {
if (!storageDir.exists())
//noinspection ResultOfMethodCallIgnored
storageDir.mkdir();
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) {
fos.write(pkcs8EncodedKeySpec.getEncoded());
} catch (IOException e) {
log.error("Could not save key " + name, e);
throw new RuntimeException("Could not save key " + name, e);
}
}
}

View file

@ -0,0 +1,89 @@
/*
* 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.common.crypto;
import bisq.common.consensus.UsedForTradeContractJson;
import bisq.common.proto.network.NetworkPayload;
import bisq.common.util.Utilities;
import com.google.protobuf.ByteString;
import com.google.common.annotations.VisibleForTesting;
import java.security.PublicKey;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Same as KeyRing but with public keys only.
* Used to send public keys over the wire to other peer.
*/
@Slf4j
@EqualsAndHashCode
@Getter
public final class PubKeyRing implements NetworkPayload, UsedForTradeContractJson {
private final byte[] signaturePubKeyBytes;
private final byte[] encryptionPubKeyBytes;
private transient PublicKey signaturePubKey;
private transient PublicKey encryptionPubKey;
public PubKeyRing(PublicKey signaturePubKey, PublicKey encryptionPubKey) {
this.signaturePubKeyBytes = Sig.getPublicKeyBytes(signaturePubKey);
this.encryptionPubKeyBytes = Encryption.getPublicKeyBytes(encryptionPubKey);
this.signaturePubKey = signaturePubKey;
this.encryptionPubKey = encryptionPubKey;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
public PubKeyRing(byte[] signaturePubKeyBytes, byte[] encryptionPubKeyBytes) {
this.signaturePubKeyBytes = signaturePubKeyBytes;
this.encryptionPubKeyBytes = encryptionPubKeyBytes;
signaturePubKey = Sig.getPublicKeyFromBytes(signaturePubKeyBytes);
encryptionPubKey = Encryption.getPublicKeyFromBytes(encryptionPubKeyBytes);
}
@Override
public protobuf.PubKeyRing toProtoMessage() {
return protobuf.PubKeyRing.newBuilder()
.setSignaturePubKeyBytes(ByteString.copyFrom(signaturePubKeyBytes))
.setEncryptionPubKeyBytes(ByteString.copyFrom(encryptionPubKeyBytes))
.build();
}
public static PubKeyRing fromProto(protobuf.PubKeyRing proto) {
return new PubKeyRing(
proto.getSignaturePubKeyBytes().toByteArray(),
proto.getEncryptionPubKeyBytes().toByteArray());
}
@Override
public String toString() {
return "PubKeyRing{" +
"signaturePubKeyHex=" + Utilities.bytesAsHexString(signaturePubKeyBytes) +
", encryptionPubKeyHex=" + Utilities.bytesAsHexString(encryptionPubKeyBytes) +
"}";
}
}

View file

@ -0,0 +1,19 @@
package bisq.common.crypto;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class PubKeyRingProvider implements Provider<PubKeyRing> {
private final PubKeyRing pubKeyRing;
@Inject
public PubKeyRingProvider(KeyRing keyRing) {
pubKeyRing = keyRing.getPubKeyRing();
}
@Override
public PubKeyRing get() {
return pubKeyRing;
}
}

View file

@ -0,0 +1,80 @@
/*
* 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.common.crypto;
import bisq.common.proto.network.NetworkPayload;
import com.google.protobuf.ByteString;
import java.security.PublicKey;
import lombok.Value;
@Value
public final class SealedAndSigned implements NetworkPayload {
private final byte[] encryptedSecretKey;
private final byte[] encryptedPayloadWithHmac;
private final byte[] signature;
private final byte[] sigPublicKeyBytes;
transient private final PublicKey sigPublicKey;
public SealedAndSigned(byte[] encryptedSecretKey,
byte[] encryptedPayloadWithHmac,
byte[] signature,
PublicKey sigPublicKey) {
this.encryptedSecretKey = encryptedSecretKey;
this.encryptedPayloadWithHmac = encryptedPayloadWithHmac;
this.signature = signature;
this.sigPublicKey = sigPublicKey;
sigPublicKeyBytes = Sig.getPublicKeyBytes(sigPublicKey);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private SealedAndSigned(byte[] encryptedSecretKey,
byte[] encryptedPayloadWithHmac,
byte[] signature,
byte[] sigPublicKeyBytes) {
this.encryptedSecretKey = encryptedSecretKey;
this.encryptedPayloadWithHmac = encryptedPayloadWithHmac;
this.signature = signature;
this.sigPublicKeyBytes = sigPublicKeyBytes;
sigPublicKey = Sig.getPublicKeyFromBytes(sigPublicKeyBytes);
}
public protobuf.SealedAndSigned toProtoMessage() {
return protobuf.SealedAndSigned.newBuilder()
.setEncryptedSecretKey(ByteString.copyFrom(encryptedSecretKey))
.setEncryptedPayloadWithHmac(ByteString.copyFrom(encryptedPayloadWithHmac))
.setSignature(ByteString.copyFrom(signature))
.setSigPublicKeyBytes(ByteString.copyFrom(sigPublicKeyBytes))
.build();
}
public static SealedAndSigned fromProto(protobuf.SealedAndSigned proto) {
return new SealedAndSigned(proto.getEncryptedSecretKey().toByteArray(),
proto.getEncryptedPayloadWithHmac().toByteArray(),
proto.getSignature().toByteArray(),
proto.getSigPublicKeyBytes().toByteArray());
}
}

View file

@ -0,0 +1,141 @@
/*
* 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.common.crypto;
import bisq.common.util.Base64;
import bisq.common.util.Utilities;
import com.google.common.base.Charsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* StorageSignatureKeyPair/STORAGE_SIGN_KEY_ALGO: That is used for signing the data to be stored to the P2P network (by flooding).
* The algo is selected because it originated from the TomP2P version which used DSA.
* Changing to EC keys might be considered.
* <p></p>
* MsgSignatureKeyPair/MSG_SIGN_KEY_ALGO/MSG_SIGN_ALGO: That is used when sending a message to a peer which is encrypted and signed.
* Changing to EC keys might be considered.
*/
public class Sig {
private static final Logger log = LoggerFactory.getLogger(Sig.class);
public static final String KEY_ALGO = "DSA";
private static final String ALGO = "SHA256withDSA";
/**
* @return keyPair
*/
public static KeyPair generateKeyPair() {
long ts = System.currentTimeMillis();
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGO);
keyPairGenerator.initialize(1024);
return keyPairGenerator.genKeyPair();
} catch (NoSuchAlgorithmException e) {
log.error("Could not create key.", e);
throw new RuntimeException("Could not create key.");
}
}
/**
* @param privateKey
* @param data
* @return
*/
public static byte[] sign(PrivateKey privateKey, byte[] data) throws CryptoException {
try {
Signature sig = Signature.getInstance(ALGO);
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
} catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new CryptoException("Signing failed. " + e.getMessage());
}
}
/**
* @param privateKey
* @param message UTF-8 encoded message to sign
* @return Base64 encoded signature
*/
public static String sign(PrivateKey privateKey, String message) throws CryptoException {
byte[] sigAsBytes = sign(privateKey, message.getBytes(Charsets.UTF_8));
return Base64.encode(sigAsBytes);
}
/**
* @param publicKey
* @param data
* @param signature
* @return
*/
public static boolean verify(PublicKey publicKey, byte[] data, byte[] signature) throws CryptoException {
try {
Signature sig = Signature.getInstance(ALGO);
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
} catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new CryptoException("Signature verification failed", e);
}
}
/**
* @param publicKey
* @param message UTF-8 encoded message
* @param signature Base64 encoded signature
* @return
*/
public static boolean verify(PublicKey publicKey, String message, String signature) throws CryptoException {
return verify(publicKey, message.getBytes(Charsets.UTF_8), Base64.decode(signature));
}
/**
* @param sigPublicKeyBytes
* @return
*/
public static PublicKey getPublicKeyFromBytes(byte[] sigPublicKeyBytes) {
try {
return KeyFactory.getInstance(Sig.KEY_ALGO).generatePublic(new X509EncodedKeySpec(sigPublicKeyBytes));
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(sigPublicKeyBytes), e);
e.printStackTrace();
throw new KeyConversionException(e);
}
}
public static byte[] getPublicKeyBytes(PublicKey sigPublicKey) {
return new X509EncodedKeySpec(sigPublicKey.getEncoded()).getEncoded();
}
}

View file

@ -0,0 +1,56 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.file;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class CorruptedStorageFileHandler {
private final List<String> files = new ArrayList<>();
@Inject
public CorruptedStorageFileHandler() {
}
public void addFile(String fileName) {
files.add(fileName);
}
public Optional<List<String>> getFiles() {
if (files.isEmpty()) {
return Optional.empty();
}
if (files.size() == 1 && files.get(0).equals("ViewPathAsString")) {
log.debug("We detected incompatible data base file for Navigation. " +
"That is a minor issue happening with refactoring of UI classes " +
"and we don't display a warning popup to the user.");
return Optional.empty();
}
return Optional.of(files);
}
}

View file

@ -0,0 +1,224 @@
/*
* 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.common.file;
import bisq.common.util.Utilities;
import com.google.common.io.Files;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class FileUtil {
public static void rollingBackup(File dir, String fileName, int numMaxBackupFiles) {
if (dir.exists()) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
if (!backupDir.exists())
if (!backupDir.mkdir())
log.warn("make dir failed.\nBackupDir=" + backupDir.getAbsolutePath());
File origFile = new File(Paths.get(dir.getAbsolutePath(), fileName).toString());
if (origFile.exists()) {
String dirName = "backups_" + fileName;
if (dirName.contains("."))
dirName = dirName.replace(".", "_");
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
if (!backupFileDir.exists())
if (!backupFileDir.mkdir())
log.warn("make backupFileDir failed.\nBackupFileDir=" + backupFileDir.getAbsolutePath());
File backupFile = new File(Paths.get(backupFileDir.getAbsolutePath(), new Date().getTime() + "_" + fileName).toString());
try {
Files.copy(origFile, backupFile);
pruneBackup(backupFileDir, numMaxBackupFiles);
} catch (IOException e) {
log.error("Backup key failed: " + e.getMessage());
e.printStackTrace();
}
}
}
}
private static void pruneBackup(File backupDir, int numMaxBackupFiles) {
if (backupDir.isDirectory()) {
File[] files = backupDir.listFiles();
if (files != null) {
List<File> filesList = Arrays.asList(files);
if (filesList.size() > numMaxBackupFiles) {
filesList.sort(Comparator.comparing(File::getName));
File file = filesList.get(0);
if (file.isFile()) {
if (!file.delete())
log.error("Failed to delete file: " + file);
else
pruneBackup(backupDir, numMaxBackupFiles);
} else {
pruneBackup(new File(Paths.get(backupDir.getAbsolutePath(), file.getName()).toString()), numMaxBackupFiles);
}
}
}
}
}
public static void deleteDirectory(File file) throws IOException {
deleteDirectory(file, null, true);
}
public static void deleteDirectory(File file,
@Nullable File exclude,
boolean ignoreLockedFiles) throws IOException {
boolean excludeFileFound = false;
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null)
for (File f : files) {
boolean excludeFileFoundLocal = exclude != null && f.getAbsolutePath().equals(exclude.getAbsolutePath());
excludeFileFound |= excludeFileFoundLocal;
if (!excludeFileFoundLocal)
deleteDirectory(f, exclude, ignoreLockedFiles);
}
}
// Finally delete main file/dir if exclude file was not found in directory
if (!excludeFileFound && !(exclude != null && file.getAbsolutePath().equals(exclude.getAbsolutePath()))) {
try {
deleteFileIfExists(file, ignoreLockedFiles);
} catch (Throwable t) {
log.error("Could not delete file. Error=" + t.toString());
throw new IOException(t);
}
}
}
public static void deleteFileIfExists(File file) throws IOException {
deleteFileIfExists(file, true);
}
public static void deleteFileIfExists(File file, boolean ignoreLockedFiles) throws IOException {
try {
if (Utilities.isWindows())
file = file.getCanonicalFile();
if (file.exists() && !file.delete()) {
if (ignoreLockedFiles) {
// We check if file is locked. On Windows all open files are locked by the OS, so we
if (isFileLocked(file))
log.info("Failed to delete locked file: " + file.getAbsolutePath());
} else {
final String message = "Failed to delete file: " + file.getAbsolutePath();
log.error(message);
throw new IOException(message);
}
}
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();
throw new IOException(t);
}
}
private static boolean isFileLocked(File file) {
return !file.canWrite();
}
public static void resourceToFile(String resourcePath,
File destinationFile) throws ResourceNotFoundException, IOException {
try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) {
if (inputStream == null) {
throw new ResourceNotFoundException(resourcePath);
}
try (FileOutputStream fileOutputStream = new FileOutputStream(destinationFile)) {
IOUtils.copy(inputStream, fileOutputStream);
}
}
}
public static void renameFile(File oldFile, File newFile) throws IOException {
if (Utilities.isWindows()) {
// Work around an issue on Windows whereby you can't rename over existing files.
final File canonical = newFile.getCanonicalFile();
if (canonical.exists() && !canonical.delete()) {
throw new IOException("Failed to delete canonical file for replacement with save");
}
if (!oldFile.renameTo(canonical)) {
throw new IOException("Failed to rename " + oldFile + " to " + canonical);
}
} else if (!oldFile.renameTo(newFile)) {
throw new IOException("Failed to rename " + oldFile + " to " + newFile);
}
}
public static void copyFile(File origin, File target) throws IOException {
if (!origin.exists()) {
return;
}
try {
Files.copy(origin, target);
} catch (IOException e) {
log.error("Copy file failed", e);
throw new IOException("Failed to copy " + origin + " to " + target);
}
}
public static void copyDirectory(File source, File destination) throws IOException {
FileUtils.copyDirectory(source, destination);
}
public static File createNewFile(Path path) throws IOException {
File file = path.toFile();
if (!file.createNewFile()) {
throw new IOException("There already exists a file with path: " + path);
}
return file;
}
public static void removeAndBackupFile(File dbDir, File storageFile, String fileName, String backupFolderName)
throws IOException {
File corruptedBackupDir = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName).toString());
if (!corruptedBackupDir.exists() && !corruptedBackupDir.mkdir()) {
log.warn("make dir failed");
}
File corruptedFile = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName, fileName).toString());
if (storageFile.exists()) {
renameFile(storageFile, corruptedFile);
}
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.common.file;
import bisq.common.util.Utilities;
import java.nio.file.Paths;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Slf4j
public class JsonFileManager {
private final static List<JsonFileManager> INSTANCES = new ArrayList<>();
public static void shutDownAllInstances() {
INSTANCES.forEach(JsonFileManager::shutDown);
}
@Nullable
private ThreadPoolExecutor executor;
private final File dir;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public JsonFileManager(File dir) {
this.dir = dir;
if (!dir.exists() && !dir.mkdir()) {
log.warn("make dir failed");
}
INSTANCES.add(this);
}
@NotNull
protected ThreadPoolExecutor getExecutor() {
if (executor == null) {
executor = Utilities.getThreadPoolExecutor("JsonFileManagerExecutor", 5, 50, 60);
}
return executor;
}
public void shutDown() {
if (executor != null) {
executor.shutdown();
}
}
public void writeToDiscThreaded(String json, String fileName) {
getExecutor().execute(() -> writeToDisc(json, fileName));
}
public void writeToDisc(String json, String fileName) {
File jsonFile = new File(Paths.get(dir.getAbsolutePath(), fileName + ".json").toString());
File tempFile = null;
PrintWriter printWriter = null;
try {
tempFile = File.createTempFile("temp", null, dir);
tempFile.deleteOnExit();
printWriter = new PrintWriter(tempFile);
printWriter.println(json);
// This close call and comment is borrowed from FileManager. Not 100% sure it that is really needed but
// seems that had fixed in the past and we got reported issues on Windows so that fix might be still
// required.
// Close resources before replacing file with temp file because otherwise it causes problems on windows
// when rename temp file
printWriter.close();
FileUtil.renameFile(tempFile, jsonFile);
} catch (Throwable t) {
log.error("storageFile " + jsonFile.toString());
t.printStackTrace();
} finally {
if (tempFile != null && tempFile.exists()) {
log.warn("Temp file still exists after failed save. We will delete it now. storageFile=" + fileName);
if (!tempFile.delete())
log.error("Cannot delete temp file.");
}
if (printWriter != null)
printWriter.close();
}
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.common.file;
public class ResourceNotFoundException extends Exception {
public ResourceNotFoundException(String path) {
super("Resource not found: path = " + path);
}
}

View file

@ -0,0 +1,25 @@
/*
* 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.common.handlers;
/**
* For reporting error message only (UI)
*/
public interface ErrorMessageHandler {
void handleErrorMessage(String errorMessage);
}

View file

@ -0,0 +1,25 @@
/*
* 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.common.handlers;
/**
* For reporting throwable objects only
*/
public interface ExceptionHandler {
void handleException(Throwable throwable);
}

View file

@ -0,0 +1,25 @@
/*
* 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.common.handlers;
/**
* For reporting a description message and throwable
*/
public interface FaultHandler {
void handleFault(String errorMessage, Throwable throwable);
}

View file

@ -0,0 +1,22 @@
/*
* 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.common.handlers;
public interface ResultHandler {
void handleResult();
}

View file

@ -0,0 +1,522 @@
/*
* 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.common.persistence;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
import bisq.common.file.CorruptedStorageFileHandler;
import bisq.common.file.FileUtil;
import bisq.common.handlers.ResultHandler;
import bisq.common.proto.persistable.PersistableEnvelope;
import bisq.common.proto.persistable.PersistenceProtoResolver;
import bisq.common.util.Utilities;
import com.google.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.common.util.Preconditions.checkDir;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Responsible for reading persisted data and writing it on disk. We read usually only at start-up and keep data in RAM.
* We write all data which got a request for persistence at shut down at the very last moment when all other services
* are shut down, so allowing changes to the data in the very last moment. For critical data we set {@link Source}
* to HIGH which causes a timer to trigger a write to disk after 1 minute. We use that for not very frequently altered
* data and data which cannot be recovered from the network.
*
* We decided to not use threading (as it was in previous versions) as the read operation happens only at start-up and
* with the modified model that data is written at shut down we eliminate frequent and expensive disk I/O. Risks of
* deadlock or data inconsistency and a more complex model have been a further argument for that model. In fact
* previously we wasted a lot of resources as way too many threads have been created without doing actual work as well
* the write operations got triggered way too often specially for the very frequent changes at SequenceNumberMap and
* the very large DaoState (at dao blockchain sync that slowed down sync).
*
*
* @param <T> The type of the {@link PersistableEnvelope} to be written or read from disk
*/
@Slf4j
public class PersistenceManager<T extends PersistableEnvelope> {
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
public static final Map<String, PersistenceManager<?>> ALL_PERSISTENCE_MANAGERS = new HashMap<>();
private static boolean flushAtShutdownCalled;
private static final AtomicBoolean allServicesInitialized = new AtomicBoolean(false);
public static void onAllServicesInitialized() {
allServicesInitialized.set(true);
ALL_PERSISTENCE_MANAGERS.values().forEach(persistenceManager -> {
// In case we got a requestPersistence call before we got initialized we trigger the timer for the
// persist call
if (persistenceManager.persistenceRequested) {
persistenceManager.maybeStartTimerForPersistence();
}
});
}
public static void flushAllDataToDiskAtBackup(ResultHandler completeHandler) {
flushAllDataToDisk(completeHandler, false);
}
public static void flushAllDataToDiskAtShutdown(ResultHandler completeHandler) {
flushAllDataToDisk(completeHandler, true);
}
// We require being called only once from the global shutdown routine. As the shutdown routine has a timeout
// and error condition where we call the method as well beside the standard path and it could be that those
// alternative code paths call our method after it was called already, so it is a valid but rare case.
// We add a guard to prevent repeated calls.
private static void flushAllDataToDisk(ResultHandler completeHandler, boolean doShutdown) {
if (!allServicesInitialized.get()) {
log.warn("Application has not completed start up yet so we do not flush data to disk.");
completeHandler.handleResult();
return;
}
// We don't know from which thread we are called so we map to user thread
UserThread.execute(() -> {
if (doShutdown) {
if (flushAtShutdownCalled) {
log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call.");
return;
}
flushAtShutdownCalled = true;
}
log.info("Start flushAllDataToDisk");
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
if (openInstances.get() == 0) {
log.info("No PersistenceManager instances have been created yet.");
completeHandler.handleResult();
}
new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> {
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
// a requestPersistence call after an important state update. Those are usually rather small data stores.
// Otherwise we only persist if requestPersistence was called since the last persist call.
// We also check if we have called read already to avoid a very early write attempt before we have ever
// read the data, which would lead to a write of empty data
// (fixes https://github.com/bisq-network/bisq/issues/4844).
if (persistenceManager.readCalled.get() &&
(persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested)) {
// We always get our completeHandler called even if exceptions happen. In case a file write fails
// we still call our shutdown and count down routine as the completeHandler is triggered in any case.
// We get our result handler called from the write thread so we map back to user thread.
persistenceManager.persistNow(() ->
UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown)));
} else {
onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown);
}
});
});
}
// We get called always from user thread here.
private static void onWriteCompleted(ResultHandler completeHandler,
AtomicInteger openInstances,
PersistenceManager<?> persistenceManager,
boolean doShutdown) {
if (doShutdown) {
persistenceManager.shutdown();
}
if (openInstances.decrementAndGet() == 0) {
log.info("flushAllDataToDisk completed");
completeHandler.handleResult();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Enum
///////////////////////////////////////////////////////////////////////////////////////////
public enum Source {
// For data stores we received from the network and which could be rebuilt. We store only for avoiding too much network traffic.
NETWORK(1, TimeUnit.MINUTES.toMillis(5), false),
// For data stores which are created from private local data. This data could only be rebuilt from backup files.
PRIVATE(10, 200, true),
// For data stores which are created from private local data. Loss of that data would not have critical consequences.
PRIVATE_LOW_PRIO(4, TimeUnit.MINUTES.toMillis(1), false);
@Getter
private final int numMaxBackupFiles;
@Getter
private final long delay;
@Getter
private final boolean flushAtShutDown;
Source(int numMaxBackupFiles, long delay, boolean flushAtShutDown) {
this.numMaxBackupFiles = numMaxBackupFiles;
this.delay = delay;
this.flushAtShutDown = flushAtShutDown;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final File dir;
private final PersistenceProtoResolver persistenceProtoResolver;
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
private File storageFile;
private T persistable;
private String fileName;
private Source source = Source.PRIVATE_LOW_PRIO;
private Path usedTempFilePath;
private volatile boolean persistenceRequested;
@Nullable
private Timer timer;
private ExecutorService writeToDiskExecutor;
public final AtomicBoolean initCalled = new AtomicBoolean(false);
public final AtomicBoolean readCalled = new AtomicBoolean(false);
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public PersistenceManager(@Named(Config.STORAGE_DIR) File dir,
PersistenceProtoResolver persistenceProtoResolver,
CorruptedStorageFileHandler corruptedStorageFileHandler) {
this.dir = checkDir(dir);
this.persistenceProtoResolver = persistenceProtoResolver;
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(T persistable, Source source) {
this.initialize(persistable, persistable.getDefaultStorageFileName(), source);
}
public void initialize(T persistable, String fileName, Source source) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that initialize call.");
return;
}
if (ALL_PERSISTENCE_MANAGERS.containsKey(fileName)) {
RuntimeException runtimeException = new RuntimeException("We must not create multiple " +
"PersistenceManager instances for file " + fileName + ".");
// We want to get logged from where we have been called so lets print the stack trace.
runtimeException.printStackTrace();
throw runtimeException;
}
if (initCalled.get()) {
RuntimeException runtimeException = new RuntimeException("We must not call initialize multiple times. " +
"PersistenceManager for file: " + fileName + ".");
// We want to get logged from where we have been called so lets print the stack trace.
runtimeException.printStackTrace();
throw runtimeException;
}
initCalled.set(true);
this.persistable = persistable;
this.fileName = fileName;
this.source = source;
storageFile = new File(dir, fileName);
ALL_PERSISTENCE_MANAGERS.put(fileName, this);
}
public void shutdown() {
ALL_PERSISTENCE_MANAGERS.remove(fileName);
if (timer != null) {
timer.stop();
}
if (writeToDiskExecutor != null) {
writeToDiskExecutor.shutdown();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Reading file
///////////////////////////////////////////////////////////////////////////////////////////
/**
* Read persisted file in a thread.
*
* @param resultHandler Consumer of persisted data once it was read from disk.
* @param orElse Called if no file exists or reading of file failed.
*/
public void readPersisted(Consumer<T> resultHandler, Runnable orElse) {
readPersisted(checkNotNull(fileName), resultHandler, orElse);
}
/**
* Read persisted file in a thread.
* We map result handler calls to UserThread, so clients don't need to worry about threading
*
* @param fileName File name of our persisted data.
* @param resultHandler Consumer of persisted data once it was read from disk.
* @param orElse Called if no file exists or reading of file failed.
*/
public void readPersisted(String fileName, Consumer<T> resultHandler, Runnable orElse) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that readPersisted call.");
return;
}
new Thread(() -> {
T persisted = getPersisted(fileName);
if (persisted != null) {
UserThread.execute(() -> resultHandler.accept(persisted));
} else {
UserThread.execute(orElse);
}
}, "PersistenceManager-read-" + fileName).start();
}
// API for synchronous reading of data. Not recommended to be used in application code.
// Currently used by tests and monitor. Should be converted to the threaded API as well.
@Nullable
public T getPersisted() {
return getPersisted(checkNotNull(fileName));
}
@Nullable
public T getPersisted(String fileName) {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
return null;
}
readCalled.set(true);
File storageFile = new File(dir, fileName);
if (!storageFile.exists()) {
return null;
}
long ts = System.currentTimeMillis();
try (FileInputStream fileInputStream = new FileInputStream(storageFile)) {
protobuf.PersistableEnvelope proto = protobuf.PersistableEnvelope.parseDelimitedFrom(fileInputStream);
//noinspection unchecked
T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto);
log.info("Reading {} completed in {} ms", fileName, System.currentTimeMillis() - ts);
return persistableEnvelope;
} catch (Throwable t) {
log.error("Reading {} failed with {}.", fileName, t.getMessage());
try {
// We keep a backup which might be used for recovery
FileUtil.removeAndBackupFile(dir, storageFile, fileName, "backup_of_corrupted_data");
DevEnv.logErrorAndThrowIfDevMode(t.toString());
} catch (IOException e1) {
e1.printStackTrace();
log.error(e1.getMessage());
// We swallow Exception if backup fails
}
if (corruptedStorageFileHandler != null) {
corruptedStorageFileHandler.addFile(storageFile.getName());
}
}
return null;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Write file to disk
///////////////////////////////////////////////////////////////////////////////////////////
public void requestPersistence() {
if (flushAtShutdownCalled) {
log.warn("We have started the shut down routine already. We ignore that requestPersistence call.");
return;
}
persistenceRequested = true;
// If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at
// onAllServicesInitialized
if (!allServicesInitialized.get()) {
return;
}
maybeStartTimerForPersistence();
}
private void maybeStartTimerForPersistence() {
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
// can be rather long.
if (timer == null) {
timer = UserThread.runAfter(() -> {
persistNow(null);
UserThread.execute(() -> timer = null);
}, source.delay, TimeUnit.MILLISECONDS);
}
}
public void persistNow(@Nullable Runnable completeHandler) {
long ts = System.currentTimeMillis();
try {
// The serialisation is done on the user thread to avoid threading issue with potential mutations of the
// persistable object. Keeping it on the user thread we are in a synchronize model.
protobuf.PersistableEnvelope serialized = (protobuf.PersistableEnvelope) persistable.toPersistableMessage();
// For the write to disk task we use a thread. We do not have any issues anymore if the persistable objects
// gets mutated while the thread is running as we have serialized it already and do not operate on the
// reference to the persistable object.
getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler));
long duration = System.currentTimeMillis() - ts;
if (duration > 100) {
log.info("Serializing {} took {} msec", fileName, duration);
}
} catch (Throwable e) {
log.error("Error in saveToFile toProtoMessage: {}, {}", persistable.getClass().getSimpleName(), fileName);
e.printStackTrace();
throw new RuntimeException(e);
}
}
public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) {
if (!allServicesInitialized.get()) {
log.warn("Application has not completed start up yet so we do not permit writing data to disk.");
UserThread.execute(completeHandler);
return;
}
long ts = System.currentTimeMillis();
File tempFile = null;
FileOutputStream fileOutputStream = null;
try {
// Before we write we backup existing file
FileUtil.rollingBackup(dir, fileName, source.getNumMaxBackupFiles());
if (!dir.exists() && !dir.mkdir())
log.warn("make dir failed {}", fileName);
tempFile = usedTempFilePath != null
? FileUtil.createNewFile(usedTempFilePath)
: File.createTempFile("temp_" + fileName, null, dir);
// Don't use a new temp file path each time, as that causes the delete-on-exit hook to leak memory:
tempFile.deleteOnExit();
fileOutputStream = new FileOutputStream(tempFile);
serialized.writeDelimitedTo(fileOutputStream);
// Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide
// to not write through to physical media for at least a few seconds, but this is the best we can do.
fileOutputStream.flush();
fileOutputStream.getFD().sync();
// Close resources before replacing file with temp file because otherwise it causes problems on windows
// when rename temp file
fileOutputStream.close();
FileUtil.renameFile(tempFile, storageFile);
usedTempFilePath = tempFile.toPath();
} catch (Throwable t) {
// If an error occurred, don't attempt to reuse this path again, in case temp file cleanup fails.
usedTempFilePath = null;
log.error("Error at saveToFile, storageFile={}", fileName, t);
} finally {
if (tempFile != null && tempFile.exists()) {
log.warn("Temp file still exists after failed save. We will delete it now. storageFile={}", fileName);
if (!tempFile.delete()) {
log.error("Cannot delete temp file.");
}
}
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
// We swallow that
e.printStackTrace();
log.error("Cannot close resources." + e.getMessage());
}
long duration = System.currentTimeMillis() - ts;
if (duration > 100) {
log.info("Writing the serialized {} completed in {} msec", fileName, duration);
}
persistenceRequested = false;
if (completeHandler != null) {
UserThread.execute(completeHandler);
}
}
}
private ExecutorService getWriteToDiskExecutor() {
if (writeToDiskExecutor == null) {
String name = "Write-" + fileName + "_to-disk";
writeToDiskExecutor = Utilities.getSingleThreadExecutor(name);
}
return writeToDiskExecutor;
}
@Override
public String toString() {
return "PersistenceManager{" +
"\n fileName='" + fileName + '\'' +
",\n dir=" + dir +
",\n storageFile=" + storageFile +
",\n persistable=" + persistable +
",\n source=" + source +
",\n usedTempFilePath=" + usedTempFilePath +
",\n persistenceRequested=" + persistenceRequested +
"\n}";
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.common.proto;
import bisq.common.Payload;
import bisq.common.proto.persistable.PersistablePayload;
public interface ProtoResolver {
Payload fromProto(protobuf.PaymentAccountPayload proto);
PersistablePayload fromProto(protobuf.PersistableNetworkPayload proto);
}

View file

@ -0,0 +1,112 @@
/*
* 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.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.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class ProtoUtil {
public static Set<byte[]> byteSetFromProtoByteStringList(List<ByteString> byteStringList) {
return byteStringList.stream().map(ByteString::toByteArray).collect(Collectors.toSet());
}
/**
* Returns the input String, except when it's the empty string: "", then null is returned.
* Note: "" is the default value for a protobuffer string, so this means it's not filled in.
*/
@Nullable
public static String stringOrNullFromProto(String proto) {
return "".equals(proto) ? null : proto;
}
@Nullable
public static byte[] byteArrayOrNullFromProto(ByteString proto) {
return proto.isEmpty() ? null : proto.toByteArray();
}
/**
* Get a Java enum from a Protobuf enum in a safe way.
*
* @param enumType the class of the enum, e.g: BlaEnum.class
* @param name the name of the enum entry, e.g: proto.getWinner().name()
* @param <E> the enum Type
* @return an enum
*/
@Nullable
public static <E extends Enum<E>> E enumFromProto(Class<E> enumType, String name) {
String enumName = name != null ? name : "UNDEFINED";
E result = Enums.getIfPresent(enumType, enumName).orNull();
if (result == null) {
result = Enums.getIfPresent(enumType, "UNDEFINED").orNull();
log.debug("We try to lookup for an enum entry with name 'UNDEFINED' and use that if available, " +
"otherwise the enum is null. enum={}", result);
return result;
}
return result;
}
public static <T extends Message> Iterable<T> collectionToProto(Collection<? extends Proto> collection,
Class<T> messageType) {
return collection.stream()
.map(e -> {
final Message message = e.toProtoMessage();
try {
return messageType.cast(message);
} catch (ClassCastException t) {
log.error("Message could not be cast. message={}, messageType={}", message, messageType);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public static <T> Iterable<T> collectionToProto(Collection<? extends Proto> collection,
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);
}
public static Set<String> protocolStringListToSet(ProtocolStringList protocolStringList) {
return CollectionUtils.isEmpty(protocolStringList) ? new HashSet<>() : new HashSet<>(protocolStringList);
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.common.proto;
import java.io.IOException;
public class ProtobufferException extends IOException {
public ProtobufferException(String message) {
super(message);
}
public ProtobufferException(String message, Throwable e) {
super(message, e);
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common.proto;
public class ProtobufferRuntimeException extends RuntimeException {
public ProtobufferRuntimeException(String message) {
super(message);
}
public ProtobufferRuntimeException(String message, Throwable e) {
super(message, e);
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.common.proto.network;
import bisq.common.Envelope;
import com.google.protobuf.Message;
import lombok.EqualsAndHashCode;
import static com.google.common.base.Preconditions.checkArgument;
@EqualsAndHashCode
public abstract class NetworkEnvelope implements Envelope {
protected final int messageVersion;
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
protected NetworkEnvelope(int messageVersion) {
this.messageVersion = messageVersion;
}
public protobuf.NetworkEnvelope.Builder getNetworkEnvelopeBuilder() {
return protobuf.NetworkEnvelope.newBuilder().setMessageVersion(messageVersion);
}
@Override
public Message toProtoMessage() {
return getNetworkEnvelopeBuilder().build();
}
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder().build();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public int getMessageVersion() {
// -1 is used for the case that we use an envelope message as payload (mailbox)
// so we check only against 0 which is the default value if not set
checkArgument(messageVersion != 0, "messageVersion is not set (0).");
return messageVersion;
}
@Override
public String toString() {
return "NetworkEnvelope{" +
"\n messageVersion=" + messageVersion +
"\n}";
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.common.proto.network;
import bisq.common.Payload;
/**
* Interface for objects used inside WireEnvelope or other WirePayloads.
*/
public interface NetworkPayload extends Payload {
}

View file

@ -0,0 +1,34 @@
/*
* 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.common.proto.network;
import bisq.common.proto.ProtoResolver;
import bisq.common.proto.ProtobufferException;
import java.time.Clock;
public interface NetworkProtoResolver extends ProtoResolver {
NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException;
NetworkPayload fromProto(protobuf.StoragePayload proto);
NetworkPayload fromProto(protobuf.StorageEntryWrapper proto);
Clock getClock();
}

View file

@ -0,0 +1,50 @@
/*
* 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.common.proto.persistable;
import bisq.common.util.CollectionUtils;
import com.google.protobuf.Message;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class NavigationPath implements PersistableEnvelope {
private List<String> path = List.of();
@Override
public Message toProtoMessage() {
final protobuf.NavigationPath.Builder builder = protobuf.NavigationPath.newBuilder();
if (!CollectionUtils.isEmpty(path)) builder.addAllPath(path);
return protobuf.PersistableEnvelope.newBuilder().setNavigationPath(builder).build();
}
public static NavigationPath fromProto(protobuf.NavigationPath proto) {
return new NavigationPath(List.copyOf(proto.getPathList()));
}
}

View file

@ -0,0 +1,36 @@
/*
* 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.common.proto.persistable;
import bisq.common.Envelope;
import com.google.protobuf.Message;
/**
* Interface for the outside envelope object persisted to disk.
*/
public interface PersistableEnvelope extends Envelope {
default Message toPersistableMessage() {
return toProtoMessage();
}
default String getDefaultStorageFileName() {
return this.getClass().getSimpleName();
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.common.proto.persistable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import lombok.Getter;
public abstract class PersistableList<T extends PersistablePayload> implements PersistableEnvelope {
@Getter
public final List<T> list = createList();
protected List<T> createList() {
return new ArrayList<>();
}
public PersistableList() {
}
protected PersistableList(Collection<T> collection) {
setAll(collection);
}
public void setAll(Collection<T> collection) {
this.list.clear();
this.list.addAll(collection);
}
public boolean add(T item) {
if (!list.contains(item)) {
list.add(item);
return true;
}
return false;
}
public boolean remove(T item) {
return list.remove(item);
}
public Stream<T> stream() {
return list.stream();
}
public int size() {
return list.size();
}
public boolean contains(T item) {
return list.contains(item);
}
public boolean isEmpty() {
return list.isEmpty();
}
public void forEach(Consumer<? super T> action) {
list.forEach(action);
}
public void clear() {
list.clear();
}
}

View file

@ -0,0 +1,51 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.proto.persistable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.Collection;
import java.util.List;
public abstract class PersistableListAsObservable<T extends PersistablePayload> extends PersistableList<T> {
public PersistableListAsObservable() {
}
protected PersistableListAsObservable(Collection<T> collection) {
super(collection);
}
protected List<T> createList() {
return FXCollections.observableArrayList();
}
public ObservableList<T> getObservableList() {
return (ObservableList<T>) getList();
}
public void addListener(ListChangeListener<T> listener) {
((ObservableList<T>) getList()).addListener(listener);
}
public void removeListener(ListChangeListener<T> listener) {
((ObservableList<T>) getList()).removeListener(listener);
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.common.proto.persistable;
import bisq.common.Payload;
/**
* Interface for objects used inside Envelope or other Payloads.
*/
public interface PersistablePayload extends Payload {
}

View file

@ -0,0 +1,22 @@
/*
* 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.common.proto.persistable;
public interface PersistedDataHost {
void readPersisted(Runnable completeHandler);
}

View file

@ -0,0 +1,25 @@
/*
* 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.common.proto.persistable;
import bisq.common.proto.ProtoResolver;
public interface PersistenceProtoResolver extends ProtoResolver {
PersistableEnvelope fromProto(protobuf.PersistableEnvelope persistable);
}

View file

@ -0,0 +1,104 @@
package bisq.common.reactfx;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.util.Duration;
/**
* Provides factory methods for timers that are manipulated from and execute
* their action on the JavaFX application thread.
*
* Copied from:
* https://github.com/TomasMikula/ReactFX/blob/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/FxTimer.java
*
*/
public class FxTimer implements Timer {
/**
* Prepares a (stopped) timer that lasts for {@code delay} and whose action runs when timer <em>ends</em>.
*/
public static Timer create(java.time.Duration delay, Runnable action) {
return new FxTimer(delay, delay, action, 1);
}
/**
* Equivalent to {@code create(delay, action).restart()}.
*/
public static Timer runLater(java.time.Duration delay, Runnable action) {
Timer timer = create(delay, action);
timer.restart();
return timer;
}
/**
* Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically
* when the timer <em>ends</em>.
*/
public static Timer createPeriodic(java.time.Duration interval, Runnable action) {
return new FxTimer(interval, interval, action, Animation.INDEFINITE);
}
/**
* Equivalent to {@code createPeriodic(interval, action).restart()}.
*/
public static Timer runPeriodically(java.time.Duration interval, Runnable action) {
Timer timer = createPeriodic(interval, action);
timer.restart();
return timer;
}
/**
* Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically
* when the timer <em>starts</em>.
*/
public static Timer createPeriodic0(java.time.Duration interval, Runnable action) {
return new FxTimer(java.time.Duration.ZERO, interval, action, Animation.INDEFINITE);
}
/**
* Equivalent to {@code createPeriodic0(interval, action).restart()}.
*/
public static Timer runPeriodically0(java.time.Duration interval, Runnable action) {
Timer timer = createPeriodic0(interval, action);
timer.restart();
return timer;
}
private final Duration actionTime;
private final Timeline timeline;
private final Runnable action;
private long seq = 0;
private FxTimer(java.time.Duration actionTime, java.time.Duration period, Runnable action, int cycles) {
this.actionTime = Duration.millis(actionTime.toMillis());
this.timeline = new Timeline();
this.action = action;
timeline.getKeyFrames().add(new KeyFrame(this.actionTime)); // used as placeholder
if (period != actionTime) {
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(period.toMillis())));
}
timeline.setCycleCount(cycles);
}
@Override
public void restart() {
stop();
long expected = seq;
timeline.getKeyFrames().set(0, new KeyFrame(actionTime, ae -> {
if(seq == expected) {
action.run();
}
}));
timeline.play();
}
@Override
public void stop() {
timeline.stop();
++seq;
}
}

View file

@ -0,0 +1,10 @@
Copyright (c) 2013-2014, Tomas Mikula
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,6 @@
This package is a very minimal subset of the external library `org.reactfx`.
Two small files from `org.reactfx` were embedded into the project
to avoid having it as dependency:
[https://github.com/TomasMikula/ReactFX]

View file

@ -0,0 +1,66 @@
package bisq.common.reactfx;
/**
* Timer represents a delayed action. This means that every timer has an
* associated action and an associated delay. Action and delay are specified
* on timer creation.
*
* <p>Every timer also has an associated thread (such as JavaFX application
* thread or a single-thread executor's thread). Timer may only be accessed
* from its associated thread. Timer's action is executed on its associated
* thread, too. This design allows to implement guarantees provided by
* {@link #stop()}.
*
* Copied from:
* https://raw.githubusercontent.com/TomasMikula/ReactFX/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/Timer.java*
*/
public interface Timer {
/**
* Schedules the associated action to be executed after the associated
* delay. If the action is already scheduled but hasn't been executed yet,
* the timeout is reset, so that the action won't be executed before the
* full delay from now.
*/
void restart();
/**
* If the associated action has been scheduled for execution but not yet
* executed, this method prevents it from being executed at all. This is
* also true in case the timer's timeout has already expired, but the
* associated action hasn't had a chance to be executed on the associated
* thread. Note that this is a stronger guarantee than the one given by
* {@link javafx.animation.Animation#stop()}:
*
* <pre>
* {@code
* Timeline timeline = new Timeline(new KeyFrame(
* Duration.millis(1000),
* ae -> System.out.println("FIRED ANYWAY")));
* timeline.play();
*
* // later on the JavaFX application thread,
* // but still before the action has been executed
* timeline.stop();
*
* // later, "FIRED ANYWAY" may still be printed
* }
* </pre>
*
* In contrast, using the {@link FxTimer}, the action is guaranteed not to
* be executed after {@code stop()}:
* <pre>
* {@code
* Timer timer = FxTimer.runLater(
* Duration.ofMillis(1000),
* () -> System.out.println("FIRED"));
*
* // later on the JavaFX application thread,
* // but still before the action has been executed
* timer.stop();
*
* // "FIRED" is guaranteed *not* to be printed
* }
* </pre>
*/
void stop();
}

View file

@ -0,0 +1,129 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.setup;
import bisq.common.UserThread;
import bisq.common.app.AsciiLogo;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.util.Profiler;
import bisq.common.util.Utilities;
import org.bitcoinj.store.BlockStoreException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import ch.qos.logback.classic.Level;
import lombok.extern.slf4j.Slf4j;
import sun.misc.Signal;
@Slf4j
public class CommonSetup {
public static void setup(Config config, GracefulShutDownHandler gracefulShutDownHandler) {
setupLog(config);
AsciiLogo.showAsciiLogo();
Version.setBaseCryptoNetworkId(config.baseCurrencyNetwork.ordinal());
Version.printVersion();
maybePrintPathOfCodeSource();
Profiler.printSystemLoad();
setSystemProperties();
setupSigIntHandlers(gracefulShutDownHandler);
DevEnv.setup(config);
}
public static void printSystemLoadPeriodically(int delayMin) {
UserThread.runPeriodically(Profiler::printSystemLoad, delayMin, TimeUnit.MINUTES);
}
public static void setupUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
// Might come from another thread
if (throwable.getCause() != null && throwable.getCause().getCause() != null &&
throwable.getCause().getCause() instanceof BlockStoreException) {
log.error(throwable.getMessage());
} else if (throwable instanceof ClassCastException &&
"sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else if (throwable instanceof UnsupportedOperationException &&
"The system tray is not supported on the current platform.".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else {
log.error("Uncaught Exception from thread " + Thread.currentThread().getName());
log.error("throwableMessage= " + throwable.getMessage());
log.error("throwableClass= " + throwable.getClass());
log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable));
throwable.printStackTrace();
UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false));
}
};
Thread.setDefaultUncaughtExceptionHandler(handler);
Thread.currentThread().setUncaughtExceptionHandler(handler);
}
private static void setupLog(Config config) {
String logPath = Paths.get(config.appDataDir.getPath(), "bisq").toString();
Log.setup(logPath);
Utilities.printSysInfo();
Log.setLevel(Level.toLevel(config.logLevel));
}
protected static void setSystemProperties() {
if (Utilities.isLinux())
System.setProperty("prism.lcdtext", "false");
}
protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) {
Signal.handle(new Signal("INT"), signal -> {
log.info("Received {}", signal);
UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> {
}));
});
Signal.handle(new Signal("TERM"), signal -> {
log.info("Received {}", signal);
UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> {
}));
});
}
protected static void maybePrintPathOfCodeSource() {
try {
final String pathOfCodeSource = Utilities.getPathOfCodeSource();
if (!pathOfCodeSource.endsWith("classes"))
log.info("Path to Bisq jar file: " + pathOfCodeSource);
} catch (URISyntaxException e) {
log.error(e.toString());
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.common.setup;
import bisq.common.handlers.ResultHandler;
public interface GracefulShutDownHandler {
void gracefulShutDown(ResultHandler resultHandler);
}

View file

@ -0,0 +1,22 @@
/*
* 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.common.setup;
public interface UncaughtExceptionHandler {
void handleUncaughtException(Throwable throwable, boolean doShutDown);
}

View file

@ -0,0 +1,24 @@
/*
* 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.common.taskrunner;
public class InterceptTaskException extends RuntimeException {
public InterceptTaskException(String message) {
super(message);
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.common.taskrunner;
public interface Model {
void onComplete();
}

View file

@ -0,0 +1,76 @@
/*
* 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.common.taskrunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class Task<T extends Model> {
private static final Logger log = LoggerFactory.getLogger(Task.class);
public static Class<? extends Task> taskToIntercept;
private final TaskRunner taskHandler;
protected final T model;
protected String errorMessage = "An error occurred at task: " + getClass().getSimpleName();
protected boolean completed;
public Task(TaskRunner taskHandler, T model) {
this.taskHandler = taskHandler;
this.model = model;
}
protected abstract void run();
protected void runInterceptHook() {
if (getClass() == taskToIntercept)
throw new InterceptTaskException("Task intercepted for testing purpose. Task = " + getClass().getSimpleName());
}
protected void appendToErrorMessage(String message) {
errorMessage += "\n" + message;
}
protected void appendExceptionToErrorMessage(Throwable t) {
if (t.getMessage() != null)
errorMessage += "\nException message: " + t.getMessage();
else
errorMessage += "\nException: " + t.toString();
}
protected void complete() {
completed = true;
taskHandler.handleComplete();
}
protected void failed(String message) {
appendToErrorMessage(message);
failed();
}
protected void failed(Throwable t) {
log.error(errorMessage, t);
taskHandler.handleErrorMessage(errorMessage);
}
protected void failed() {
log.error(errorMessage);
taskHandler.handleErrorMessage(errorMessage);
}
}

View file

@ -0,0 +1,93 @@
/*
* 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.common.taskrunner;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TaskRunner<T extends Model> {
private final Queue<Class<? extends Task<T>>> tasks = new LinkedBlockingQueue<>();
private final T sharedModel;
private final Class<T> sharedModelClass;
private final ResultHandler resultHandler;
private final ErrorMessageHandler errorMessageHandler;
private boolean failed = false;
private boolean isCanceled;
private Class<? extends Task<T>> currentTask;
public TaskRunner(T sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
//noinspection unchecked
this(sharedModel, (Class<T>) sharedModel.getClass(), resultHandler, errorMessageHandler);
}
public TaskRunner(T sharedModel, Class<T> sharedModelClass, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
this.sharedModel = sharedModel;
this.resultHandler = resultHandler;
this.errorMessageHandler = errorMessageHandler;
this.sharedModelClass = sharedModelClass;
}
@SafeVarargs
public final void addTasks(Class<? extends Task<T>>... items) {
tasks.addAll(Arrays.asList(items));
}
public void run() {
next();
}
private void next() {
if (!failed && !isCanceled) {
if (tasks.size() > 0) {
try {
currentTask = tasks.poll();
log.info("Run task: " + currentTask.getSimpleName());
currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run();
} catch (Throwable throwable) {
throwable.printStackTrace();
handleErrorMessage("Error at taskRunner: " + throwable.getMessage());
}
} else {
resultHandler.handleResult();
}
}
}
public void cancel() {
isCanceled = true;
}
void handleComplete() {
next();
}
void handleErrorMessage(String errorMessage) {
log.error("Task failed: " + currentTask.getSimpleName() + " / errorMessage: " + errorMessage);
failed = true;
errorMessageHandler.handleErrorMessage(errorMessage);
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.common.util;
/**
* We use Java 8 builtin Base64 because it is much faster than Guava and Apache versions:
* http://java-performance.info/base64-encoding-and-decoding-performance/
*/
public class Base64 {
public static byte[] decode(String base64) {
return java.util.Base64.getDecoder().decode(base64);
}
public static String encode(byte[] bytes) {
return java.util.Base64.getEncoder().encodeToString(bytes);
}
}

View file

@ -0,0 +1,35 @@
package bisq.common.util;
import java.util.Collection;
import java.util.Map;
/**
* Collection utility methods copied from Spring Framework v4.3.6's
* {@code org.springframework.util.CollectionUtils} class in order to make it possible to
* drop Bisq's dependency on Spring altogether. The name of the class and methods have
* been preserved here to minimize the impact to the Bisq codebase of making this change.
* All that is necessary to swap this implementation in is to change the CollectionUtils
* import statement.
*/
public class CollectionUtils {
/**
* Return {@code true} if the supplied Collection is {@code null} or empty.
* Otherwise, return {@code false}.
* @param collection the Collection to check
* @return whether the given Collection is empty
*/
public static boolean isEmpty(Collection<?> collection) {
return (collection == null || collection.isEmpty());
}
/**
* Return {@code true} if the supplied Map is {@code null} or empty.
* Otherwise, return {@code false}.
* @param map the Map to check
* @return whether the given Map is empty
*/
public static boolean isEmpty(Map<?, ?> map) {
return (map == null || map.isEmpty());
}
}

View file

@ -0,0 +1,175 @@
/*
* 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.common.util;
import java.net.URI;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
// Taken form https://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform,
// originally net.mightypork.rpack.utils.DesktopApi
@Slf4j
class DesktopUtil {
public static boolean browse(URI uri) {
return openSystemSpecific(uri.toString());
}
public static boolean open(File file) {
return openSystemSpecific(file.getPath());
}
public static boolean edit(File file) {
// you can try something like
// runCommand("gimp", "%s", file.getPath())
// based on user preferences.
return openSystemSpecific(file.getPath());
}
private static boolean openSystemSpecific(String what) {
EnumOS os = getOs();
if (os.isLinux()) {
if (runCommand("kde-open", "%s", what)) return true;
if (runCommand("gnome-open", "%s", what)) return true;
if (runCommand("xdg-open", "%s", what)) return true;
}
if (os.isMac()) {
if (runCommand("open", "%s", what)) return true;
}
if (os.isWindows()) {
return runCommand("explorer", "%s", "\"" + what + "\"");
}
return false;
}
@SuppressWarnings("SameParameterValue")
private static boolean runCommand(String command, String args, String file) {
log.info("Trying to exec: cmd = {} args = {} file = {}", command, args, file);
String[] parts = prepareCommand(command, args, file);
try {
Process p = Runtime.getRuntime().exec(parts);
if (p == null) return false;
try {
int value = p.exitValue();
if (value == 0) {
log.warn("Process ended immediately.");
} else {
log.warn("Process crashed.");
}
return false;
} catch (IllegalThreadStateException e) {
log.info("Process is running.");
return true;
}
} catch (IOException e) {
log.warn("Error running command. {}", e.toString());
return false;
}
}
private static String[] prepareCommand(String command, String args, String file) {
List<String> parts = new ArrayList<>();
parts.add(command);
if (args != null) {
for (String s : args.split(" ")) {
s = String.format(s, file); // put in the filename thing
parts.add(s.trim());
}
}
return parts.toArray(new String[parts.size()]);
}
public enum EnumOS {
linux,
macos,
solaris,
unknown,
windows;
public boolean isLinux() {
return this == linux || this == solaris;
}
public boolean isMac() {
return this == macos;
}
public boolean isWindows() {
return this == windows;
}
}
private static EnumOS getOs() {
String s = System.getProperty("os.name").toLowerCase();
if (s.contains("win")) {
return EnumOS.windows;
}
if (s.contains("mac")) {
return EnumOS.macos;
}
if (s.contains("solaris")) {
return EnumOS.solaris;
}
if (s.contains("sunos")) {
return EnumOS.solaris;
}
if (s.contains("linux")) {
return EnumOS.linux;
}
if (s.contains("unix")) {
return EnumOS.linux;
} else {
return EnumOS.unknown;
}
}
}

View file

@ -0,0 +1,82 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.util;
import java.util.DoubleSummaryStatistics;
/* Adds logic to DoubleSummaryStatistics for keeping track of sum of squares
* and computing population variance and population standard deviation.
* Kahan summation algorithm (for `getSumOfSquares`) sourced from the DoubleSummaryStatistics class.
* Incremental variance algorithm sourced from https://math.stackexchange.com/a/1379804/316756
*/
public class DoubleSummaryStatisticsWithStdDev extends DoubleSummaryStatistics {
private double sumOfSquares;
private double sumOfSquaresCompensation; // Low order bits of sum of squares
private double simpleSumOfSquares; // Used to compute right sum of squares for non-finite inputs
@Override
public void accept(double value) {
super.accept(value);
double valueSquared = value * value;
simpleSumOfSquares += valueSquared;
sumOfSquaresWithCompensation(valueSquared);
}
public void combine(DoubleSummaryStatisticsWithStdDev other) {
super.combine(other);
simpleSumOfSquares += other.simpleSumOfSquares;
sumOfSquaresWithCompensation(other.sumOfSquares);
sumOfSquaresWithCompensation(other.sumOfSquaresCompensation);
}
/* Incorporate a new squared double value using Kahan summation /
* compensated summation.
*/
private void sumOfSquaresWithCompensation(double valueSquared) {
double tmp = valueSquared - sumOfSquaresCompensation;
double velvel = sumOfSquares + tmp; // Little wolf of rounding error
sumOfSquaresCompensation = (velvel - sumOfSquares) - tmp;
sumOfSquares = velvel;
}
private double getSumOfSquares() {
// Better error bounds to add both terms as the final sum of squares
double tmp = sumOfSquares + sumOfSquaresCompensation;
if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquares))
// If the compensated sum of squares is spuriously NaN from
// accumulating one or more same-signed infinite values,
// return the correctly-signed infinity stored in
// simpleSumOfSquares.
return simpleSumOfSquares;
else
return tmp;
}
private double getVariance() {
double sumOfSquares = getSumOfSquares();
long count = getCount();
double mean = getAverage();
return (sumOfSquares / count) - (mean * mean);
}
public final double getStandardDeviation() {
double variance = getVariance();
return Math.sqrt(variance);
}
}

View file

@ -0,0 +1,83 @@
/*
* 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.common.util;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Validator for extraDataMap fields used in network payloads.
* Ensures that we don't get the network attacked by huge data inserted there.
*/
@Slf4j
public class ExtraDataMapValidator {
// ExtraDataMap is only used for exceptional cases to not break backward compatibility.
// We don't expect many entries there.
public final static int MAX_SIZE = 10;
public final static int MAX_KEY_LENGTH = 100;
public final static int MAX_VALUE_LENGTH = 100000; // 100 kb
public static Map<String, String> getValidatedExtraDataMap(@Nullable Map<String, String> extraDataMap) {
return getValidatedExtraDataMap(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH);
}
public static Map<String, String> getValidatedExtraDataMap(@Nullable Map<String, String> extraDataMap, int maxSize,
int maxKeyLength, int maxValueLength) {
if (extraDataMap == null)
return null;
try {
checkArgument(extraDataMap.entrySet().size() <= maxSize,
"Size of map must not exceed " + maxSize);
extraDataMap.forEach((key, value) -> {
checkArgument(key.length() <= maxKeyLength,
"Length of key must not exceed " + maxKeyLength);
checkArgument(value.length() <= maxValueLength,
"Length of value must not exceed " + maxValueLength);
});
return extraDataMap;
} catch (Throwable t) {
return new HashMap<>();
}
}
public static void validate(@Nullable Map<String, String> extraDataMap) {
validate(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH);
}
public static void validate(@Nullable Map<String, String> extraDataMap, int maxSize, int maxKeyLength,
int maxValueLength) {
if (extraDataMap == null)
return;
checkArgument(extraDataMap.entrySet().size() <= maxSize,
"Size of map must not exceed " + maxSize);
extraDataMap.forEach((key, value) -> {
checkArgument(key.length() <= maxKeyLength,
"Length of key must not exceed " + maxKeyLength);
checkArgument(value.length() <= maxValueLength,
"Length of value must not exceed " + maxValueLength);
});
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.common.util;
import com.google.common.io.BaseEncoding;
public class Hex {
public static byte[] decode(String hex) {
return BaseEncoding.base16().lowerCase().decode(hex.toLowerCase());
}
public static String encode(byte[] bytes) {
return BaseEncoding.base16().lowerCase().encode(bytes);
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.common.util;
public class InvalidVersionException extends Exception {
public InvalidVersionException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonExclude {
}

View file

@ -0,0 +1,182 @@
/*
* 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.common.util;
import com.google.common.math.DoubleMath;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkArgument;
public class MathUtils {
private static final Logger log = LoggerFactory.getLogger(MathUtils.class);
public static double roundDouble(double value, int precision) {
return roundDouble(value, precision, RoundingMode.HALF_UP);
}
@SuppressWarnings("SameParameterValue")
public static double roundDouble(double value, int precision, RoundingMode roundingMode) {
if (precision < 0)
throw new IllegalArgumentException();
if (!Double.isFinite(value))
throw new IllegalArgumentException("Expected a finite double, but found " + value);
try {
BigDecimal bd = BigDecimal.valueOf(value);
bd = bd.setScale(precision, roundingMode);
return bd.doubleValue();
} catch (Throwable t) {
log.error(t.toString());
return 0;
}
}
public static long roundDoubleToLong(double value) {
return roundDoubleToLong(value, RoundingMode.HALF_UP);
}
@SuppressWarnings("SameParameterValue")
public static long roundDoubleToLong(double value, RoundingMode roundingMode) {
return DoubleMath.roundToLong(value, roundingMode);
}
public static int roundDoubleToInt(double value) {
return roundDoubleToInt(value, RoundingMode.HALF_UP);
}
@SuppressWarnings("SameParameterValue")
public static int roundDoubleToInt(double value, RoundingMode roundingMode) {
return DoubleMath.roundToInt(value, roundingMode);
}
public static long doubleToLong(double value) {
return Double.valueOf(value).longValue();
}
public static double scaleUpByPowerOf10(double value, int exponent) {
double factor = Math.pow(10, exponent);
return value * factor;
}
public static double scaleUpByPowerOf10(long value, int exponent) {
double factor = Math.pow(10, exponent);
return ((double) value) * factor;
}
public static double scaleDownByPowerOf10(double value, int exponent) {
double factor = Math.pow(10, exponent);
return value / factor;
}
public static double scaleDownByPowerOf10(long value, int exponent) {
double factor = Math.pow(10, exponent);
return ((double) value) / factor;
}
public static double exactMultiply(double value1, double value2) {
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
}
public static long getMedian(Long[] list) {
if (list.length == 0) {
return 0L;
}
int middle = list.length / 2;
long median;
if (list.length % 2 == 1) {
median = list[middle];
} else {
median = MathUtils.roundDoubleToLong((list[middle - 1] + list[middle]) / 2.0);
}
return median;
}
public static class MovingAverage {
final Deque<Long> window;
private final int size;
private long sum;
private final double outlier;
// Outlier as ratio
public MovingAverage(int size, double outlier) {
this.size = size;
window = new ArrayDeque<>(size);
this.outlier = outlier;
sum = 0;
}
public Optional<Double> next(long val) {
try {
var fullAtStart = isFull();
if (fullAtStart) {
if (outlier > 0) {
// Return early if it's an outlier
checkArgument(size != 0);
var avg = (double) sum / size;
if (Math.abs(avg - val) / avg > outlier) {
return Optional.empty();
}
}
sum -= window.remove();
}
window.add(val);
sum += val;
if (!fullAtStart && isFull() && outlier != 0) {
removeInitialOutlier();
}
// When discarding outliers, the first n non discarded elements return Optional.empty()
return outlier > 0 && !isFull() ? Optional.empty() : current();
} catch (Throwable t) {
log.error(t.toString());
return Optional.empty();
}
}
boolean isFull() {
return window.size() == size;
}
private void removeInitialOutlier() {
var element = window.iterator();
while (element.hasNext()) {
var val = element.next();
int div = size - 1;
checkArgument(div != 0);
var avgExVal = (double) (sum - val) / div;
if (Math.abs(avgExVal - val) / avgExVal > outlier) {
element.remove();
break;
}
}
}
public Optional<Double> current() {
return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size());
}
}
}

View file

@ -0,0 +1,177 @@
/*
* 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.common.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PermutationUtil {
/**
* @param list Original list
* @param indicesToRemove List of indices to remove
* @param <T> Type of List items
* @return Partial list where items at indices of indicesToRemove have been removed
*/
public static <T> List<T> getPartialList(List<T> list, List<Integer> indicesToRemove) {
List<T> altered = new ArrayList<>(list);
// Eliminate duplicates
indicesToRemove = new ArrayList<>(new HashSet<>(indicesToRemove));
// Sort
Collections.sort(indicesToRemove);
// Reverse list.
// We need to remove from highest index downwards to not change order of remaining indices
Collections.reverse(indicesToRemove);
indicesToRemove.forEach(index -> {
if (altered.size() > index && index >= 0)
altered.remove((int) index);
});
return altered;
}
public static <T, R> List<T> findMatchingPermutation(R targetValue,
List<T> list,
BiFunction<R, List<T>, Boolean> predicate,
int maxIterations) {
if (predicate.apply(targetValue, list)) {
return list;
} else {
return findMatchingPermutation(targetValue,
list,
new ArrayList<>(),
predicate,
new AtomicInteger(maxIterations));
}
}
private static <T, R> List<T> findMatchingPermutation(R targetValue,
List<T> list,
List<List<T>> lists,
BiFunction<R, List<T>, Boolean> predicate,
AtomicInteger maxIterations) {
for (int level = 0; level < list.size(); level++) {
// Test one level at a time
var result = checkLevel(targetValue, list, predicate, level, 0, maxIterations);
if (!result.isEmpty()) {
return result;
}
}
return new ArrayList<>();
}
@NonNull
private static <T, R> List<T> checkLevel(R targetValue,
List<T> previousLevel,
BiFunction<R, List<T>, Boolean> predicate,
int level,
int permutationIndex,
AtomicInteger maxIterations) {
if (previousLevel.size() == 1) {
return new ArrayList<>();
}
for (int i = permutationIndex; i < previousLevel.size(); i++) {
if (maxIterations.get() <= 0) {
return new ArrayList<>();
}
List<T> newList = new ArrayList<>(previousLevel);
newList.remove(i);
if (level == 0) {
maxIterations.decrementAndGet();
// Check all permutations on this level
if (predicate.apply(targetValue, newList)) {
return newList;
}
} else {
// Test next level
var result = checkLevel(targetValue, newList, predicate, level - 1, i, maxIterations);
if (!result.isEmpty()) {
return result;
}
}
}
return new ArrayList<>();
}
//TODO optimize algorithm so that it starts from all objects and goes down instead starting with from the bottom.
// That should help that we are not hitting the iteration limit so easily.
/**
* Returns a list of all possible permutations of a give sorted list ignoring duplicates.
* E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]]
* Number of variations and iterations grows with 2^n - 1 where n is the number of items in the list.
* With 20 items we reach about 1 million iterations and it takes about 0.5 sec.
* To avoid performance issues we added the maxIterations parameter to stop once the number of iterations has
* reached the maxIterations and return in such a case the list of permutations we have been able to create.
* Depending on the type of object which is stored in the list the memory usage should be considered as well for
* choosing the right maxIterations value.
*
* @param list List from which we create permutations
* @param maxIterations Max. number of iterations including inner iterations
* @param <T> Type of list items
* @return List of possible permutations of the original list
*/
public static <T> List<List<T>> findAllPermutations(List<T> list, int maxIterations) {
List<List<T>> result = new ArrayList<>();
int counter = 0;
long ts = System.currentTimeMillis();
for (T item : list) {
counter++;
if (counter > maxIterations) {
log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " +
"counter={}", counter);
return result;
}
List<List<T>> subLists = new ArrayList<>();
for (int n = 0; n < result.size(); n++) {
counter++;
if (counter > maxIterations) {
log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " +
"counter={}", counter);
return result;
}
List<T> subList = new ArrayList<>(result.get(n));
subList.add(item);
subLists.add(subList);
}
// add single item
result.add(new ArrayList<>(Collections.singletonList(item)));
// add subLists
result.addAll(subLists);
}
log.info("findAllPermutations took {} ms for {} items and {} iterations. Heap size used: {} MB",
(System.currentTimeMillis() - ts), list.size(), counter, Profiler.getUsedMemoryInMB());
return result;
}
}

View file

@ -0,0 +1,32 @@
package bisq.common.util;
import java.io.File;
import static java.lang.String.format;
/**
* Custom preconditions similar to those found in
* {@link com.google.common.base.Preconditions}.
*/
public class Preconditions {
/**
* Ensures that {@code dir} is a non-null, existing and read-writeable directory.
* @param dir the directory to check
* @return the given directory, now validated
*/
public static File checkDir(File dir) {
if (dir == null)
throw new IllegalArgumentException("Directory must not be null");
if (!dir.exists())
throw new IllegalArgumentException(format("Directory '%s' does not exist", dir));
if (!dir.isDirectory())
throw new IllegalArgumentException(format("Directory '%s' is not a directory", dir));
if (!dir.canRead())
throw new IllegalArgumentException(format("Directory '%s' is not readable", dir));
if (!dir.canWrite())
throw new IllegalArgumentException(format("Directory '%s' is not writeable", dir));
return dir;
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.common.util;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Profiler {
public static void printSystemLoad() {
Runtime runtime = Runtime.getRuntime();
long free = runtime.freeMemory() / 1024 / 1024;
long total = runtime.totalMemory() / 1024 / 1024;
long used = total - free;
log.info("System report: Used memory: {} MB; Free memory: {} MB; Total memory: {} MB; No. of threads: {}",
used, free, total, Thread.activeCount());
}
public static long getUsedMemoryInMB() {
return getUsedMemoryInBytes() / 1024 / 1024;
}
public static long getUsedMemoryInBytes() {
Runtime runtime = Runtime.getRuntime();
long free = runtime.freeMemory();
long total = runtime.totalMemory();
return total - free;
}
public static long getFreeMemoryInMB() {
return Runtime.getRuntime().freeMemory() / 1024 / 1024;
}
public static long getTotalMemoryInMB() {
return Runtime.getRuntime().totalMemory() / 1024 / 1024;
}
}

View file

@ -0,0 +1,140 @@
/*
* 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.common.util;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static org.apache.commons.lang3.StringUtils.capitalize;
@Slf4j
public class ReflectionUtils {
/**
* Recursively loads a list of fields for a given class and its superclasses,
* using a filter predicate to exclude any unwanted fields.
*
* @param fields The list of fields being loaded for a class hierarchy.
* @param clazz The lowest level class in a hierarchy; excluding Object.class.
* @param isExcludedField The field exclusion predicate.
*/
public static void loadFieldListForClassHierarchy(List<Field> fields,
Class<?> clazz,
Predicate<Field> isExcludedField) {
fields.addAll(stream(clazz.getDeclaredFields())
.filter(f -> !isExcludedField.test(f))
.collect(Collectors.toList()));
Class<?> superclass = clazz.getSuperclass();
if (!Objects.equals(superclass, Object.class))
loadFieldListForClassHierarchy(fields,
superclass,
isExcludedField);
}
/**
* Returns an Optional of a setter method for a given field and a class hierarchy,
* or Optional.empty() if it does not exist.
*
* @param field The field used to find a setter method.
* @param clazz The lowest level class in a hierarchy; excluding Object.class.
* @return Optional<Method> of the setter method for a field in the class hierarchy,
* or Optional.empty() if it does not exist.
*/
public static Optional<Method> getSetterMethodForFieldInClassHierarchy(Field field,
Class<?> clazz) {
Optional<Method> setter = stream(clazz.getDeclaredMethods())
.filter((m) -> isSetterForField(m, field))
.findFirst();
if (setter.isPresent())
return setter;
Class<?> superclass = clazz.getSuperclass();
if (!Objects.equals(superclass, Object.class)) {
setter = getSetterMethodForFieldInClassHierarchy(field, superclass);
if (setter.isPresent())
return setter;
}
return Optional.empty();
}
public static boolean isSetterForField(Method m, Field f) {
return m.getName().startsWith("set")
&& m.getName().endsWith(capitalize(f.getName()))
&& m.getReturnType().getName().equals("void")
&& m.getParameterCount() == 1
&& m.getParameterTypes()[0].getName().equals(f.getType().getName());
}
public static boolean isSetterOnClass(Method setter, Class<?> clazz) {
return clazz.equals(setter.getDeclaringClass());
}
public static String getVisibilityModifierAsString(Field field) {
if (Modifier.isPrivate(field.getModifiers()))
return "private";
else if (Modifier.isProtected(field.getModifiers()))
return "protected";
else if (Modifier.isPublic(field.getModifiers()))
return "public";
else
return "";
}
public static Field getField(String name, Class<?> clazz) {
Optional<Field> field = stream(clazz.getDeclaredFields())
.filter(f -> f.getName().equals(name)).findFirst();
return field.orElseThrow(() ->
new IllegalArgumentException(format("field %s not found in class %s",
name,
clazz.getSimpleName())));
}
public static Method getMethod(String name, Class<?> clazz) {
Optional<Method> method = stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(name)).findFirst();
return method.orElseThrow(() ->
new IllegalArgumentException(format("method %s not found in class %s",
name,
clazz.getSimpleName())));
}
public static void handleSetFieldValueError(Object object,
Field field,
ReflectiveOperationException ex) {
String errMsg = format("cannot set value of field %s, on class %s",
field.getName(),
object.getClass().getSimpleName());
log.error(capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.common.util;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.lang.management.ManagementFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Borrowed from: https://dzone.com/articles/programmatically-restart-java
public class RestartUtil {
private static final Logger log = LoggerFactory.getLogger(RestartUtil.class);
/**
* Sun property pointing the main class and its arguments.
* Might not be defined on non Hotspot VM implementations.
*/
public static final String SUN_JAVA_COMMAND = "sun.java.command";
public static void restartApplication(String logPath) throws IOException {
try {
String java = System.getProperty("java.home") + "/bin/java";
List<String> vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
StringBuilder vmArgsOneLine = new StringBuilder();
// if it's the agent argument : we ignore it otherwise the
// address of the old application and the new one will be in conflict
vmArguments.stream().filter(arg -> !arg.contains("-agentlib")).forEach(arg -> {
vmArgsOneLine.append(arg);
vmArgsOneLine.append(" ");
});
// init the command to execute, add the vm args
final StringBuilder cmd = new StringBuilder(java + " " + vmArgsOneLine);
// program main and program arguments
String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" ");
// program main is a jar
if (mainCommand[0].endsWith(".jar")) {
// if it's a jar, add -jar mainJar
cmd.append("-jar ").append(new File(mainCommand[0]).getPath());
} else {
// else it's a .class, add the classpath and mainClass
cmd.append("-cp \"").append(System.getProperty("java.class.path")).append("\" ").append(mainCommand[0]);
}
// finally add program arguments
for (int i = 1; i < mainCommand.length; i++) {
cmd.append(" ");
cmd.append(mainCommand[i]);
}
try {
final String command = "nohup " + cmd.toString() + " >/dev/null 2>" + logPath + " &";
log.warn("\n\n############################################################\n" +
"Executing cmd for restart: {}" +
"\n############################################################\n\n",
command);
Runtime.getRuntime().exec(command);
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
throw new IOException("Error while trying to restart the application", e);
}
}
}

View file

@ -0,0 +1,62 @@
/*
* 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.common.util;
import java.io.Serializable;
import java.util.Objects;
public class Tuple2<A, B> implements Serializable {
private static final long serialVersionUID = 1;
final public A first;
final public B second;
public Tuple2(A first, B second) {
this.first = first;
this.second = second;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tuple2)) return false;
Tuple2<?, ?> tuple2 = (Tuple2<?, ?>) o;
if (!Objects.equals(first, tuple2.first)) return false;
return Objects.equals(second, tuple2.second);
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Tuple2{" +
"\n first=" + first +
",\n second=" + second +
"\n}";
}
}

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.common.util;
import java.util.Objects;
public class Tuple3<A, B, C> {
final public A first;
final public B second;
final public C third;
public Tuple3(A first, B second, C third) {
this.first = first;
this.second = second;
this.third = third;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tuple3)) return false;
Tuple3<?, ?, ?> tuple3 = (Tuple3<?, ?, ?>) o;
if (!Objects.equals(first, tuple3.first)) return false;
if (!Objects.equals(second, tuple3.second)) return false;
return Objects.equals(third, tuple3.third);
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
result = 31 * result + (third != null ? third.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.common.util;
import java.util.Objects;
public class Tuple4<A, B, C, D> {
final public A first;
final public B second;
final public C third;
final public D fourth;
public Tuple4(A first, B second, C third, D fourth) {
this.first = first;
this.second = second;
this.third = third;
this.fourth = fourth;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tuple4)) return false;
Tuple4<?, ?, ?, ?> tuple4 = (Tuple4<?, ?, ?, ?>) o;
if (!Objects.equals(first, tuple4.first)) return false;
if (!Objects.equals(second, tuple4.second)) return false;
if (!Objects.equals(third, tuple4.third)) return false;
return Objects.equals(fourth, tuple4.fourth);
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
result = 31 * result + (third != null ? third.hashCode() : 0);
result = 31 * result + (fourth != null ? fourth.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.common.util;
import java.util.Objects;
public class Tuple5<A, B, C, D, E> {
final public A first;
final public B second;
final public C third;
final public D fourth;
final public E fifth;
public Tuple5(A first, B second, C third, D fourth, E fifth) {
this.first = first;
this.second = second;
this.third = third;
this.fourth = fourth;
this.fifth = fifth;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tuple5)) return false;
Tuple5<?, ?, ?, ?, ?> tuple5 = (Tuple5<?, ?, ?, ?, ?>) o;
if (!Objects.equals(first, tuple5.first)) return false;
if (!Objects.equals(second, tuple5.second)) return false;
if (!Objects.equals(third, tuple5.third)) return false;
if (!Objects.equals(fourth, tuple5.fourth)) return false;
return Objects.equals(fifth, tuple5.fifth);
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
result = 31 * result + (third != null ? third.hashCode() : 0);
result = 31 * result + (fourth != null ? fourth.hashCode() : 0);
result = 31 * result + (fifth != null ? fifth.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,594 @@
/*
* 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.common.util;
import org.bitcoinj.core.Utils;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.common.base.Splitter;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import java.text.DecimalFormat;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class Utilities {
public static String objectToJson(Object object) {
Gson gson = new GsonBuilder()
.setExclusionStrategies(new AnnotationExclusionStrategy())
/*.excludeFieldsWithModifiers(Modifier.TRANSIENT)*/
/* .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)*/
.setPrettyPrinting()
.create();
return gson.toJson(object);
}
public static ExecutorService getSingleThreadExecutor(String name) {
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(name)
.setDaemon(true)
.build();
return Executors.newSingleThreadExecutor(threadFactory);
}
public static ListeningExecutorService getSingleThreadListeningExecutor(String name) {
return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name));
}
public static ListeningExecutorService getListeningExecutorService(String name,
int corePoolSize,
int maximumPoolSize,
long keepAliveTimeInSec) {
return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec));
}
public static ListeningExecutorService getListeningExecutorService(String name,
int corePoolSize,
int maximumPoolSize,
long keepAliveTimeInSec,
BlockingQueue<Runnable> workQueue) {
return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, workQueue));
}
public static ThreadPoolExecutor getThreadPoolExecutor(String name,
int corePoolSize,
int maximumPoolSize,
long keepAliveTimeInSec) {
return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec,
new ArrayBlockingQueue<>(maximumPoolSize));
}
private static ThreadPoolExecutor getThreadPoolExecutor(String name,
int corePoolSize,
int maximumPoolSize,
long keepAliveTimeInSec,
BlockingQueue<Runnable> workQueue) {
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(name)
.setDaemon(true)
.build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTimeInSec,
TimeUnit.SECONDS, workQueue, threadFactory);
executor.allowCoreThreadTimeOut(true);
executor.setRejectedExecutionHandler((r, e) -> log.debug("RejectedExecutionHandler called"));
return executor;
}
@SuppressWarnings("SameParameterValue")
public static ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor(String name,
int corePoolSize,
int maximumPoolSize,
long keepAliveTimeInSec) {
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(name)
.setDaemon(true)
.setPriority(Thread.MIN_PRIORITY)
.build();
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
executor.setKeepAliveTime(keepAliveTimeInSec, TimeUnit.SECONDS);
executor.allowCoreThreadTimeOut(true);
executor.setMaximumPoolSize(maximumPoolSize);
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
executor.setRejectedExecutionHandler((r, e) -> log.debug("RejectedExecutionHandler called"));
return executor;
}
// TODO: Can some/all of the uses of this be replaced by guava MoreExecutors.shutdownAndAwaitTermination(..)?
public static void shutdownAndAwaitTermination(ExecutorService executor, long timeout, TimeUnit unit) {
executor.shutdown();
try {
if (!executor.awaitTermination(timeout, unit)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
public static <V> FutureCallback<V> failureCallback(Consumer<Throwable> errorHandler) {
return new FutureCallback<>() {
@Override
public void onSuccess(V result) {
}
@Override
public void onFailure(@NotNull Throwable t) {
errorHandler.accept(t);
}
};
}
/**
* @return true if <code>defaults read -g AppleInterfaceStyle</code> has an exit status of <code>0</code> (i.e. _not_ returning "key not found").
*/
public static boolean isMacMenuBarDarkMode() {
try {
// check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents..
Process process = Runtime.getRuntime().exec(new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
process.waitFor(100, TimeUnit.MILLISECONDS);
return process.exitValue() == 0;
} catch (IOException | InterruptedException | IllegalThreadStateException ex) {
// IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate
return false;
}
}
public static boolean isUnix() {
return isOSX() || isLinux() || getOSName().contains("freebsd");
}
public static boolean isWindows() {
return getOSName().contains("win");
}
/**
* @return True, if Bisq is running on a virtualized OS within Qubes, false otherwise
*/
public static boolean isQubesOS() {
// For Linux qubes, "os.version" looks like "4.19.132-1.pvops.qubes.x86_64"
// The presence of the "qubes" substring indicates this Linux is running as a qube
// This is the case for all 3 virtualization modes (PV, PVH, HVM)
// In addition, this works for both simple AppVMs, as well as for StandaloneVMs
// TODO This might not work for detecting Qubes virtualization for other OSes
// like Windows
return getOSVersion().contains("qubes");
}
public static boolean isOSX() {
return getOSName().contains("mac") || getOSName().contains("darwin");
}
public static boolean isLinux() {
return getOSName().contains("linux");
}
public static boolean isDebianLinux() {
return isLinux() && new File("/etc/debian_version").isFile();
}
public static boolean isRedHatLinux() {
return isLinux() && new File("/etc/redhat-release").isFile();
}
private static String getOSName() {
return System.getProperty("os.name").toLowerCase(Locale.US);
}
public static String getOSVersion() {
return System.getProperty("os.version").toLowerCase(Locale.US);
}
/**
* Returns the well-known "user data directory" for the current operating system.
*/
public static File getUserDataDir() {
if (Utilities.isWindows())
return new File(System.getenv("APPDATA"));
if (Utilities.isOSX())
return Paths.get(System.getProperty("user.home"), "Library", "Application Support").toFile();
// *nix
return Paths.get(System.getProperty("user.home"), ".local", "share").toFile();
}
public static int getMinorVersion() throws InvalidVersionException {
String version = getOSVersion();
String[] tokens = version.split("\\.");
try {
checkArgument(tokens.length > 1);
return Integer.parseInt(tokens[1]);
} catch (IllegalArgumentException e) {
printSysInfo();
throw new InvalidVersionException("Version is not in expected format. Version=" + version);
}
}
public static int getMajorVersion() throws InvalidVersionException {
String version = getOSVersion();
String[] tokens = version.split("\\.");
try {
checkArgument(tokens.length > 0);
return Integer.parseInt(tokens[0]);
} catch (IllegalArgumentException e) {
printSysInfo();
throw new InvalidVersionException("Version is not in expected format. Version=" + version);
}
}
public static String getOSArchitecture() {
String osArch = System.getProperty("os.arch");
if (isWindows()) {
// See: Like always windows needs extra treatment
// https://stackoverflow.com/questions/20856694/how-to-find-the-os-bit-type
String arch = System.getenv("PROCESSOR_ARCHITECTURE");
String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432");
return arch.endsWith("64")
|| wow64Arch != null && wow64Arch.endsWith("64")
? "64" : "32";
} else if (osArch.contains("arm")) {
// armv8 is 64 bit, armv7l is 32 bit
return osArch.contains("64") || osArch.contains("v8") ? "64" : "32";
} else if (isLinux()) {
return osArch.startsWith("i") ? "32" : "64";
} else {
return osArch.contains("64") ? "64" : osArch;
}
}
public static void printSysInfo() {
log.info("System info: os.name={}; os.version={}; os.arch={}; sun.arch.data.model={}; JRE={}; JVM={}",
System.getProperty("os.name"),
System.getProperty("os.version"),
System.getProperty("os.arch"),
getJVMArchitecture(),
(System.getProperty("java.runtime.version", "-") + " (" + System.getProperty("java.vendor", "-") + ")"),
(System.getProperty("java.vm.version", "-") + " (" + System.getProperty("java.vm.name", "-") + ")")
);
}
public static String getJVMArchitecture() {
return System.getProperty("sun.arch.data.model");
}
public static boolean isCorrectOSArchitecture() {
boolean result = getOSArchitecture().endsWith(getJVMArchitecture());
if (!result) {
log.warn("System.getProperty(\"os.arch\") " + System.getProperty("os.arch"));
log.warn("System.getenv(\"ProgramFiles(x86)\") " + System.getenv("ProgramFiles(x86)"));
log.warn("System.getenv(\"PROCESSOR_ARCHITECTURE\")" + System.getenv("PROCESSOR_ARCHITECTURE"));
log.warn("System.getenv(\"PROCESSOR_ARCHITEW6432\") " + System.getenv("PROCESSOR_ARCHITEW6432"));
log.warn("System.getProperty(\"sun.arch.data.model\") " + System.getProperty("sun.arch.data.model"));
}
return result;
}
public static void openURI(URI uri) throws IOException {
if (!DesktopUtil.browse(uri))
throw new IOException("Failed to open URI: " + uri.toString());
}
public static void openFile(File file) throws IOException {
if (!DesktopUtil.open(file))
throw new IOException("Failed to open file: " + file.toString());
}
public static String getDownloadOfHomeDir() {
File file = new File(getSystemHomeDirectory() + "/Downloads");
if (file.exists())
return file.getAbsolutePath();
else
return getSystemHomeDirectory();
}
public static void copyToClipboard(String content) {
try {
if (content != null && content.length() > 0) {
Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(content);
clipboard.setContent(clipboardContent);
}
} catch (Throwable e) {
log.error("copyToClipboard failed " + e.getMessage());
e.printStackTrace();
}
}
public static void setThreadName(String name) {
Thread.currentThread().setName(name + "-" + new Random().nextInt(10000));
}
public static boolean isDirectory(String path) {
return new File(path).isDirectory();
}
public static String getSystemHomeDirectory() {
return Utilities.isWindows() ? System.getenv("USERPROFILE") : System.getProperty("user.home");
}
public static String encodeToHex(@Nullable byte[] bytes, boolean allowNullable) {
if (allowNullable)
return bytes != null ? Utils.HEX.encode(bytes) : "null";
else
return Utils.HEX.encode(checkNotNull(bytes, "bytes must not be null at encodeToHex"));
}
public static String bytesAsHexString(@Nullable byte[] bytes) {
return encodeToHex(bytes, true);
}
public static String encodeToHex(@Nullable byte[] bytes) {
return encodeToHex(bytes, false);
}
public static byte[] decodeFromHex(String encoded) {
return Utils.HEX.decode(encoded);
}
public static boolean isAltOrCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) {
return isAltPressed(keyCode, keyEvent) || isCtrlPressed(keyCode, keyEvent);
}
public static boolean isCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) {
return new KeyCodeCombination(keyCode, KeyCombination.SHORTCUT_DOWN).match(keyEvent) ||
new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN).match(keyEvent);
}
public static boolean isAltPressed(KeyCode keyCode, KeyEvent keyEvent) {
return new KeyCodeCombination(keyCode, KeyCombination.ALT_DOWN).match(keyEvent);
}
public static boolean isCtrlShiftPressed(KeyCode keyCode, KeyEvent keyEvent) {
return new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent);
}
public static byte[] concatenateByteArrays(byte[] array1, byte[] array2) {
return ArrayUtils.addAll(array1, array2);
}
public static Date getUTCDate(int year, int month, int dayOfMonth) {
GregorianCalendar calendar = new GregorianCalendar(year, month, dayOfMonth);
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
return calendar.getTime();
}
/**
* @param stringList String of comma separated tokens.
* @param allowWhitespace If white space inside the list tokens is allowed. If not the token will be ignored.
* @return Set of tokens
*/
public static Set<String> commaSeparatedListToSet(String stringList, boolean allowWhitespace) {
if (stringList != null) {
return Splitter.on(",")
.splitToList(allowWhitespace ? stringList : StringUtils.deleteWhitespace(stringList))
.stream()
.filter(e -> !e.isEmpty())
.collect(Collectors.toSet());
} else {
return new HashSet<>();
}
}
public static String getPathOfCodeSource() throws URISyntaxException {
return new File(Utilities.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath();
}
private static class AnnotationExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getAnnotation(JsonExclude.class) != null;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
public static String toTruncatedString(Object message) {
return toTruncatedString(message, 200, true);
}
public static String toTruncatedString(Object message, int maxLength) {
return toTruncatedString(message, maxLength, true);
}
public static String toTruncatedString(Object message, int maxLength, boolean removeLineBreaks) {
if (message == null)
return "null";
String result = StringUtils.abbreviate(message.toString(), maxLength);
if (removeLineBreaks)
return result.replace("\n", "");
return result;
}
public static String getRandomPrefix(int minLength, int maxLength) {
int length = minLength + new Random().nextInt(maxLength - minLength + 1);
String result;
switch (new Random().nextInt(3)) {
case 0:
result = RandomStringUtils.randomAlphabetic(length);
break;
case 1:
result = RandomStringUtils.randomNumeric(length);
break;
case 2:
default:
result = RandomStringUtils.randomAlphanumeric(length);
}
switch (new Random().nextInt(3)) {
case 0:
result = result.toUpperCase();
break;
case 1:
result = result.toLowerCase();
break;
case 2:
default:
}
return result;
}
public static String getShortId(String id) {
return getShortId(id, "-");
}
@SuppressWarnings("SameParameterValue")
public static String getShortId(String id, String sep) {
String[] chunks = id.split(sep);
if (chunks.length > 0)
return chunks[0];
else
return id.substring(0, Math.min(8, id.length()));
}
public static byte[] integerToByteArray(int intValue, int numBytes) {
byte[] bytes = new byte[numBytes];
for (int i = numBytes - 1; i >= 0; i--) {
bytes[i] = ((byte) (intValue & 0xFF));
intValue >>>= 8;
}
return bytes;
}
public static int byteArrayToInteger(byte[] bytes) {
int result = 0;
for (byte aByte : bytes) {
result = result << 8 | aByte & 0xff;
}
return result;
}
// Helper to filter unique elements by key
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
public static String readableFileSize(long size) {
if (size <= 0) return "0";
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.###").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
// Substitute for FormattingUtils if there is no dependency to core
public static String formatDurationAsWords(long durationMillis) {
String format = "";
String second = "second";
String minute = "minute";
String hour = "hour";
String day = "day";
String days = "days";
String hours = "hours";
String minutes = "minutes";
String seconds = "seconds";
if (durationMillis >= TimeUnit.DAYS.toMillis(1)) {
format = "d\' " + days + ", \'";
}
format += "H\' " + hours + ", \'m\' " + minutes + ", \'s\'.\'S\' " + seconds + "\'";
String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : "";
duration = StringUtils.replacePattern(duration, "^1 " + seconds + "|\\b1 " + seconds, "1 " + second);
duration = StringUtils.replacePattern(duration, "^1 " + minutes + "|\\b1 " + minutes, "1 " + minute);
duration = StringUtils.replacePattern(duration, "^1 " + hours + "|\\b1 " + hours, "1 " + hour);
duration = StringUtils.replacePattern(duration, "^1 " + days + "|\\b1 " + days, "1 " + day);
duration = duration.replace(", 0 seconds", "");
duration = duration.replace(", 0 minutes", "");
duration = duration.replace(", 0 hours", "");
duration = StringUtils.replacePattern(duration, "^0 days, ", "");
duration = StringUtils.replacePattern(duration, "^0 hours, ", "");
duration = StringUtils.replacePattern(duration, "^0 minutes, ", "");
duration = StringUtils.replacePattern(duration, "^0 seconds, ", "");
String result = duration.trim();
if (result.isEmpty()) {
result = "0.000 seconds";
}
return result;
}
}