Merge pull request #1685 from ripcurlx/merge-remaining-repositories

Merge missing repositories into mono repository
This commit is contained in:
Christoph Atteneder 2018-09-14 16:13:35 +02:00 committed by GitHub
commit cbefd37561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 4432 additions and 0 deletions

29
monitor/build.gradle Normal file
View File

@ -0,0 +1,29 @@
plugins {
id 'java'
id 'application'
}
group = 'network.bisq'
version = '0.8.0-SNAPSHOT'
sourceCompatibility = 1.8
mainClassName = 'bisq.monitor.MonitorMain'
repositories {
jcenter()
maven { url "https://jitpack.io" }
maven { url 'https://raw.githubusercontent.com/JesusMcCloud/tor-binary/master/release/' }
}
dependencies {
compile project(':core')
compile 'com.sparkjava:spark-core:2.5.2'
compile 'net.gpedro.integrations.slack:slack-webhook:1.1.1'
compileOnly 'org.projectlombok:lombok:1.16.16'
annotationProcessor 'org.projectlombok:lombok:1.16.16'
}
build.dependsOn installDist
installDist.destinationDir = file('build/app')
distZip.enabled = false

View File

@ -0,0 +1,44 @@
/*
* 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.monitor;
import bisq.monitor.metrics.MetricsModel;
import com.google.inject.Injector;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Monitor {
@Setter
private Injector injector;
@Getter
private MetricsModel metricsModel;
public Monitor() {
}
public void startApplication() {
metricsModel = injector.getInstance(MetricsModel.class);
MonitorAppSetup appSetup = injector.getInstance(MonitorAppSetup.class);
appSetup.start();
}
}

View File

@ -0,0 +1,119 @@
/*
* 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.monitor;
import bisq.monitor.metrics.p2p.MonitorP2PService;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.SetupUtils;
import bisq.core.btc.wallet.WalletsSetup;
import bisq.network.crypto.EncryptionService;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.app.Version;
import bisq.common.crypto.KeyRing;
import bisq.common.proto.persistable.PersistedDataHost;
import javax.inject.Inject;
import java.util.ArrayList;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MonitorAppSetup {
private MonitorP2PService seedNodeMonitorP2PService;
private final WalletsSetup walletsSetup;
private PeerManager peerManager;
private final KeyRing keyRing;
private final EncryptionService encryptionService;
@Inject
public MonitorAppSetup(MonitorP2PService seedNodeMonitorP2PService,
WalletsSetup walletsSetup,
PeerManager peerManager,
KeyRing keyRing,
EncryptionService encryptionService) {
this.seedNodeMonitorP2PService = seedNodeMonitorP2PService;
this.walletsSetup = walletsSetup;
this.peerManager = peerManager;
this.keyRing = keyRing;
this.encryptionService = encryptionService;
Version.setBaseCryptoNetworkId(BisqEnvironment.getBaseCurrencyNetwork().ordinal());
Version.printVersion();
}
public void start() {
SetupUtils.checkCryptoSetup(keyRing, encryptionService, () -> {
initPersistedDataHosts();
initBasicServices();
}, throwable -> {
log.error(throwable.getMessage());
throwable.printStackTrace();
System.exit(1);
});
}
public void initPersistedDataHosts() {
ArrayList<PersistedDataHost> persistedDataHosts = new ArrayList<>();
persistedDataHosts.add(seedNodeMonitorP2PService);
persistedDataHosts.add(peerManager);
// we apply at startup the reading of persisted data but don't want to get it triggered in the constructor
persistedDataHosts.forEach(e -> {
try {
log.info("call readPersisted at " + e.getClass().getSimpleName());
e.readPersisted();
} catch (Throwable e1) {
log.error("readPersisted error", e1);
}
});
}
protected void initBasicServices() {
SetupUtils.readFromResources(seedNodeMonitorP2PService.getP2PDataStorage()).addListener((observable, oldValue, newValue) -> {
if (newValue) {
seedNodeMonitorP2PService.start(new SetupListener() {
@Override
public void onTorNodeReady() {
walletsSetup.initialize(null,
() -> log.info("walletsSetup completed"),
throwable -> log.error(throwable.toString()));
}
@Override
public void onHiddenServicePublished() {
}
@Override
public void onSetupFailed(Throwable throwable) {
}
@Override
public void onRequestCustomBridges() {
}
});
}
});
}
}

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.monitor;
import bisq.core.app.AppOptionKeys;
import bisq.core.app.BisqEnvironment;
import bisq.core.btc.BtcOptionKeys;
import bisq.core.btc.UserAgent;
import bisq.core.dao.DaoOptionKeys;
import bisq.network.NetworkOptionKeys;
import bisq.common.CommonOptionKeys;
import bisq.common.app.Version;
import bisq.common.crypto.KeyStorage;
import bisq.common.storage.Storage;
import org.springframework.core.env.JOptCommandLinePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import joptsimple.OptionSet;
import java.nio.file.Paths;
import java.util.Properties;
import static com.google.common.base.Preconditions.checkNotNull;
public class MonitorEnvironment extends BisqEnvironment {
private String slackUrlSeedChannel = "";
private String slackUrlBtcChannel = "";
private String slackUrlProviderChannel = "";
private String port;
public MonitorEnvironment(OptionSet options) {
this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull(options)));
}
public MonitorEnvironment(PropertySource commandLineProperties) {
super(commandLineProperties);
slackUrlSeedChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) :
"";
slackUrlBtcChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) :
"";
slackUrlProviderChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) :
"";
port = commandLineProperties.containsProperty(MonitorOptionKeys.PORT) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.PORT) :
"80";
// hack because defaultProperties() is called from constructor and slackUrlSeedChannel would be null there
getPropertySources().remove("bisqDefaultProperties");
getPropertySources().addLast(defaultPropertiesMonitor());
}
protected PropertySource<?> defaultPropertiesMonitor() {
return new PropertiesPropertySource(BISQ_DEFAULT_PROPERTY_SOURCE_NAME, new Properties() {
{
setProperty(CommonOptionKeys.LOG_LEVEL_KEY, logLevel);
setProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL, slackUrlSeedChannel);
setProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL, slackUrlBtcChannel);
setProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL, slackUrlProviderChannel);
setProperty(MonitorOptionKeys.PORT, port);
setProperty(NetworkOptionKeys.SEED_NODES_KEY, seedNodes);
setProperty(NetworkOptionKeys.MY_ADDRESS, myAddress);
setProperty(NetworkOptionKeys.BAN_LIST, banList);
setProperty(NetworkOptionKeys.TOR_DIR, Paths.get(btcNetworkDir, "tor").toString());
setProperty(NetworkOptionKeys.NETWORK_ID, String.valueOf(baseCurrencyNetwork.ordinal()));
setProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS, socks5ProxyBtcAddress);
setProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS, socks5ProxyHttpAddress);
setProperty(AppOptionKeys.APP_DATA_DIR_KEY, appDataDir);
setProperty(AppOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg);
setProperty(AppOptionKeys.DUMP_STATISTICS, dumpStatistics);
setProperty(AppOptionKeys.APP_NAME_KEY, appName);
setProperty(AppOptionKeys.MAX_MEMORY, maxMemory);
setProperty(AppOptionKeys.USER_DATA_DIR_KEY, userDataDir);
setProperty(AppOptionKeys.PROVIDERS, providers);
setProperty(DaoOptionKeys.RPC_USER, rpcUser);
setProperty(DaoOptionKeys.RPC_PASSWORD, rpcPassword);
setProperty(DaoOptionKeys.RPC_PORT, rpcPort);
setProperty(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT, rpcBlockNotificationPort);
setProperty(DaoOptionKeys.DUMP_BLOCKCHAIN_DATA, dumpBlockchainData);
setProperty(DaoOptionKeys.FULL_DAO_NODE, fullDaoNode);
setProperty(DaoOptionKeys.GENESIS_TX_ID, genesisTxId);
setProperty(DaoOptionKeys.GENESIS_BLOCK_HEIGHT, genesisBlockHeight);
setProperty(BtcOptionKeys.BTC_NODES, btcNodes);
setProperty(BtcOptionKeys.USE_TOR_FOR_BTC, useTorForBtc);
setProperty(BtcOptionKeys.WALLET_DIR, btcNetworkDir);
setProperty(BtcOptionKeys.USER_AGENT, userAgent);
setProperty(BtcOptionKeys.USE_ALL_PROVIDED_NODES, useAllProvidedNodes);
setProperty(BtcOptionKeys.NUM_CONNECTIONS_FOR_BTC, numConnectionForBtc);
setProperty(UserAgent.NAME_KEY, appName);
setProperty(UserAgent.VERSION_KEY, Version.VERSION);
setProperty(Storage.STORAGE_DIR, Paths.get(btcNetworkDir, "db").toString());
setProperty(KeyStorage.KEY_STORAGE_DIR, Paths.get(btcNetworkDir, "keys").toString());
}
});
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.monitor;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.BisqExecutable;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.setup.CommonSetup;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import static spark.Spark.port;
import spark.Spark;
@Slf4j
public class MonitorMain extends ExecutableForAppWithP2p {
private static final String VERSION = "1.0.1";
private Monitor monitor;
public static void main(String[] args) throws Exception {
log.info("Monitor.VERSION: " + VERSION);
BisqEnvironment.setDefaultAppName("bisq_monitor");
if (BisqExecutable.setupInitialOptionParser(args))
new MonitorMain().execute(args);
}
@Override
protected void doExecute(OptionSet options) {
super.doExecute(options);
CommonSetup.setup(this);
checkMemory(bisqEnvironment, this);
startHttpServer(bisqEnvironment.getProperty(MonitorOptionKeys.PORT));
keepRunning();
}
@Override
protected void setupEnvironment(OptionSet options) {
bisqEnvironment = new MonitorEnvironment(checkNotNull(options));
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
monitor = new Monitor();
UserThread.execute(this::onApplicationLaunched);
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new MonitorModule(bisqEnvironment);
}
@Override
protected void applyInjector() {
super.applyInjector();
monitor.setInjector(injector);
}
@Override
protected void startApplication() {
monitor.startApplication();
}
private void startHttpServer(String port) {
port(Integer.parseInt(port));
Spark.get("/", (req, res) -> {
log.info("Incoming request from: " + req.userAgent());
final String resultAsHtml = monitor.getMetricsModel().getResultAsHtml();
return resultAsHtml == null ? "Still starting up..." : resultAsHtml;
});
}
@Override
protected void customizeOptionParsing(OptionParser parser) {
super.customizeOptionParsing(parser);
parser.accepts(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL,
description("Set slack secret for seed node monitor", ""))
.withRequiredArg();
parser.accepts(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL,
description("Set slack secret for Btc node monitor", ""))
.withRequiredArg();
parser.accepts(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL,
description("Set slack secret for provider node monitor", ""))
.withRequiredArg();
parser.accepts(MonitorOptionKeys.PORT,
description("Set port to listen on", "80"))
.withRequiredArg();
}
}

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.monitor;
import bisq.monitor.metrics.p2p.MonitorP2PModule;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.network.p2p.P2PModule;
import org.springframework.core.env.Environment;
import static com.google.inject.name.Names.named;
class MonitorModule extends ModuleForAppWithP2p {
public MonitorModule(Environment environment) {
super(environment);
}
@Override
protected void configure() {
super.configure();
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.PORT)).to(environment.getRequiredProperty(MonitorOptionKeys.PORT));
}
@Override
protected void configEnvironment() {
bind(BisqEnvironment.class).toInstance((MonitorEnvironment) environment);
}
@Override
protected P2PModule p2pModule() {
return new MonitorP2PModule(environment);
}
}

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.monitor;
public class MonitorOptionKeys {
public static final String SLACK_URL_SEED_CHANNEL = "slackUrlSeedChannel";
public static final String SLACK_BTC_SEED_CHANNEL = "slackUrlBtcChannel";
public static final String SLACK_PROVIDER_SEED_CHANNEL = "slackUrlProviderChannel";
public static final String PORT = "port";
}

View File

@ -0,0 +1,38 @@
/*
* 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.monitor.metrics;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
@Getter
public class Metrics {
List<Long> requestDurations = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
List<Map<String, Integer>> receivedObjectsList = new ArrayList<>();
@Setter
long lastDataRequestTs;
@Setter
long lastDataResponseTs;
@Setter
long numRequestAttempts;
}

View File

@ -0,0 +1,453 @@
/*
* 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.monitor.metrics;
import bisq.monitor.MonitorOptionKeys;
import bisq.core.btc.BitcoinNodes;
import bisq.core.btc.wallet.WalletsSetup;
import bisq.core.locale.Res;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackMessage;
import org.bitcoinj.core.Peer;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.TimeZone;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MetricsModel {
private final DateFormat dateFormat = new SimpleDateFormat("MMMMM dd, HH:mm:ss");
@Getter
private String resultAsString;
@Getter
private String resultAsHtml;
private SeedNodeRepository seedNodeRepository;
private SlackApi slackSeedApi, slackBtcApi, slackProviderApi;
private BitcoinNodes bitcoinNodes;
@Setter
private long lastCheckTs;
private long btcNodeUptimeTs;
private int totalErrors = 0;
private HashMap<NodeAddress, Metrics> map = new HashMap<>();
private List<Peer> connectedPeers;
private Map<Tuple2<BitcoinNodes.BtcNode, Boolean>, Integer> btcNodeDownTimeMap = new HashMap<>();
private Map<Tuple2<BitcoinNodes.BtcNode, Boolean>, Integer> btcNodeUpTimeMap = new HashMap<>();
@Getter
private Set<NodeAddress> nodesInError = new HashSet<>();
@Inject
public MetricsModel(SeedNodeRepository seedNodeRepository,
BitcoinNodes bitcoinNodes,
WalletsSetup walletsSetup,
@Named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) String slackUrlSeedChannel,
@Named(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) String slackUrlBtcChannel,
@Named(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) String slackUrlProviderChannel) {
this.seedNodeRepository = seedNodeRepository;
this.bitcoinNodes = bitcoinNodes;
if (!slackUrlSeedChannel.isEmpty())
slackSeedApi = new SlackApi(slackUrlSeedChannel);
if (!slackUrlBtcChannel.isEmpty())
slackBtcApi = new SlackApi(slackUrlBtcChannel);
if (!slackUrlProviderChannel.isEmpty())
slackProviderApi = new SlackApi(slackUrlProviderChannel);
walletsSetup.connectedPeersProperty().addListener((observable, oldValue, newValue) -> {
connectedPeers = newValue;
});
}
public void addToMap(NodeAddress nodeAddress, Metrics metrics) {
map.put(nodeAddress, metrics);
}
public Metrics getMetrics(NodeAddress nodeAddress) {
return map.get(nodeAddress);
}
public void updateReport() {
if (btcNodeUptimeTs == 0)
btcNodeUptimeTs = new Date().getTime();
Map<String, Double> accumulatedValues = new HashMap<>();
final double[] items = {0};
List<Map.Entry<NodeAddress, Metrics>> entryList = map.entrySet().stream()
.sorted(Comparator.comparing(entrySet -> seedNodeRepository.getOperator(entrySet.getKey())))
.collect(Collectors.toList());
totalErrors = 0;
entryList.stream().forEach(e -> {
totalErrors += e.getValue().errorMessages.stream().filter(s -> !s.isEmpty()).count();
final List<Map<String, Integer>> receivedObjectsList = e.getValue().getReceivedObjectsList();
if (!receivedObjectsList.isEmpty()) {
items[0] += 1;
Map<String, Integer> last = receivedObjectsList.get(receivedObjectsList.size() - 1);
last.entrySet().stream().forEach(e2 -> {
int accuValue = e2.getValue();
if (accumulatedValues.containsKey(e2.getKey()))
accuValue += accumulatedValues.get(e2.getKey());
accumulatedValues.put(e2.getKey(), (double) accuValue);
});
}
});
Map<String, Double> averageValues = new HashMap<>();
accumulatedValues.entrySet().stream().forEach(e -> {
averageValues.put(e.getKey(), e.getValue() / items[0]);
});
Calendar calendar = new GregorianCalendar();
calendar.setTimeZone(TimeZone.getTimeZone("CET"));
calendar.setTimeInMillis(lastCheckTs);
final String time = calendar.getTime().toString();
StringBuilder html = new StringBuilder();
html.append("<html>" +
"<head>" +
"<style>table, th, td {border: 1px solid black;}</style>" +
"</head>" +
"<body>" +
"<h3>")
.append("Seed nodes in error: <b>" + totalErrors + "</b><br/>" +
"Last check started at: " + time + "<br/></h3>" +
"<table style=\"width:100%\">" +
"<tr>" +
"<th align=\"left\">Operator</th>" +
"<th align=\"left\">Node address</th>" +
"<th align=\"left\">Total num requests</th>" +
"<th align=\"left\">Total num errors</th>" +
"<th align=\"left\">Last request</th>" +
"<th align=\"left\">Last response</th>" +
"<th align=\"left\">RRT average</th>" +
"<th align=\"left\">Num requests (retries)</th>" +
"<th align=\"left\">Last error message</th>" +
"<th align=\"left\">Last data</th>" +
"<th align=\"left\">Data deviation last request</th>" +
"</tr>");
StringBuilder sb = new StringBuilder();
sb.append("Seed nodes in error:" + totalErrors);
sb.append("\nLast check started at: " + time + "\n");
entryList.forEach(e -> {
final Metrics metrics = e.getValue();
final List<Long> allDurations = metrics.getRequestDurations();
final String allDurationsString = allDurations.stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final OptionalDouble averageOptional = allDurations.stream().mapToLong(value -> value).average();
double durationAverage = 0;
if (averageOptional.isPresent())
durationAverage = averageOptional.getAsDouble() / 1000;
final NodeAddress nodeAddress = e.getKey();
final String operator = seedNodeRepository.getOperator(nodeAddress);
final List<String> errorMessages = metrics.getErrorMessages();
final int numErrors = (int) errorMessages.stream().filter(s -> !s.isEmpty()).count();
int numRequests = allDurations.size();
String lastErrorMsg = "";
int lastIndexOfError = 0;
for (int i = 0; i < errorMessages.size(); i++) {
final String msg = errorMessages.get(i);
if (!msg.isEmpty()) {
lastIndexOfError = i;
lastErrorMsg = "Error at request " + lastIndexOfError + ":" + msg;
}
}
// String lastErrorMsg = numErrors > 0 ? errorMessages.get(errorMessages.size() - 1) : "";
final List<Map<String, Integer>> allReceivedData = metrics.getReceivedObjectsList();
Map<String, Integer> lastReceivedData = !allReceivedData.isEmpty() ? allReceivedData.get(allReceivedData.size() - 1) : new HashMap<>();
final String lastReceivedDataString = lastReceivedData.entrySet().stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final String allReceivedDataString = allReceivedData.stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final String requestTs = metrics.getLastDataRequestTs() > 0 ? dateFormat.format(new Date(metrics.getLastDataRequestTs())) : "" + "<br/>";
final String responseTs = metrics.getLastDataResponseTs() > 0 ? dateFormat.format(new Date(metrics.getLastDataResponseTs())) : "" + "<br/>";
final String numRequestAttempts = metrics.getNumRequestAttempts() + "<br/>";
sb.append("\nOperator: ").append(operator)
.append("\nNode address: ").append(nodeAddress)
.append("\nTotal num requests: ").append(numRequests)
.append("\nTotal num errors: ").append(numErrors)
.append("\nLast request: ").append(requestTs)
.append("\nLast response: ").append(responseTs)
.append("\nRRT average: ").append(durationAverage)
.append("\nNum requests (retries): ").append(numRequestAttempts)
.append("\nLast error message: ").append(lastErrorMsg)
.append("\nLast data: ").append(lastReceivedDataString);
String colorNumErrors = lastIndexOfError == numErrors ? "black" : "red";
String colorDurationAverage = durationAverage < 30 ? "black" : "red";
html.append("<tr>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + operator + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + nodeAddress + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numRequests + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numErrors + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + requestTs + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + responseTs + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorDurationAverage + "\">" + durationAverage + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numRequestAttempts + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + lastErrorMsg + "</font> ").append("</td>")
.append("<td>").append(lastReceivedDataString).append("</td><td>");
if (!allReceivedData.isEmpty()) {
sb.append("\nData deviation last request:\n");
lastReceivedData.entrySet().stream().forEach(e2 -> {
final String dataItem = e2.getKey();
double deviation = MathUtils.roundDouble((double) e2.getValue() / averageValues.get(dataItem) * 100, 2);
String str = dataItem + ": " + deviation + "%";
sb.append(str).append("\n");
String color;
final double devAbs = Math.abs(deviation - 100);
if (devAbs < 5)
color = "black";
else if (devAbs < 10)
color = "blue";
else
color = "red";
html.append("<font color=\"" + color + "\">" + str + "</font>").append("<br/>");
if (devAbs >= 20) {
if (slackSeedApi != null)
slackSeedApi.call(new SlackMessage("Warning: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node delivers diverging results for " + dataItem + ". " +
"Please check the monitoring status page at http://seedmonitor.0-2-1.net:8080/"));
}
});
sb.append("Duration all requests: ").append(allDurationsString)
.append("\nAll data: ").append(allReceivedDataString).append("\n");
html.append("</td></tr>");
}
});
html.append("</table>");
// btc nodes
sb.append("\n\n####################################\n\nBitcoin nodes\n");
final long elapsed = new Date().getTime() - btcNodeUptimeTs;
Set<String> connectedBtcPeers = connectedPeers.stream()
.map(e -> {
String hostname = e.getAddress().getHostname();
InetAddress inetAddress = e.getAddress().getAddr();
int port = e.getAddress().getPort();
if (hostname != null)
return hostname + ":" + port;
else if (inetAddress != null)
return inetAddress.getHostAddress() + ":" + port;
else
return "";
})
.collect(Collectors.toSet());
List<BitcoinNodes.BtcNode> onionBtcNodes = new ArrayList<>(bitcoinNodes.getProvidedBtcNodes().stream()
.filter(BitcoinNodes.BtcNode::hasOnionAddress)
.collect(Collectors.toSet()));
onionBtcNodes.sort((o1, o2) -> o1.getOperator() != null && o2.getOperator() != null ?
o1.getOperator().compareTo(o2.getOperator()) : 0);
printTableHeader(html, "Onion");
printTable(html, sb, onionBtcNodes, connectedBtcPeers, elapsed, true);
html.append("</tr></table>");
List<BitcoinNodes.BtcNode> clearNetNodes = new ArrayList<>(bitcoinNodes.getProvidedBtcNodes().stream()
.filter(BitcoinNodes.BtcNode::hasClearNetAddress)
.collect(Collectors.toSet()));
clearNetNodes.sort((o1, o2) -> o1.getOperator() != null && o2.getOperator() != null ?
o1.getOperator().compareTo(o2.getOperator()) : 0);
printTableHeader(html, "Clear net");
printTable(html, sb, clearNetNodes, connectedBtcPeers, elapsed, false);
sb.append("\nConnected Bitcoin nodes: " + connectedBtcPeers + "\n");
html.append("</tr></table>");
html.append("<br>Connected Bitcoin nodes: " + connectedBtcPeers + "<br>");
btcNodeUptimeTs = new Date().getTime();
html.append("</body></html>");
resultAsString = sb.toString();
resultAsHtml = html.toString();
}
private void printTableHeader(StringBuilder html, String type) {
html.append("<br><h3>Bitcoin " + type + " nodes<h3><table style=\"width:100%\">" +
"<tr>" +
"<th align=\"left\">Operator</th>" +
"<th align=\"left\">Domain name</th>" +
"<th align=\"left\">IP address</th>" +
"<th align=\"left\">Btc node onion address</th>" +
"<th align=\"left\">UpTime</th>" +
"<th align=\"left\">DownTime</th>" +
"</tr>");
}
private void printTable(StringBuilder html, StringBuilder sb, List<BitcoinNodes.BtcNode> allBtcNodes, Set<String> connectedBtcPeers, long elapsed, boolean isOnion) {
allBtcNodes.stream().forEach(node -> {
int upTime = 0;
int downTime = 0;
Tuple2<BitcoinNodes.BtcNode, Boolean> key = new Tuple2<>(node, isOnion);
if (btcNodeUpTimeMap.containsKey(key))
upTime = btcNodeUpTimeMap.get(key);
key = new Tuple2<>(node, isOnion);
if (btcNodeDownTimeMap.containsKey(key))
downTime = btcNodeDownTimeMap.get(key);
boolean isConnected = false;
// return !connectedBtcPeers.contains(host);
if (node.hasOnionAddress() && connectedBtcPeers.contains(node.getOnionAddress() + ":" + node.getPort()))
isConnected = true;
final String clearNetHost = node.getAddress() != null ? node.getAddress() + ":" + node.getPort() : node.getHostName() + ":" + node.getPort();
if (node.hasClearNetAddress() && connectedBtcPeers.contains(clearNetHost))
isConnected = true;
if (isConnected) {
upTime += elapsed;
btcNodeUpTimeMap.put(key, upTime);
} else {
downTime += elapsed;
btcNodeDownTimeMap.put(key, downTime);
}
String upTimeString = formatDurationAsWords(upTime, true);
String downTimeString = formatDurationAsWords(downTime, true);
String colorNumErrors = isConnected ? "black" : "red";
html.append("<tr>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getOperator() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getHostName() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getAddress() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getOnionAddress() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + upTimeString + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + downTimeString + "</font> ").append("</td>");
sb.append("\nOperator: ").append(node.getOperator()).append("\n");
sb.append("Domain name: ").append(node.getHostName()).append("\n");
sb.append("IP address: ").append(node.getAddress()).append("\n");
sb.append("Btc node onion address: ").append(node.getOnionAddress()).append("\n");
sb.append("UpTime: ").append(upTimeString).append("\n");
sb.append("DownTime: ").append(downTimeString).append("\n");
});
}
public void log() {
log.info("\n\n#################################################################\n" +
resultAsString +
"#################################################################\n\n");
}
public static String formatDurationAsWords(long durationMillis, boolean showSeconds) {
String format;
String second = Res.get("time.second");
String minute = Res.get("time.minute");
String hour = Res.get("time.hour").toLowerCase();
String day = Res.get("time.day").toLowerCase();
String days = Res.get("time.days");
String hours = Res.get("time.hours");
String minutes = Res.get("time.minutes");
String seconds = Res.get("time.seconds");
if (showSeconds) {
format = "d\' " + days + ", \'H\' " + hours + ", \'m\' " + minutes + ", \'s\' " + seconds + "\'";
} else
format = "d\' " + days + ", \'H\' " + hours + ", \'m\' " + minutes + "\'";
String duration = DurationFormatUtils.formatDuration(durationMillis, format);
String tmp;
duration = " " + duration;
tmp = StringUtils.replaceOnce(duration, " 0 " + days, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + hours, "");
if (tmp.length() != duration.length()) {
tmp = StringUtils.replaceOnce(tmp, " 0 " + minutes, "");
duration = tmp;
if (tmp.length() != tmp.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 " + seconds, "");
}
}
}
if (duration.length() != 0) {
duration = duration.substring(1);
}
tmp = StringUtils.replaceOnce(duration, " 0 " + seconds, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + minutes, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + hours, "");
if (tmp.length() != duration.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 " + days, "");
}
}
}
duration = " " + duration;
duration = StringUtils.replaceOnce(duration, " 1 " + seconds, " 1 " + second);
duration = StringUtils.replaceOnce(duration, " 1 " + minutes, " 1 " + minute);
duration = StringUtils.replaceOnce(duration, " 1 " + hours, " 1 " + hour);
duration = StringUtils.replaceOnce(duration, " 1 " + days, " 1 " + day);
duration = duration.trim();
if (duration.equals(","))
duration = duration.replace(",", "");
if (duration.startsWith(" ,"))
duration = duration.replace(" ,", "");
else if (duration.startsWith(", "))
duration = duration.replace(", ", "");
return duration;
}
public void addNodesInError(NodeAddress nodeAddress) {
nodesInError.add(nodeAddress);
}
public void removeNodesInError(NodeAddress nodeAddress) {
nodesInError.remove(nodeAddress);
}
}

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.monitor.metrics.p2p;
import bisq.monitor.metrics.MetricsModel;
import bisq.network.NetworkOptionKeys;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.NetworkNodeProvider;
import bisq.network.p2p.P2PModule;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.BanList;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.peers.getdata.RequestDataManager;
import bisq.network.p2p.peers.keepalive.KeepAliveManager;
import bisq.network.p2p.peers.peerexchange.PeerExchangeManager;
import bisq.network.p2p.storage.P2PDataStorage;
import org.springframework.core.env.Environment;
import com.google.inject.Singleton;
import com.google.inject.name.Names;
import java.io.File;
import static com.google.inject.name.Names.named;
public class MonitorP2PModule extends P2PModule {
public MonitorP2PModule(Environment environment) {
super(environment);
}
@Override
protected void configure() {
bind(MetricsModel.class).in(Singleton.class);
bind(MonitorP2PService.class).in(Singleton.class);
bind(PeerManager.class).in(Singleton.class);
bind(P2PDataStorage.class).in(Singleton.class);
bind(RequestDataManager.class).in(Singleton.class);
bind(PeerExchangeManager.class).in(Singleton.class);
bind(KeepAliveManager.class).in(Singleton.class);
bind(Broadcaster.class).in(Singleton.class);
bind(BanList.class).in(Singleton.class);
bind(NetworkNode.class).toProvider(NetworkNodeProvider.class).in(Singleton.class);
bind(Socks5ProxyProvider.class).in(Singleton.class);
Boolean useLocalhostForP2P = environment.getProperty(NetworkOptionKeys.USE_LOCALHOST_FOR_P2P, boolean.class, false);
bind(boolean.class).annotatedWith(Names.named(NetworkOptionKeys.USE_LOCALHOST_FOR_P2P)).toInstance(useLocalhostForP2P);
File torDir = new File(environment.getRequiredProperty(NetworkOptionKeys.TOR_DIR));
bind(File.class).annotatedWith(named(NetworkOptionKeys.TOR_DIR)).toInstance(torDir);
// use a fixed port as arbitrator use that for his ID
Integer port = environment.getProperty(NetworkOptionKeys.PORT_KEY, int.class, 9999);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.PORT_KEY)).toInstance(port);
Integer maxConnections = environment.getProperty(NetworkOptionKeys.MAX_CONNECTIONS, int.class, P2PService.MAX_CONNECTIONS_DEFAULT);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.MAX_CONNECTIONS)).toInstance(maxConnections);
Integer networkId = environment.getProperty(NetworkOptionKeys.NETWORK_ID, int.class, 1);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.NETWORK_ID)).toInstance(networkId);
bindConstant().annotatedWith(named(NetworkOptionKeys.SEED_NODES_KEY)).to(environment.getRequiredProperty(NetworkOptionKeys.SEED_NODES_KEY));
bindConstant().annotatedWith(named(NetworkOptionKeys.MY_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.MY_ADDRESS));
bindConstant().annotatedWith(named(NetworkOptionKeys.BAN_LIST)).to(environment.getRequiredProperty(NetworkOptionKeys.BAN_LIST));
bindConstant().annotatedWith(named(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS));
bindConstant().annotatedWith(named(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS));
}
}

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.monitor.metrics.p2p;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.app.Log;
import bisq.common.proto.persistable.PersistedDataHost;
import javax.inject.Inject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class MonitorP2PService implements SetupListener, PersistedDataHost {
private final NetworkNode networkNode;
@Getter
private final P2PDataStorage p2PDataStorage;
private final MonitorRequestManager requestDataManager;
private final Socks5ProxyProvider socks5ProxyProvider;
private SetupListener listener;
private volatile boolean shutDownInProgress;
private boolean shutDownComplete;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MonitorP2PService(NetworkNode networkNode,
P2PDataStorage p2PDataStorage,
MonitorRequestManager requestDataManager,
Socks5ProxyProvider socks5ProxyProvider) {
this.networkNode = networkNode;
this.p2PDataStorage = p2PDataStorage;
this.requestDataManager = requestDataManager;
this.socks5ProxyProvider = socks5ProxyProvider;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void readPersisted() {
p2PDataStorage.readPersisted();
}
public void start(SetupListener listener) {
this.listener = listener;
networkNode.start(this);
}
public void shutDown(Runnable shutDownCompleteHandler) {
Log.traceCall();
if (!shutDownInProgress) {
shutDownInProgress = true;
if (requestDataManager != null) {
requestDataManager.shutDown();
}
if (networkNode != null) {
networkNode.shutDown(() -> {
shutDownComplete = true;
});
} else {
shutDownComplete = true;
}
} else {
log.debug("shutDown already in progress");
if (shutDownComplete) {
shutDownCompleteHandler.run();
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// SetupListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onTorNodeReady() {
socks5ProxyProvider.setSocks5ProxyInternal(networkNode.getSocksProxy());
listener.onTorNodeReady();
}
@Override
public void onHiddenServicePublished() {
checkArgument(networkNode.getNodeAddress() != null, "Address must be set when we have the hidden service ready");
requestDataManager.start();
listener.onHiddenServicePublished();
}
@Override
public void onSetupFailed(Throwable throwable) {
listener.onSetupFailed(throwable);
}
@Override
public void onRequestCustomBridges() {
listener.onRequestCustomBridges();
}
}

View File

@ -0,0 +1,300 @@
/*
* 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.monitor.metrics.p2p;
import bisq.monitor.metrics.Metrics;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.getdata.messages.GetDataRequest;
import bisq.network.p2p.peers.getdata.messages.GetDataResponse;
import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.network.NetworkPayload;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@Slf4j
class MonitorRequestHandler implements MessageListener {
private static final long TIMEOUT = 120;
private NodeAddress peersNodeAddress;
private long requestTs;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onComplete();
@SuppressWarnings("UnusedParameters")
void onFault(String errorMessage, NodeAddress nodeAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
private final P2PDataStorage dataStorage;
private final Metrics metrics;
private final Listener listener;
private Timer timeoutTimer;
private final int nonce = new Random().nextInt();
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public MonitorRequestHandler(NetworkNode networkNode, P2PDataStorage dataStorage, Metrics metrics, Listener listener) {
this.networkNode = networkNode;
this.dataStorage = dataStorage;
this.metrics = metrics;
this.listener = listener;
}
public void cancel() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestData(NodeAddress nodeAddress) {
Log.traceCall("nodeAddress=" + nodeAddress);
peersNodeAddress = nodeAddress;
requestTs = new Date().getTime();
if (!stopped) {
Set<byte[]> excludedKeys = dataStorage.getAppendOnlyDataStoreMap().entrySet().stream()
.map(e -> e.getKey().bytes)
.collect(Collectors.toSet());
GetDataRequest getDataRequest = new PreliminaryGetDataRequest(nonce, excludedKeys);
metrics.setLastDataRequestTs(System.currentTimeMillis());
if (timeoutTimer != null) {
log.warn("timeoutTimer was already set. That must not happen.");
timeoutTimer.stop();
if (DevEnv.isDevMode())
throw new RuntimeException("timeoutTimer was already set. That must not happen.");
}
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
if (!stopped) {
String errorMessage = "A timeout occurred at sending getDataRequest:" + getDataRequest +
" on nodeAddress:" + nodeAddress;
log.warn(errorMessage + " / RequestDataHandler=" + MonitorRequestHandler.this);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.trace("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
log.info("We send a PreliminaryGetDataRequest to peer {}. ", nodeAddress);
networkNode.addMessageListener(this);
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getDataRequest);
Futures.addCallback(future, new FutureCallback<Connection>() {
@Override
public void onSuccess(Connection connection) {
if (!stopped) {
log.info("Send PreliminaryGetDataRequest to " + nodeAddress + " has succeeded.");
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by an previous timeout.");
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending getDataRequest to " + nodeAddress +
" failed.\n\t" +
"getDataRequest=" + getDataRequest + "." +
"\n\tException=" + throwable.getMessage();
log.warn(errorMessage);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by an previous timeout.");
}
}
});
} else {
log.warn("We have stopped already. We ignore that requestData call.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelop, Connection connection) {
if (networkEnvelop instanceof GetDataResponse &&
connection.getPeersNodeAddressOptional().isPresent() &&
connection.getPeersNodeAddressOptional().get().equals(peersNodeAddress)) {
Log.traceCall(networkEnvelop.toString() + "\n\tconnection=" + connection);
if (!stopped) {
GetDataResponse getDataResponse = (GetDataResponse) networkEnvelop;
if (getDataResponse.getRequestNonce() == nonce) {
stopTimeoutTimer();
Map<String, Set<NetworkPayload>> payloadByClassName = new HashMap<>();
final Set<ProtectedStorageEntry> dataSet = getDataResponse.getDataSet();
dataSet.stream().forEach(e -> {
final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload();
if (protectedStoragePayload == null) {
log.warn("StoragePayload was null: {}", networkEnvelop.toString());
return;
}
// For logging different data types
String className = protectedStoragePayload.getClass().getSimpleName();
if (!payloadByClassName.containsKey(className))
payloadByClassName.put(className, new HashSet<>());
payloadByClassName.get(className).add(protectedStoragePayload);
});
Set<PersistableNetworkPayload> persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet();
if (persistableNetworkPayloadSet != null) {
persistableNetworkPayloadSet.stream().forEach(persistableNetworkPayload -> {
// For logging different data types
String className = persistableNetworkPayload.getClass().getSimpleName();
if (!payloadByClassName.containsKey(className))
payloadByClassName.put(className, new HashSet<>());
payloadByClassName.get(className).add(persistableNetworkPayload);
});
}
// Log different data types
StringBuilder sb = new StringBuilder();
sb.append("\n#################################################################\n");
sb.append("Connected to node: ").append(peersNodeAddress.getFullAddress()).append("\n");
final int items = dataSet.size() +
(persistableNetworkPayloadSet != null ? persistableNetworkPayloadSet.size() : 0);
sb.append("Received ").append(items).append(" instances\n");
Map<String, Integer> receivedObjects = new HashMap<>();
final boolean[] arbitratorReceived = new boolean[1];
payloadByClassName.entrySet().stream().forEach(e -> {
final String dataItemName = e.getKey();
// We expect always at least an Arbitrator
if (!arbitratorReceived[0] && dataItemName.equals("Arbitrator"))
arbitratorReceived[0] = true;
sb.append(dataItemName)
.append(": ")
.append(e.getValue().size())
.append("\n");
receivedObjects.put(dataItemName, e.getValue().size());
});
sb.append("#################################################################");
log.info(sb.toString());
metrics.getReceivedObjectsList().add(receivedObjects);
final long duration = new Date().getTime() - requestTs;
log.info("Requesting data took {} ms", duration);
metrics.getRequestDurations().add(duration);
metrics.getErrorMessages().add(arbitratorReceived[0] ? "" : "No Arbitrator objects received! Seed node need to be restarted!");
metrics.setLastDataResponseTs(System.currentTimeMillis());
cleanup();
connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER, listener::onComplete);
} else {
log.debug("Nonce not matching. That can happen rarely if we get a response after a canceled " +
"handshake (timeout causes connection close but peer might have sent a msg before " +
"connection was closed).\n\t" +
"We drop that message. nonce={} / requestNonce={}",
nonce, getDataResponse.getRequestNonce());
}
} else {
log.warn("We have stopped already. We ignore that onDataRequest call.");
}
}
}
public void stop() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) {
cleanup();
// We do not log every error only if it fails several times in a row.
// We do not close the connection as it might be we have opened a new connection for that peer and
// we don't want to close that. We do not know the connection at fault as the fault handler does not contain that,
// so we could only search for connections for that nodeAddress but that would close an new connection attempt.
listener.onFault(errorMessage, nodeAddress);
}
private void cleanup() {
Log.traceCall();
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
private void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
}

View File

@ -0,0 +1,283 @@
/*
* 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.monitor.metrics.p2p;
import bisq.monitor.MonitorOptionKeys;
import bisq.monitor.metrics.Metrics;
import bisq.monitor.metrics.MetricsModel;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.ConnectionListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.Timer;
import bisq.common.UserThread;
import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackMessage;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MonitorRequestManager implements ConnectionListener {
private static final long RETRY_DELAY_SEC = 30;
private static final long CLEANUP_TIMER = 60;
private static final long REQUEST_PERIOD_MIN = 10;
private static final int MAX_RETRIES = 5;
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
private final int numNodes;
private SlackApi slackApi;
private P2PDataStorage dataStorage;
private SeedNodeRepository seedNodeRepository;
private MetricsModel metricsModel;
private final Set<NodeAddress> seedNodeAddresses;
private final Map<NodeAddress, MonitorRequestHandler> handlerMap = new HashMap<>();
private Map<NodeAddress, Timer> retryTimerMap = new HashMap<>();
private Map<NodeAddress, Integer> retryCounterMap = new HashMap<>();
private boolean stopped;
private int completedRequestIndex;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MonitorRequestManager(NetworkNode networkNode,
P2PDataStorage dataStorage,
SeedNodeRepository seedNodeRepository,
MetricsModel metricsModel,
@Named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) String slackUrlSeedChannel) {
this.networkNode = networkNode;
this.dataStorage = dataStorage;
this.seedNodeRepository = seedNodeRepository;
this.metricsModel = metricsModel;
if (!slackUrlSeedChannel.isEmpty())
slackApi = new SlackApi(slackUrlSeedChannel);
this.networkNode.addConnectionListener(this);
seedNodeAddresses = new HashSet<>(seedNodeRepository.getSeedNodeAddresses());
seedNodeAddresses.stream().forEach(nodeAddress -> metricsModel.addToMap(nodeAddress, new Metrics()));
numNodes = seedNodeAddresses.size();
}
public void shutDown() {
stopped = true;
stopAllRetryTimers();
networkNode.removeConnectionListener(this);
closeAllHandlers();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void start() {
requestAllNodes();
UserThread.runPeriodically(this::requestAllNodes, REQUEST_PERIOD_MIN, TimeUnit.MINUTES);
// We want to update the data for the btc nodes more frequently
UserThread.runPeriodically(metricsModel::updateReport, 10);
}
private void requestAllNodes() {
stopAllRetryTimers();
closeAllConnections();
// we give 1 sec. for all connection shutdown
final int[] delay = {1000};
metricsModel.setLastCheckTs(System.currentTimeMillis());
seedNodeAddresses.stream().forEach(nodeAddress -> {
UserThread.runAfter(() -> requestFromNode(nodeAddress), delay[0], TimeUnit.MILLISECONDS);
delay[0] += 100;
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// ConnectionListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnection(Connection connection) {
}
@Override
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
closeHandler(connection);
}
@Override
public void onError(Throwable throwable) {
}
///////////////////////////////////////////////////////////////////////////////////////////
// RequestData
///////////////////////////////////////////////////////////////////////////////////////////
private void requestFromNode(NodeAddress nodeAddress) {
if (!stopped) {
if (!handlerMap.containsKey(nodeAddress)) {
final Metrics metrics = metricsModel.getMetrics(nodeAddress);
MonitorRequestHandler requestDataHandler = new MonitorRequestHandler(networkNode,
dataStorage,
metrics,
new MonitorRequestHandler.Listener() {
@Override
public void onComplete() {
log.trace("RequestDataHandshake of outbound connection complete. nodeAddress={}",
nodeAddress);
stopRetryTimer(nodeAddress);
retryCounterMap.remove(nodeAddress);
metrics.setNumRequestAttempts(retryCounterMap.getOrDefault(nodeAddress, 1));
// need to remove before listeners are notified as they cause the update call
handlerMap.remove(nodeAddress);
metricsModel.updateReport();
completedRequestIndex++;
if (completedRequestIndex == numNodes)
metricsModel.log();
if (metricsModel.getNodesInError().contains(nodeAddress)) {
metricsModel.removeNodesInError(nodeAddress);
if (slackApi != null)
slackApi.call(new SlackMessage("Fixed: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node is recovered."));
}
}
@Override
public void onFault(String errorMessage, NodeAddress nodeAddress) {
handlerMap.remove(nodeAddress);
stopRetryTimer(nodeAddress);
int retryCounter = retryCounterMap.getOrDefault(nodeAddress, 0);
metrics.setNumRequestAttempts(retryCounter);
if (retryCounter < MAX_RETRIES) {
log.info("We got an error at peer={}. We will try again after a delay of {} sec. error={} ",
nodeAddress, RETRY_DELAY_SEC, errorMessage);
final Timer timer = UserThread.runAfter(() -> requestFromNode(nodeAddress), RETRY_DELAY_SEC);
retryTimerMap.put(nodeAddress, timer);
retryCounterMap.put(nodeAddress, ++retryCounter);
} else {
log.warn("We got repeated errors at peer={}. error={} ",
nodeAddress, errorMessage);
metricsModel.addNodesInError(nodeAddress);
metrics.getErrorMessages().add(errorMessage + " (" + new Date().toString() + ")");
metricsModel.updateReport();
completedRequestIndex++;
if (completedRequestIndex == numNodes)
metricsModel.log();
retryCounterMap.remove(nodeAddress);
if (slackApi != null)
slackApi.call(new SlackMessage("Error: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node failed " + RETRY_DELAY_SEC + " times with error message: " + errorMessage));
}
}
});
handlerMap.put(nodeAddress, requestDataHandler);
requestDataHandler.requestData(nodeAddress);
} else {
log.warn("We have started already a requestDataHandshake to peer. nodeAddress=" + nodeAddress + "\n" +
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.");
UserThread.runAfter(() -> {
if (handlerMap.containsKey(nodeAddress)) {
MonitorRequestHandler handler = handlerMap.get(nodeAddress);
handler.stop();
handlerMap.remove(nodeAddress);
}
}, CLEANUP_TIMER);
}
} else {
log.warn("We have stopped already. We ignore that requestData call.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void closeAllConnections() {
networkNode.getAllConnections().stream().forEach(connection -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER));
}
private void stopAllRetryTimers() {
retryTimerMap.values().stream().forEach(Timer::stop);
retryTimerMap.clear();
retryCounterMap.clear();
}
private void stopRetryTimer(NodeAddress nodeAddress) {
retryTimerMap.entrySet().stream()
.filter(e -> e.getKey().equals(nodeAddress))
.forEach(e -> e.getValue().stop());
retryTimerMap.remove(nodeAddress);
}
private void closeHandler(Connection connection) {
Optional<NodeAddress> peersNodeAddressOptional = connection.getPeersNodeAddressOptional();
if (peersNodeAddressOptional.isPresent()) {
NodeAddress nodeAddress = peersNodeAddressOptional.get();
if (handlerMap.containsKey(nodeAddress)) {
handlerMap.get(nodeAddress).cancel();
handlerMap.remove(nodeAddress);
}
} else {
log.trace("closeRequestDataHandler: nodeAddress not set in connection " + connection);
}
}
private void closeAllHandlers() {
handlerMap.values().stream().forEach(MonitorRequestHandler::cancel);
handlerMap.clear();
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq.common.storage.Storage" level="WARN"/>
<logger name="bisq.common.storage.FileManager" level="WARN"/>
<logger name="com.neemre.btcdcli4j" level="WARN"/>
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
</configuration>

1
pricenode/Procfile Normal file
View File

@ -0,0 +1 @@
web: if [ "$HIDDEN" == true ]; then ./tor/bin/run_tor java -jar -Dserver.port=$PORT build/libs/bisq-pricenode.jar; else java -jar -Dserver.port=$PORT build/libs/bisq-pricenode.jar; fi

View File

@ -0,0 +1,43 @@
Deploy on Heroku
--------
Run the following commands:
heroku create
heroku buildpacks:add heroku/gradle
heroku config:set BITCOIN_AVG_PUBKEY=[your pubkey] BITCOIN_AVG_PRIVKEY=[your privkey]
git push heroku master
curl https://your-app-123456.herokuapp.com/getAllMarketPrices
To register the node as a Tor hidden service, first install the Heroku Tor buildpack:
heroku buildpacks:add https://github.com/cbeams/heroku-buildpack-tor.git
git push heroku master
> NOTE: this deployment will take a while, because the new buildpack must download and build Tor from source.
Next, generate your Tor hidden service private key and .onion address:
heroku run bash
./tor/bin/tor -f torrc
When the process reports that it is "100% bootstrapped", kill it, then copy the generated private key and .onion hostname values:
cat build/tor-hidden-service/hostname
cat build/tor-hidden-service/private_key
exit
> IMPORTANT: Save the private key value in a secure location so that this node can be re-created elsewhere with the same .onion address in the future if necessary.
Now configure the hostname and private key values as environment variables for your Heroku app:
heroku config:set HIDDEN=true HIDDEN_DOT_ONION=[your .onion] HIDDEN_PRIVATE_KEY="[your tor privkey]"
git push heroku master
When the application finishes restarting, you should still be able to access it via the clearnet, e.g. with:
curl https://your-app-123456.herokuapp.com/getAllMarketPrices
And via your Tor Browser at:
http://$YOUR_ONION/getAllMarketPrices

20
pricenode/TODO.md Normal file
View File

@ -0,0 +1,20 @@
# Refactorings
The list of stuff remaining to complete the PR at https://github.com/bisq-network/pricenode/pull/7
- Document provider implementations w/ links to API docs, etc
- Add integration tests
- Document / discuss how operators should (ideally) operate their pricenodes on a push-to-deploy model, e.g. how it's done on Heroku
## Non-refactorings
Most or all of these will become individual issues / PRs. Just capturing them here for convenience now. Not all may make sense.
- Deprecate existing get* endpoints (e.g. /getAllMarketPrices) in favor of '/exchange-rates', '/fee-estimate;
- Eliminate dependency on bisq-core (only real need now is CurrencyUtil for list of supported coins)
- Remove command line args for fee estimation params; hard-code these values and update them via commits, not via one-off changes by each operator
- Remove 'getParams' in favor of Boot actuator endpoint
- Update bisq-network/exchange to refer to 'provider' as 'pricenode'
- Invert the dependency arrangement. Move 'ProviderRepository' et al from bisq-network/exchange here into
bisq-network/pricenode and have bisq-network/exchange depend on it as a client lib
- Save bandwidth and be idiomatic by not pretty-printing json returned from /getAllMarketPrices et al

34
pricenode/build.gradle Normal file
View File

@ -0,0 +1,34 @@
plugins {
id "java"
id "org.springframework.boot" version "1.5.10.RELEASE"
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
version = file("src/main/resources/version.txt").text
jar.manifest.attributes(
"Implementation-Title": rootProject.name,
"Implementation-Version": version)
jar.archiveName "${rootProject.name}.jar"
repositories {
jcenter()
maven { url "https://jitpack.io" }
maven { url "https://raw.githubusercontent.com/JesusMcCloud/tor-binary/master/release/" }
}
dependencies {
compile project(":core")
compile("org.knowm.xchange:xchange-bitcoinaverage:4.3.3")
compile("org.knowm.xchange:xchange-coinmarketcap:4.3.3")
compile("org.knowm.xchange:xchange-poloniex:4.3.3")
compile("org.springframework.boot:spring-boot-starter-web:1.5.10.RELEASE")
compile("org.springframework.boot:spring-boot-starter-actuator")
}
task stage {
dependsOn assemble
}

View File

@ -0,0 +1,26 @@
###
# The directory of the Dockerfile should contain your 'hostname' and 'private_key' files.
# In the docker-compose.yml file you can pass the ONION_ADDRESS referenced below.
###
# pull base image
FROM openjdk:8-jdk
RUN apt-get update && apt-get install -y --no-install-recommends \
vim \
tor \
fakeroot \
sudo \
openjfx && rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/bisq-network/pricenode.git
WORKDIR /pricenode/
RUN ./gradlew assemble
COPY loop.sh start_node.sh start_tor.sh ./
COPY hostname private_key /var/lib/tor/
COPY torrc /etc/tor/
RUN chmod +x *.sh && chown debian-tor:debian-tor /etc/tor/torrc /var/lib/tor/hostname /var/lib/tor/private_key
CMD ./start_tor.sh && ./start_node.sh
#CMD tail -f /dev/null

View File

@ -0,0 +1,43 @@
Needed information to start a pricenode
==
Copy to this directory:
--
* a tor `hostname` file, containing your onion address
* a tor `private_key` file, containing the private key for your tor hidden service
Edit docker-compose.yml:
--
* fill in your public and private api keys (needs a btcaverage developer subscription)
Needed software to start a pricenode
==
* docker
* docker-compose
How to start
==
`docker-compose up -d`
How to monitor
==
See if it's running: `docker ps`
Check the logs: `docker-compose logs`
Notes when using CoreOs
==
Using CoreOs as host OS is entirely optional!
* the cloudconfig.yml file is a configuration file for starting a coreos machine
from scratch.
* when installing a Coreos server, docker-compose needs to be additionally installed next to the
already provided docker installation

View File

@ -0,0 +1,103 @@
#cloud-config
coreos:
update:
reboot-strategy: off
units:
- name: iptables-restore.service
enable: true
command: start
- name: create-swap.service
command: start
runtime: true
content: |
[Unit]
Description=Create swap file
Before=swap.service
[Service]
Type=oneshot
Environment="SWAPFILE=/2GiB.swap"
ExecStart=/usr/bin/touch ${SWAPFILE}
ExecStart=/usr/bin/chattr +C ${SWAPFILE}
ExecStart=/usr/bin/fallocate -l 2048m ${SWAPFILE}
ExecStart=/usr/bin/chmod 600 ${SWAPFILE}
ExecStart=/usr/sbin/mkswap ${SWAPFILE}
[Install]
WantedBy=multi-user.target
- name: swap.service
command: start
content: |
[Unit]
Description=Turn on swap
[Service]
Type=oneshot
Environment="SWAPFILE=/2GiB.swap"
RemainAfterExit=true
ExecStartPre=/usr/sbin/losetup -f ${SWAPFILE}
ExecStart=/usr/bin/sh -c "/sbin/swapon $(/usr/sbin/losetup -j ${SWAPFILE} | /usr/bin/cut -d : -f 1)"
ExecStop=/usr/bin/sh -c "/sbin/swapoff $(/usr/sbin/losetup -j ${SWAPFILE} | /usr/bin/cut -d : -f 1)"
ExecStopPost=/usr/bin/sh -c "/usr/sbin/losetup -d $(/usr/sbin/losetup -j ${SWAPFILE} | /usr/bin/cut -d : -f 1)"
[Install]
WantedBy=multi-user.target
- name: restart.service
content: |
[Unit]
Description=Restart docker containers
[Service]
Type=oneshot
ExecStart=/home/core/docker/restartContainers.sh
- name: restart.timer
command: start
content: |
[Unit]
Description=Restarts the app container 2 times a week
[Timer]
OnCalendar=Mon,Thu *-*-* 6:0:0
write_files:
- path: /etc/sysctl.d/swap.conf
permissions: 0644
owner: root
content: |
vm.swappiness=10
vm.vfs_cache_pressure=50
write_files:
- path: /etc/ssh/sshd_config
permissions: 0600
owner: root
content: |
# Use most defaults for sshd configuration.
UsePrivilegeSeparation sandbox
Subsystem sftp internal-sftp
UseDNS no
PermitRootLogin no
AllowUsers core
AuthenticationMethods publickey
write_files:
- path: /var/lib/iptables/rules-save
permissions: 0644
owner: 'root:root'
content: |
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -i eth1 -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 0 -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT
COMMIT
# the last line of the file needs to be a blank line or a comment

View File

@ -0,0 +1,21 @@
version: '3'
# Fill in your own BTCAVERAGE public and private keys
services:
pricenode:
restart: unless-stopped
build:
context: .
image: bisq:pricenode
ports:
- 80:80
- 8080:8080
environment:
- BTCAVERAGE_PRIVKEY=!!!!!!!!!!!!!!!!!!!!!!!!! YOUR PRIVATE KEY !!!!!!!!!!!!!!!!!!!!!!!!!!!
- BTCAVERAGE_PUBKEY=!!!!!!!!!!!!!!!!!!!!!!!!!! YOUR PUBKEY !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
entropy:
restart: always
image: harbur/haveged:1.7c-1
container_name: haveged-entropy
privileged: true

View File

@ -0,0 +1,4 @@
#!/bin/bash
mkdir -p /opt/bin
curl -L `curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r '.assets[].browser_download_url | select(contains("Linux") and contains("x86_64"))'` > /opt/bin/docker-compose
chmod +x /opt/bin/docker-compose

8
pricenode/docker/loop.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
while true
do
echo `date` "(Re)-starting node"
BITCOIN_AVG_PUBKEY=$BTCAVERAGE_PUBKEY BITCOIN_AVG_PRIVKEY=$BTCAVERAGE_PRIVKEY java -jar ./build/libs/bisq-pricenode.jar 2 2
echo `date` "node terminated unexpectedly!!"
sleep 3
done

View File

@ -0,0 +1,4 @@
#!/bin/sh
docker-compose build --no-cache && docker-compose up -d
docker image prune -f
docker-compose logs -f

View File

@ -0,0 +1 @@
nohup sh loop.sh

View File

@ -0,0 +1,4 @@
#!/bin/bash
# sudo -u debian-tor
nohup sudo -u debian-tor tor > /dev/null 2>errors_tor.log &

2
pricenode/docker/torrc Normal file
View File

@ -0,0 +1,2 @@
HiddenServiceDir /var/lib/tor/
HiddenServicePort 80 127.0.0.1:8080

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.price;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Properties;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
new SpringApplicationBuilder(Main.class)
.properties(bisqProperties())
.run(args);
}
private static Properties bisqProperties() {
Properties props = new Properties();
File propsFile = new File(System.getenv("HOME"), ".config/bisq.properties");
if (propsFile.exists()) {
try {
props.load(new FileInputStream(propsFile));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
return props;
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.price;
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class PriceController {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@ModelAttribute
public void logRequest(HttpServletRequest request) {
log.info("Incoming {} request from: {}", request.getServletPath(), request.getHeader("User-Agent"));
}
}

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.price;
import org.springframework.context.SmartLifecycle;
import java.time.Duration;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class PriceProvider<T> implements SmartLifecycle, Supplier<T> {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
private final Timer timer = new Timer(true);
protected final Duration refreshInterval;
private T cachedResult;
public PriceProvider(Duration refreshInterval) {
this.refreshInterval = refreshInterval;
log.info("will refresh every {}", refreshInterval);
}
@Override
public final T get() {
if (!isRunning())
throw new IllegalStateException("call start() before calling get()");
return cachedResult;
}
@Override
public final void start() {
// we call refresh outside the context of a timer once at startup to ensure that
// any exceptions thrown get propagated and cause the application to halt
refresh();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
refresh();
} catch (Throwable t) {
// we only log scheduled calls to refresh that fail to ensure that
// the application does *not* halt, assuming the failure is temporary
// and on the side of the upstream price provider, eg. BitcoinAverage
log.warn("refresh failed", t);
}
}
}, refreshInterval.toMillis(), refreshInterval.toMillis());
}
private void refresh() {
long ts = System.currentTimeMillis();
cachedResult = doGet();
log.info("refresh took {} ms.", (System.currentTimeMillis() - ts));
onRefresh();
}
protected abstract T doGet();
protected void onRefresh() {
}
@Override
public void stop() {
timer.cancel();
}
@Override
public void stop(Runnable callback) {
stop();
callback.run();
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public boolean isRunning() {
return cachedResult != null;
}
@Override
public int getPhase() {
return 0;
}
}

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.price.mining;
/**
* A value object representing the mining fee rate for a given base currency.
*/
public class FeeRate {
private final String currency;
private final long price;
private final long timestamp;
public FeeRate(String currency, long price, long timestamp) {
this.currency = currency;
this.price = price;
this.timestamp = timestamp;
}
public String getCurrency() {
return currency;
}
public long getPrice() {
return price;
}
public long getTimestamp() {
return timestamp;
}
}

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.price.mining;
import bisq.price.PriceController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
class FeeRateController extends PriceController {
private final FeeRateService feeRateService;
public FeeRateController(FeeRateService feeRateService) {
this.feeRateService = feeRateService;
}
@GetMapping(path = "/getFees")
public Map<String, Object> getFees() {
return feeRateService.getFees();
}
}

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.price.mining;
import bisq.price.PriceProvider;
import java.time.Duration;
/**
* Abstract base class for providers of mining {@link FeeRate} data.
*/
public abstract class FeeRateProvider extends PriceProvider<FeeRate> {
public FeeRateProvider(Duration refreshInterval) {
super(refreshInterval);
}
}

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.price.mining;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* High-level mining {@link FeeRate} operations.
*/
@Service
class FeeRateService {
private final Set<FeeRateProvider> providers;
public FeeRateService(Set<FeeRateProvider> providers) {
this.providers = providers;
}
public Map<String, Object> getFees() {
Map<String, Long> metadata = new HashMap<>();
Map<String, Long> allFeeRates = new HashMap<>();
providers.forEach(p -> {
FeeRate feeRate = p.get();
String currency = feeRate.getCurrency();
if ("BTC".equals(currency)) {
metadata.put("bitcoinFeesTs", feeRate.getTimestamp());
}
allFeeRates.put(currency.toLowerCase() + "TxFee", feeRate.getPrice());
});
return new HashMap<String, Object>() {{
putAll(metadata);
put("dataMap", allFeeRates);
}};
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.price.mining.providers;
import bisq.price.PriceController;
import bisq.price.mining.FeeRate;
import bisq.price.mining.FeeRateProvider;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.CommandLinePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
@Component
class BitcoinFeeRateProvider extends FeeRateProvider {
private static final long MIN_FEE_RATE = 10; // satoshi/byte
private static final long MAX_FEE_RATE = 1000;
private static final int DEFAULT_MAX_BLOCKS = 2;
private static final int DEFAULT_REFRESH_INTERVAL = 2;
private final RestTemplate restTemplate = new RestTemplate();
private final int maxBlocks;
public BitcoinFeeRateProvider(Environment env) {
super(Duration.ofMinutes(refreshInterval(env)));
this.maxBlocks = maxBlocks(env);
}
protected FeeRate doGet() {
return new FeeRate("BTC", getEstimatedFeeRate(), Instant.now().getEpochSecond());
}
private long getEstimatedFeeRate() {
return getFeeRatePredictions()
.filter(p -> p.get("maxDelay") <= maxBlocks)
.findFirst()
.map(p -> p.get("maxFee"))
.map(r -> {
log.info("latest fee rate prediction is {} sat/byte", r);
return r;
})
.map(r -> Math.max(r, MIN_FEE_RATE))
.map(r -> Math.min(r, MAX_FEE_RATE))
.orElse(MIN_FEE_RATE);
}
private Stream<Map<String, Long>> getFeeRatePredictions() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
// now using /fees/list because /fees/recommended estimates were too high
.fromUriString("https://bitcoinfees.earn.com/api/v1/fees/list")
.build().toUri())
.header("User-Agent", "") // required to avoid 403
.build(),
new ParameterizedTypeReference<Map<String, List<Map<String, Long>>>>() {
}
).getBody().entrySet().stream()
.flatMap(e -> e.getValue().stream());
}
private static Optional<String[]> args(Environment env) {
return Optional.ofNullable(
env.getProperty(CommandLinePropertySource.DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME, String[].class));
}
private static int maxBlocks(Environment env) {
return args(env)
.filter(args -> args.length >= 1)
.map(args -> Integer.valueOf(args[0]))
.orElse(DEFAULT_MAX_BLOCKS);
}
private static long refreshInterval(Environment env) {
return args(env)
.filter(args -> args.length >= 2)
.map(args -> Integer.valueOf(args[1]))
.orElse(DEFAULT_REFRESH_INTERVAL);
}
@RestController
class Controller extends PriceController {
@GetMapping(path = "/getParams")
public String getParams() {
return String.format("%s;%s", maxBlocks, refreshInterval.toMillis());
}
}
}

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.price.mining.providers;
import org.springframework.stereotype.Component;
@Component
class DashFeeRateProvider extends FixedFeeRateProvider {
public DashFeeRateProvider() {
super("DASH", 50);
}
}

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.price.mining.providers;
import org.springframework.stereotype.Component;
@Component
class DogecoinFeeRateProvider extends FixedFeeRateProvider {
public DogecoinFeeRateProvider() {
super("DOGE", 5_000_000);
}
}

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.price.mining.providers;
import bisq.price.mining.FeeRate;
import bisq.price.mining.FeeRateProvider;
import java.time.Duration;
import java.time.Instant;
abstract class FixedFeeRateProvider extends FeeRateProvider {
private final String currency;
private final long price;
public FixedFeeRateProvider(String currency, long price) {
super(Duration.ofDays(1));
this.currency = currency;
this.price = price;
}
protected final FeeRate doGet() {
return new FeeRate(currency, price, Instant.now().getEpochSecond());
}
}

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.price.mining.providers;
import org.springframework.stereotype.Component;
@Component
class LitecoinFeeRateProvider extends FixedFeeRateProvider {
public LitecoinFeeRateProvider() {
super("LTC", 500);
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.price.spot;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Objects;
/**
* A value object representing the spot price in bitcoin for a given currency at a given
* time as reported by a given provider.
*/
public class ExchangeRate {
private final String currency;
private final double price;
private final long timestamp;
private final String provider;
public ExchangeRate(String currency, BigDecimal price, Date timestamp, String provider) {
this(
currency,
price.doubleValue(),
timestamp.getTime(),
provider
);
}
public ExchangeRate(String currency, double price, long timestamp, String provider) {
this.currency = currency;
this.price = price;
this.timestamp = timestamp;
this.provider = provider;
}
@JsonProperty(value = "currencyCode", index = 1)
public String getCurrency() {
return currency;
}
@JsonProperty(value = "price", index = 2)
public double getPrice() {
return this.price;
}
@JsonProperty(value = "timestampSec", index = 3)
public long getTimestamp() {
return this.timestamp;
}
@JsonProperty(value = "provider", index = 4)
public String getProvider() {
return provider;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExchangeRate exchangeRate = (ExchangeRate) o;
return Double.compare(exchangeRate.price, price) == 0 &&
timestamp == exchangeRate.timestamp &&
Objects.equals(currency, exchangeRate.currency) &&
Objects.equals(provider, exchangeRate.provider);
}
@Override
public int hashCode() {
return Objects.hash(currency, price, timestamp, provider);
}
@Override
public String toString() {
return "ExchangeRate{" +
"currency='" + currency + '\'' +
", price=" + price +
", timestamp=" + timestamp +
", provider=" + provider +
'}';
}
}

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.price.spot;
import bisq.price.PriceController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
class ExchangeRateController extends PriceController {
private final ExchangeRateService exchangeRateService;
public ExchangeRateController(ExchangeRateService exchangeRateService) {
this.exchangeRateService = exchangeRateService;
}
@GetMapping(path = "/getAllMarketPrices")
public Map<String, Object> getAllMarketPrices() {
return exchangeRateService.getAllMarketPrices();
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.price.spot;
import bisq.price.PriceProvider;
import java.time.Duration;
import java.util.Set;
/**
* Abstract base class for providers of bitcoin {@link ExchangeRate} data. Implementations
* are marked with the {@link org.springframework.stereotype.Component} annotation in
* order to be discovered via classpath scanning. Implementations are also marked with the
* {@link org.springframework.core.annotation.Order} annotation to determine their
* precedence over each other in the case of two or more services returning exchange rate
* data for the same currency pair. In such cases, results from the provider with the
* higher order value will taking precedence over the provider with a lower value,
* presuming that such providers are being iterated over in an ordered list.
*
* @see ExchangeRateService#ExchangeRateService(java.util.List)
*/
public abstract class ExchangeRateProvider extends PriceProvider<Set<ExchangeRate>> {
private final String name;
private final String prefix;
public ExchangeRateProvider(String name, String prefix, Duration refreshInterval) {
super(refreshInterval);
this.name = name;
this.prefix = prefix;
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix;
}
@Override
protected void onRefresh() {
get().stream()
.filter(e -> "USD".equals(e.getCurrency()) || "LTC".equals(e.getCurrency()))
.forEach(e -> log.info("BTC/{}: {}", e.getCurrency(), e.getPrice()));
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.price.spot;
import bisq.price.spot.providers.BitcoinAverage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* High-level {@link ExchangeRate} data operations.
*/
@Service
class ExchangeRateService {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
private final List<ExchangeRateProvider> providers;
/**
* Construct an {@link ExchangeRateService} with a list of all
* {@link ExchangeRateProvider} implementations discovered via classpath scanning.
*
* @param providers all {@link ExchangeRateProvider} implementations in ascending
* order of precedence
*/
public ExchangeRateService(List<ExchangeRateProvider> providers) {
this.providers = providers;
}
public Map<String, Object> getAllMarketPrices() {
Map<String, Object> metadata = new LinkedHashMap<>();
Map<String, ExchangeRate> allExchangeRates = new LinkedHashMap<>();
providers.forEach(p -> {
Set<ExchangeRate> exchangeRates = p.get();
metadata.putAll(getMetadata(p, exchangeRates));
exchangeRates.forEach(e ->
allExchangeRates.put(e.getCurrency(), e)
);
});
return new LinkedHashMap<String, Object>() {{
putAll(metadata);
// Use a sorted list by currency code to make comparision of json data between different
// price nodes easier
List<ExchangeRate> values = new ArrayList<>(allExchangeRates.values());
values.sort(Comparator.comparing(ExchangeRate::getCurrency));
put("data", values);
}};
}
private Map<String, Object> getMetadata(ExchangeRateProvider provider, Set<ExchangeRate> exchangeRates) {
Map<String, Object> metadata = new LinkedHashMap<>();
// In case a provider is not available we still want to deliver the data of the other providers, so we catch
// a possible exception and leave timestamp at 0. The Bisq app will check if the timestamp is in a tolerance
// window and if it is too old it will show that the price is not available.
long timestamp = 0;
try {
timestamp = getTimestamp(provider, exchangeRates);
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();
}
if (provider instanceof BitcoinAverage.Local) {
metadata.put("btcAverageTs", timestamp);
}
String prefix = provider.getPrefix();
metadata.put(prefix + "Ts", timestamp);
metadata.put(prefix + "Count", exchangeRates.size());
return metadata;
}
private long getTimestamp(ExchangeRateProvider provider, Set<ExchangeRate> exchangeRates) {
return exchangeRates.stream()
.filter(e -> provider.getName().equals(e.getProvider()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No exchange rate data found for " + provider.getName()))
.getTimestamp();
}
}

View File

@ -0,0 +1,158 @@
/*
* 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.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import org.knowm.xchange.bitcoinaverage.dto.marketdata.BitcoinAverageTicker;
import org.knowm.xchange.bitcoinaverage.dto.marketdata.BitcoinAverageTickers;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* See the BitcoinAverage API documentation at https://apiv2.bitcoinaverage.com/#ticker-data-all
*/
public abstract class BitcoinAverage extends ExchangeRateProvider {
/**
* Max number of requests allowed per month on the BitcoinAverage developer plan.
* Note the actual max value is 45,000; we use the more conservative value below to
* ensure we do not exceed it. See https://bitcoinaverage.com/en/plans.
*/
private static final double MAX_REQUESTS_PER_MONTH = 42_514;
private final RestTemplate restTemplate = new RestTemplate();
private final String symbolSet;
private String pubKey;
private Mac mac;
/**
* @param symbolSet "global" or "local"; see https://apiv2.bitcoinaverage.com/#supported-currencies
*/
public BitcoinAverage(String name, String prefix, double pctMaxRequests, String symbolSet, Environment env) {
super(name, prefix, refreshIntervalFor(pctMaxRequests));
this.symbolSet = symbolSet;
this.pubKey = env.getRequiredProperty("BITCOIN_AVG_PUBKEY");
this.mac = initMac(env.getRequiredProperty("BITCOIN_AVG_PRIVKEY"));
}
@Override
public Set<ExchangeRate> doGet() {
return getTickersKeyedByCurrency().entrySet().stream()
.filter(e -> supportedCurrency(e.getKey()))
.map(e ->
new ExchangeRate(
e.getKey(),
e.getValue().getLast(),
e.getValue().getTimestamp(),
this.getName()
)
)
.collect(Collectors.toSet());
}
private boolean supportedCurrency(String currencyCode) {
// ignore Venezuelan bolivars as the "official" exchange rate is just wishful thinking
// we should use this API with a custom provider instead: http://api.bitcoinvenezuela.com/1
return !"VEF".equals(currencyCode);
}
private Map<String, BitcoinAverageTicker> getTickersKeyedByCurrency() {
// go from a map with keys like "BTCUSD", "BTCVEF"
return getTickersKeyedByCurrencyPair().entrySet().stream()
// to a map with keys like "USD", "VEF"
.collect(Collectors.toMap(e -> e.getKey().substring(3), Map.Entry::getValue));
}
private Map<String, BitcoinAverageTicker> getTickersKeyedByCurrencyPair() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
.fromUriString("https://apiv2.bitcoinaverage.com/indices/{symbol-set}/ticker/all?crypto=BTC")
.buildAndExpand(symbolSet)
.toUri())
.header("X-signature", getAuthSignature())
.build(),
BitcoinAverageTickers.class
).getBody().getTickers();
}
protected String getAuthSignature() {
String payload = String.format("%s.%s", Instant.now().getEpochSecond(), pubKey);
return String.format("%s.%s", payload, Hex.toHexString(mac.doFinal(payload.getBytes())));
}
private static Mac initMac(String privKey) {
String algorithm = "HmacSHA256";
SecretKey secretKey = new SecretKeySpec(privKey.getBytes(), algorithm);
try {
Mac mac = Mac.getInstance(algorithm);
mac.init(secretKey);
return mac;
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
}
}
private static Duration refreshIntervalFor(double pctMaxRequests) {
long requestsPerMonth = (long) (MAX_REQUESTS_PER_MONTH * pctMaxRequests);
return Duration.ofDays(31).dividedBy(requestsPerMonth);
}
@Component
@Order(1)
public static class Global extends BitcoinAverage {
public Global(Environment env) {
super("BTCA_G", "btcAverageG", 0.3, "global", env);
}
}
@Component
@Order(2)
public static class Local extends BitcoinAverage {
public Local(Environment env) {
super("BTCA_L", "btcAverageL", 0.7, "local", env);
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import bisq.price.util.Altcoins;
import org.knowm.xchange.coinmarketcap.dto.marketdata.CoinMarketCapTicker;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
@Order(3)
class CoinMarketCap extends ExchangeRateProvider {
private final RestTemplate restTemplate = new RestTemplate();
public CoinMarketCap() {
super("CMC", "coinmarketcap", Duration.ofMinutes(5)); // large data structure, so don't request it too often
}
@Override
public Set<ExchangeRate> doGet() {
return getTickers()
.filter(t -> Altcoins.ALL_SUPPORTED.contains(t.getIsoCode()))
.map(t ->
new ExchangeRate(
t.getIsoCode(),
t.getPriceBTC(),
t.getLastUpdated(),
this.getName()
)
)
.collect(Collectors.toSet());
}
private Stream<CoinMarketCapTicker> getTickers() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
.fromUriString("https://api.coinmarketcap.com/v1/ticker/?limit=200").build()
.toUri())
.build(),
new ParameterizedTypeReference<List<CoinMarketCapTicker>>() {
}
).getBody().stream();
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import bisq.price.util.Altcoins;
import org.knowm.xchange.currency.Currency;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.poloniex.dto.marketdata.PoloniexMarketData;
import org.knowm.xchange.poloniex.dto.marketdata.PoloniexTicker;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
@Order(4)
class Poloniex extends ExchangeRateProvider {
private final RestTemplate restTemplate = new RestTemplate();
public Poloniex() {
super("POLO", "poloniex", Duration.ofMinutes(1));
}
@Override
public Set<ExchangeRate> doGet() {
Date timestamp = new Date(); // Poloniex tickers don't include their own timestamp
return getTickers()
.filter(t -> t.getCurrencyPair().base.equals(Currency.BTC))
.filter(t -> Altcoins.ALL_SUPPORTED.contains(t.getCurrencyPair().counter.getCurrencyCode()))
.map(t ->
new ExchangeRate(
t.getCurrencyPair().counter.getCurrencyCode(),
t.getPoloniexMarketData().getLast(),
timestamp,
this.getName()
)
)
.collect(Collectors.toSet());
}
private Stream<PoloniexTicker> getTickers() {
return getTickersKeyedByCurrencyPair().entrySet().stream()
.map(e -> {
String pair = e.getKey();
PoloniexMarketData data = e.getValue();
String[] symbols = pair.split("_"); // e.g. BTC_USD => [BTC, USD]
return new PoloniexTicker(data, new CurrencyPair(symbols[0], symbols[1]));
});
}
private Map<String, PoloniexMarketData> getTickersKeyedByCurrencyPair() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
.fromUriString("https://poloniex.com/public?command=returnTicker").build()
.toUri())
.build(),
new ParameterizedTypeReference<Map<String, PoloniexMarketData>>() {
}
).getBody();
}
}

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.price.util;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency;
import java.util.Set;
import java.util.stream.Collectors;
public abstract class Altcoins {
public static final Set<String> ALL_SUPPORTED =
CurrencyUtil.getAllSortedCryptoCurrencies().stream()
.map(TradeCurrency::getCode)
.collect(Collectors.toSet());
}

View File

@ -0,0 +1,55 @@
/*
* 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.price.util;
import bisq.price.PriceController;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.InputStreamReader;
@RestController
class VersionController extends PriceController implements InfoContributor {
private final String version;
public VersionController(@Value("classpath:version.txt") Resource versionTxt) throws IOException {
this.version = FileCopyUtils.copyToString(
new InputStreamReader(
versionTxt.getInputStream()
)
).trim();
}
@GetMapping(path = "/getVersion")
public String getVersion() {
return version;
}
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("version", version);
}
}

View File

@ -0,0 +1 @@
spring.jackson.serialization.indent_output=true

View File

@ -0,0 +1,6 @@
__ _ _ __
/ /_ (_)________ _ ____ _____(_)_______ ____ ____ ____/ /__
/ __ \/ / ___/ __ `/_____/ __ \/ ___/ / ___/ _ \/ __ \/ __ \/ __ / _ \
/ /_/ / (__ ) /_/ /_____/ /_/ / / / / /__/ __/ / / / /_/ / /_/ / __/
/_.___/_/____/\__, / / .___/_/ /_/\___/\___/_/ /_/\____/\__,_/\___/
/_/ /_/ ${application.formatted-version}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq" level="INFO"/>
<logger name="org.springframework.boot.context.embedded.tomcat" level="INFO"/>
</configuration>

View File

@ -0,0 +1 @@
0.7.2-SNAPSHOT

2
pricenode/torrc Normal file
View File

@ -0,0 +1,2 @@
HiddenServiceDir build/tor-hidden-service
HiddenServicePort 80 127.0.0.1:8080

1
relay/Procfile Normal file
View File

@ -0,0 +1 @@
web: if [ "$HIDDEN" == true ]; then ./tor/bin/run_tor java -jar -Dserver.port=$PORT build/libs/bisq-relay.jar; else java -jar -Dserver.port=$PORT build/libs/bisq-relay.jar; fi

36
relay/build.gradle Normal file
View File

@ -0,0 +1,36 @@
plugins {
id "java"
id "org.springframework.boot" version "1.5.10.RELEASE"
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
version = file("src/main/resources/version.txt").text
jar.manifest.attributes(
"Implementation-Title": rootProject.name,
"Implementation-Version": version)
jar.archiveName "${rootProject.name}.jar"
repositories {
mavenLocal()
jcenter()
maven { url "https://jitpack.io" }
maven { url "https://raw.githubusercontent.com/JesusMcCloud/tor-binary/master/release/" }
}
dependencies {
compile project(":common")
compile("com.sparkjava:spark-core:2.5.2")
compile("com.turo:pushy:0.13.2")
compile("com.google.firebase:firebase-admin:6.2.0")
compileOnly 'org.projectlombok:lombok:1.16.16'
//annotationProcessor 'org.projectlombok:lombok:1.16.16'
}
task stage {
dependsOn assemble
}

View File

@ -0,0 +1,127 @@
/*
* 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.relay;
import bisq.common.app.Log;
import bisq.common.util.Utilities;
import org.apache.commons.codec.binary.Hex;
import java.io.File;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import static spark.Spark.get;
import static spark.Spark.port;
public class RelayMain {
private static final Logger log = LoggerFactory.getLogger(RelayMain.class);
private static final String VERSION = "0.1.0";
private static RelayService relayService;
static {
// Need to set default locale initially otherwise we get problems at non-english OS
Locale.setDefault(new Locale("en", Locale.getDefault().getCountry()));
Utilities.removeCryptographyRestrictions();
}
/**
* @param args Pass port as program argument if other port than default port 8080 is wanted.
*/
public static void main(String[] args) {
final String logPath = System.getProperty("user.home") + File.separator + "provider";
Log.setup(logPath);
Log.setLevel(Level.INFO);
log.info("Log files under: " + logPath);
log.info("RelayVersion.VERSION: " + VERSION);
Utilities.printSysInfo();
String appleCertPwPath;
if (args.length > 0)
appleCertPwPath = args[0];
else
throw new RuntimeException("You need to set the path to the password text file for the Apple push certificate as first argument.");
String appleCertPath;
if (args.length > 1)
appleCertPath = args[1];
else
throw new RuntimeException("You need to set the path to the Apple push certificate as second argument.");
String appleBundleId;
if (args.length > 2)
appleBundleId = args[2];
else
throw new RuntimeException("You need to set the Apple bundle ID as third argument.");
String androidCertPath;
if (args.length > 3)
androidCertPath = args[3];
else
throw new RuntimeException("You need to set the Android certificate path as 4th argument.");
int port = 8080;
if (args.length > 4)
port = Integer.parseInt(args[4]);
port(port);
relayService = new RelayService(appleCertPwPath, appleCertPath, appleBundleId, androidCertPath);
handleRelay();
keepRunning();
}
private static void handleRelay() {
get("/relay", (request, response) -> {
log.info("Incoming relay request from: " + request.userAgent());
boolean isAndroid = request.queryParams("isAndroid").equalsIgnoreCase("true");
boolean useSound = request.queryParams("snd").equalsIgnoreCase("true");
String token = new String(Hex.decodeHex(request.queryParams("token").toCharArray()), "UTF-8");
String encryptedMessage = new String(Hex.decodeHex(request.queryParams("msg").toCharArray()), "UTF-8");
log.info("isAndroid={}\nuseSound={}\napsTokenHex={}\nencryptedMessage={}", isAndroid, useSound, token,
encryptedMessage);
if (isAndroid) {
return relayService.sendAndroidMessage(token, encryptedMessage, useSound);
} else {
boolean isProduction = request.queryParams("isProduction").equalsIgnoreCase("true");
boolean isContentAvailable = request.queryParams("isContentAvailable").equalsIgnoreCase("true");
return relayService.sendAppleMessage(isProduction, isContentAvailable, token, encryptedMessage, useSound);
}
});
}
private static void keepRunning() {
//noinspection InfiniteLoopStatement
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
}

View File

@ -0,0 +1,154 @@
/*
* 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.relay;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;
import lombok.extern.slf4j.Slf4j;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import com.turo.pushy.apns.ApnsClient;
import com.turo.pushy.apns.ApnsClientBuilder;
import com.turo.pushy.apns.PushNotificationResponse;
import com.turo.pushy.apns.util.ApnsPayloadBuilder;
import com.turo.pushy.apns.util.SimpleApnsPushNotification;
import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
@Slf4j
class RelayService {
private static final String ANDROID_DATABASE_URL = "https://bisqnotifications.firebaseio.com";
// Used in Bisq app to check for success state. We won't want a code dependency just for that string so we keep it
// duplicated in core and here. Must not be changed.
private static final String SUCCESS = "success";
private final String appleBundleId;
private ApnsClient productionApnsClient;
private ApnsClient devApnsClient; // used for iOS development in XCode
RelayService(String appleCertPwPath, String appleCertPath, String appleBundleId, String androidCertPath) {
this.appleBundleId = appleBundleId;
setupForAndroid(androidCertPath);
setupForApple(appleCertPwPath, appleCertPath);
}
private void setupForAndroid(String androidCertPath) {
try {
InputStream androidCertStream = new FileInputStream(androidCertPath);
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.fromStream(androidCertStream))
.setDatabaseUrl(ANDROID_DATABASE_URL)
.build();
FirebaseApp.initializeApp(options);
} catch (IOException e) {
log.error(e.toString());
e.printStackTrace();
}
}
private void setupForApple(String appleCertPwPath, String appleCertPath) {
try {
InputStream certInputStream = new FileInputStream(appleCertPwPath);
Scanner scanner = new Scanner(certInputStream);
String password = scanner.next();
productionApnsClient = new ApnsClientBuilder()
.setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
.setClientCredentials(new File(appleCertPath), password)
.build();
devApnsClient = new ApnsClientBuilder()
.setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST)
.setClientCredentials(new File(appleCertPath), password)
.build();
} catch (IOException e) {
log.error(e.toString());
e.printStackTrace();
}
}
String sendAppleMessage(boolean isProduction, boolean isContentAvailable, String apsTokenHex, String encryptedMessage, boolean useSound) {
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
if (useSound)
payloadBuilder.setSoundFileName("default");
payloadBuilder.setAlertBody("Bisq notification");
payloadBuilder.setContentAvailable(isContentAvailable);
payloadBuilder.addCustomProperty("encrypted", encryptedMessage);
final String payload = payloadBuilder.buildWithDefaultMaximumLength();
log.info("payload " + payload);
SimpleApnsPushNotification simpleApnsPushNotification = new SimpleApnsPushNotification(apsTokenHex, appleBundleId, payload);
ApnsClient apnsClient = isProduction ? productionApnsClient : devApnsClient;
PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>>
notificationFuture = apnsClient.sendNotification(simpleApnsPushNotification);
try {
PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = notificationFuture.get();
if (pushNotificationResponse.isAccepted()) {
log.info("Push notification accepted by APNs gateway.");
return SUCCESS;
} else {
String msg1 = "Notification rejected by the APNs gateway: " +
pushNotificationResponse.getRejectionReason();
String msg2 = "";
if (pushNotificationResponse.getTokenInvalidationTimestamp() != null)
msg2 = " and the token is invalid as of " +
pushNotificationResponse.getTokenInvalidationTimestamp();
log.info(msg1 + msg2);
return "Error: " + msg1 + msg2;
}
} catch (InterruptedException | ExecutionException e) {
log.error(e.toString());
e.printStackTrace();
return "Error: " + e.toString();
}
}
String sendAndroidMessage(String apsTokenHex, String encryptedMessage, boolean useSound) {
Message.Builder messageBuilder = Message.builder();
Notification notification = new Notification("Bisq", "Notification");
messageBuilder.setNotification(notification);
messageBuilder.putData("encrypted", encryptedMessage);
messageBuilder.setToken(apsTokenHex);
if (useSound)
messageBuilder.putData("sound", "default");
Message message = messageBuilder.build();
try {
FirebaseMessaging firebaseMessaging = FirebaseMessaging.getInstance();
firebaseMessaging.send(message);
return SUCCESS;
} catch (FirebaseMessagingException e) {
log.error(e.toString());
e.printStackTrace();
return "Error: " + e.toString();
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq" level="INFO"/>
</configuration>

View File

@ -0,0 +1 @@
0.8.0

2
relay/torrc Normal file
View File

@ -0,0 +1,2 @@
HiddenServiceDir build/tor-hidden-service
HiddenServicePort 80 127.0.0.1:8080

23
seednode/.dockerignore Normal file
View File

@ -0,0 +1,23 @@
docs/
.git/
.dockerignore
.editorconfig
.travis.yml
docker-compose.yml
docker/development/
docker/prod/
docker/README.md
# Gradle
.gradle
build
# IDEA
.idea
*.iml
# macOS
.DS_Store
# Vim
*.sw[op]

39
seednode/build.gradle Normal file
View File

@ -0,0 +1,39 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'maven'
apply plugin: 'com.github.johnrengelman.shadow'
group = 'network.bisq'
version = '0.8.0-SNAPSHOT'
sourceCompatibility = 1.8
mainClassName = 'bisq.seednode.SeedNodeMain'
sourceSets.main.resources.srcDirs += ['src/main/java'] // to copy fxml and css files
repositories {
jcenter()
maven { url "https://jitpack.io" }
maven { url 'https://raw.githubusercontent.com/JesusMcCloud/tor-binary/master/release/' }
}
dependencies {
compile project(':core')
runtime 'org.bouncycastle:bcprov-jdk15on:1.56'
compileOnly 'org.projectlombok:lombok:1.16.16'
annotationProcessor 'org.projectlombok:lombok:1.16.16'
}
build.dependsOn installDist
installDist.destinationDir = file('build/app')
distZip.enabled = false

3
seednode/create_jar.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
./gradlew build -x test shadowJar

View File

@ -0,0 +1,22 @@
version: '2.1'
services:
seednode:
build:
context: .
dockerfile: docker/development/Dockerfile
image: bisq-seednode
ports:
- 8000:8000
environment:
- NODE_PORT=8000
- BASE_CURRENCY_NETWORK=BTC_REGTEST
- SEED_NODES=seednode:8000
- MY_ADDRESS=seednode:8000
- USE_LOCALHOST_FOR_P2P=true
volumes:
- m2:/root/.m2
volumes:
m2:
name: m2

21
seednode/docker/README.md Normal file
View File

@ -0,0 +1,21 @@
# bisq-seednode docker
Both images use the same [startSeedNode.sh](startSeedNode.sh) script so inspect it to see what environment variables you can tweak.
## Production image
In order to build image:
docker build . -f docker/prod/Dockerfile -t bisq/seednode
Run:
docker run bisq/seednode
You might want to mount tor hidden service directory:
docker run -v /your/tor/dir:/root/.local/share/seednode/btc_mainnet/tor/hiddenservice/ bisq/seednode
## Development image
docker-compose build

View File

@ -0,0 +1,18 @@
###
# WARNING!!! THIS IMAGE IS FOR D E V E L O P M E N T USE ONLY!
###
FROM openjdk:8-jdk
RUN apt-get update && apt-get install -y --no-install-recommends \
openjfx && rm -rf /var/lib/apt/lists/*
WORKDIR /bisq-seednode
CMD ./docker/startSeedNode.sh
ENV APP_NAME=seednode
ENV NODE_PORT=8000
EXPOSE 8000
COPY . .

View File

@ -0,0 +1,16 @@
FROM openjdk:8-jdk
RUN apt-get update && apt-get install -y --no-install-recommends \
openjfx && rm -rf /var/lib/apt/lists/*
WORKDIR /bisq-seednode
CMD ./docker/startSeedNode.sh
ENV APP_NAME=seednode
ENV NODE_PORT=8000
EXPOSE 8000
COPY . .
RUN ./docker/setup.sh
ENV SKIP_BUILD=true

5
seednode/docker/setup.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
if [ "$SKIP_BUILD" != "true" ]; then
./gradlew build
fi

View File

@ -0,0 +1,36 @@
#!/bin/bash
SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
SETUP_SCRIPT=${SCRIPT_DIR}/setup.sh
source ${SETUP_SCRIPT}
ARGS=""
if [ ! -z "$BASE_CURRENCY_NETWORK" ]; then
ARGS="$ARGS --baseCurrencyNetwork=$BASE_CURRENCY_NETWORK"
fi
if [ ! -z "$MAX_CONNECTIONS" ]; then
ARGS="$ARGS --maxConnections=$MAX_CONNECTIONS"
fi
if [ ! -z "$NODE_PORT" ]; then
ARGS="$ARGS --nodePort=$NODE_PORT"
fi
if [ ! -z "$APP_NAME" ]; then
ARGS="$ARGS --appName=$APP_NAME"
fi
if [ ! -z "$SEED_NODES" ]; then
ARGS="$ARGS --seedNodes=$SEED_NODES"
fi
if [ ! -z "$BTC_NODES" ]; then
ARGS="$ARGS --btcNodes=$BTC_NODES"
fi
if [ ! -z "$USE_LOCALHOST_FOR_P2P" ]; then
ARGS="$ARGS --useLocalhostForP2P=$USE_LOCALHOST_FOR_P2P"
fi
if [ ! -z "$MY_ADDRESS" ]; then
ARGS="$ARGS --myAddress=${MY_ADDRESS}"
elif [ ! -z "$ONION_ADDRESS" ]; then
ARGS="$ARGS --myAddress=${ONION_ADDRESS}.onion:$NODE_PORT"
fi
JAVA_OPTS='-Xms1800m -Xmx1800m' ./build/app/bin/bisq-seednode $ARGS

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.seednode;
import bisq.core.app.misc.AppSetup;
import bisq.core.app.misc.AppSetupWithP2P;
import bisq.core.app.misc.AppSetupWithP2PAndDAO;
import bisq.core.dao.DaoOptionKeys;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNode {
@Setter
private Injector injector;
private AppSetup appSetup;
public SeedNode() {
}
public void startApplication() {
Boolean fullDaoNode = injector.getInstance(Key.get(Boolean.class, Names.named(DaoOptionKeys.FULL_DAO_NODE)));
appSetup = fullDaoNode ? injector.getInstance(AppSetupWithP2PAndDAO.class) : injector.getInstance(AppSetupWithP2P.class);
appSetup.start();
}
}

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.seednode;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.BisqExecutable;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.app.Capabilities;
import bisq.common.setup.CommonSetup;
import joptsimple.OptionSet;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final String VERSION = "0.8.0";
private SeedNode seedNode;
public static void main(String[] args) throws Exception {
log.info("SeedNode.VERSION: " + VERSION);
BisqEnvironment.setDefaultAppName("bisq_seednode");
if (BisqExecutable.setupInitialOptionParser(args))
new SeedNodeMain().execute(args);
}
@Override
protected void doExecute(OptionSet options) {
super.doExecute(options);
checkMemory(bisqEnvironment, this);
CommonSetup.setup(this);
keepRunning();
}
@Override
protected void addCapabilities() {
Capabilities.addCapability(Capabilities.Capability.SEED_NODE.ordinal());
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
seedNode = new SeedNode();
UserThread.execute(this::onApplicationLaunched);
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new ModuleForAppWithP2p(bisqEnvironment);
}
@Override
protected void applyInjector() {
super.applyInjector();
seedNode.setInjector(injector);
}
@Override
protected void startApplication() {
seedNode.startApplication();
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq.common.storage.Storage" level="WARN"/>
<logger name="bisq.common.storage.FileManager" level="WARN"/>
<logger name="com.neemre.btcdcli4j" level="WARN"/>
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
</configuration>

View File

@ -3,5 +3,10 @@ include 'common'
include 'p2p'
include 'core'
include 'desktop'
include 'monitor'
include 'pricenode'
include 'relay'
include 'seednode'
include 'statsnode'
rootProject.name = 'bisq'

27
statsnode/build.gradle Normal file
View File

@ -0,0 +1,27 @@
plugins {
id 'java'
id 'application'
}
group = 'network.bisq'
version = '0.8.0-SNAPSHOT'
sourceCompatibility = 1.8
mainClassName = 'bisq.statistics.StatisticsMain'
repositories {
jcenter()
maven { url "https://jitpack.io" }
maven { url 'https://raw.githubusercontent.com/JesusMcCloud/tor-binary/master/release/' }
}
dependencies {
compile project(':core')
compileOnly 'org.projectlombok:lombok:1.16.16'
annotationProcessor 'org.projectlombok:lombok:1.16.16'
}
build.dependsOn installDist
installDist.destinationDir = file('build/app')
distZip.enabled = false

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.statistics;
import bisq.core.app.misc.AppSetup;
import bisq.core.app.misc.AppSetupWithP2P;
import bisq.core.offer.OfferBookService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.P2PService;
import com.google.inject.Injector;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Statistics {
@Setter
private Injector injector;
private OfferBookService offerBookService; // pin to not get GC'ed
private PriceFeedService priceFeedService;
private TradeStatisticsManager tradeStatisticsManager;
private P2PService p2pService;
private AppSetup appSetup;
public Statistics() {
}
public void startApplication() {
p2pService = injector.getInstance(P2PService.class);
offerBookService = injector.getInstance(OfferBookService.class);
priceFeedService = injector.getInstance(PriceFeedService.class);
tradeStatisticsManager = injector.getInstance(TradeStatisticsManager.class);
// We need the price feed for market based offers
priceFeedService.setCurrencyCode("USD");
p2pService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onUpdatedDataReceived() {
// we need to have tor ready
log.info("onBootstrapComplete: we start requestPriceFeed");
priceFeedService.requestPriceFeed(price -> log.info("requestPriceFeed. price=" + price),
(errorMessage, throwable) -> log.warn("Exception at requestPriceFeed: " + throwable.getMessage()));
tradeStatisticsManager.onAllServicesInitialized();
}
});
appSetup = injector.getInstance(AppSetupWithP2P.class);
appSetup.start();
}
}

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.statistics;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.BisqExecutable;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.setup.CommonSetup;
import joptsimple.OptionSet;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class StatisticsMain extends ExecutableForAppWithP2p {
private static final String VERSION = "0.6.1";
private Statistics statistics;
public static void main(String[] args) throws Exception {
log.info("Statistics.VERSION: " + VERSION);
BisqEnvironment.setDefaultAppName("bisq_statistics");
if (BisqExecutable.setupInitialOptionParser(args))
new StatisticsMain().execute(args);
}
@Override
protected void doExecute(OptionSet options) {
super.doExecute(options);
CommonSetup.setup(this);
checkMemory(bisqEnvironment, this);
keepRunning();
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
statistics = new Statistics();
UserThread.execute(this::onApplicationLaunched);
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new ModuleForAppWithP2p(bisqEnvironment);
}
@Override
protected void applyInjector() {
super.applyInjector();
statistics.setInjector(injector);
}
@Override
protected void startApplication() {
statistics.startApplication();
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq.common.storage.Storage" level="WARN"/>
<logger name="bisq.common.storage.FileManager" level="WARN"/>
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
</configuration>