mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge pull request #1685 from ripcurlx/merge-remaining-repositories
Merge missing repositories into mono repository
This commit is contained in:
commit
cbefd37561
29
monitor/build.gradle
Normal file
29
monitor/build.gradle
Normal 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
|
44
monitor/src/main/java/bisq/monitor/Monitor.java
Normal file
44
monitor/src/main/java/bisq/monitor/Monitor.java
Normal 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();
|
||||
}
|
||||
}
|
119
monitor/src/main/java/bisq/monitor/MonitorAppSetup.java
Normal file
119
monitor/src/main/java/bisq/monitor/MonitorAppSetup.java
Normal 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() {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
129
monitor/src/main/java/bisq/monitor/MonitorEnvironment.java
Normal file
129
monitor/src/main/java/bisq/monitor/MonitorEnvironment.java
Normal 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
134
monitor/src/main/java/bisq/monitor/MonitorMain.java
Normal file
134
monitor/src/main/java/bisq/monitor/MonitorMain.java
Normal 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();
|
||||
}
|
||||
}
|
56
monitor/src/main/java/bisq/monitor/MonitorModule.java
Normal file
56
monitor/src/main/java/bisq/monitor/MonitorModule.java
Normal 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);
|
||||
}
|
||||
}
|
26
monitor/src/main/java/bisq/monitor/MonitorOptionKeys.java
Normal file
26
monitor/src/main/java/bisq/monitor/MonitorOptionKeys.java
Normal 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";
|
||||
}
|
38
monitor/src/main/java/bisq/monitor/metrics/Metrics.java
Normal file
38
monitor/src/main/java/bisq/monitor/metrics/Metrics.java
Normal 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;
|
||||
}
|
453
monitor/src/main/java/bisq/monitor/metrics/MetricsModel.java
Normal file
453
monitor/src/main/java/bisq/monitor/metrics/MetricsModel.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
19
monitor/src/main/resources/logback.xml
Normal file
19
monitor/src/main/resources/logback.xml
Normal 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
1
pricenode/Procfile
Normal 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
|
43
pricenode/README-HEROKU.md
Normal file
43
pricenode/README-HEROKU.md
Normal 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
20
pricenode/TODO.md
Normal 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
34
pricenode/build.gradle
Normal 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
|
||||
}
|
26
pricenode/docker/Dockerfile
Normal file
26
pricenode/docker/Dockerfile
Normal 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
|
43
pricenode/docker/README.md
Normal file
43
pricenode/docker/README.md
Normal 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
|
103
pricenode/docker/cloudconfig.yml
Normal file
103
pricenode/docker/cloudconfig.yml
Normal 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
|
21
pricenode/docker/docker-compose.yml
Normal file
21
pricenode/docker/docker-compose.yml
Normal 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
|
4
pricenode/docker/installDockerCompose.sh
Normal file
4
pricenode/docker/installDockerCompose.sh
Normal 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
8
pricenode/docker/loop.sh
Normal 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
|
4
pricenode/docker/rebuildAndRestart.sh
Executable file
4
pricenode/docker/rebuildAndRestart.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
docker-compose build --no-cache && docker-compose up -d
|
||||
docker image prune -f
|
||||
docker-compose logs -f
|
1
pricenode/docker/start_node.sh
Normal file
1
pricenode/docker/start_node.sh
Normal file
@ -0,0 +1 @@
|
||||
nohup sh loop.sh
|
4
pricenode/docker/start_tor.sh
Normal file
4
pricenode/docker/start_tor.sh
Normal 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
2
pricenode/docker/torrc
Normal file
@ -0,0 +1,2 @@
|
||||
HiddenServiceDir /var/lib/tor/
|
||||
HiddenServicePort 80 127.0.0.1:8080
|
51
pricenode/src/main/java/bisq/price/Main.java
Normal file
51
pricenode/src/main/java/bisq/price/Main.java
Normal 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;
|
||||
}
|
||||
}
|
35
pricenode/src/main/java/bisq/price/PriceController.java
Normal file
35
pricenode/src/main/java/bisq/price/PriceController.java
Normal 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"));
|
||||
}
|
||||
}
|
115
pricenode/src/main/java/bisq/price/PriceProvider.java
Normal file
115
pricenode/src/main/java/bisq/price/PriceProvider.java
Normal 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;
|
||||
}
|
||||
}
|
46
pricenode/src/main/java/bisq/price/mining/FeeRate.java
Normal file
46
pricenode/src/main/java/bisq/price/mining/FeeRate.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}};
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
99
pricenode/src/main/java/bisq/price/spot/ExchangeRate.java
Normal file
99
pricenode/src/main/java/bisq/price/spot/ExchangeRate.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
108
pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java
Normal file
108
pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
32
pricenode/src/main/java/bisq/price/util/Altcoins.java
Normal file
32
pricenode/src/main/java/bisq/price/util/Altcoins.java
Normal 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());
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
1
pricenode/src/main/resources/application.properties
Normal file
1
pricenode/src/main/resources/application.properties
Normal file
@ -0,0 +1 @@
|
||||
spring.jackson.serialization.indent_output=true
|
6
pricenode/src/main/resources/banner.txt
Normal file
6
pricenode/src/main/resources/banner.txt
Normal file
@ -0,0 +1,6 @@
|
||||
__ _ _ __
|
||||
/ /_ (_)________ _ ____ _____(_)_______ ____ ____ ____/ /__
|
||||
/ __ \/ / ___/ __ `/_____/ __ \/ ___/ / ___/ _ \/ __ \/ __ \/ __ / _ \
|
||||
/ /_/ / (__ ) /_/ /_____/ /_/ / / / / /__/ __/ / / / /_/ / /_/ / __/
|
||||
/_.___/_/____/\__, / / .___/_/ /_/\___/\___/_/ /_/\____/\__,_/\___/
|
||||
/_/ /_/ ${application.formatted-version}
|
16
pricenode/src/main/resources/logback.xml
Normal file
16
pricenode/src/main/resources/logback.xml
Normal 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>
|
1
pricenode/src/main/resources/version.txt
Normal file
1
pricenode/src/main/resources/version.txt
Normal file
@ -0,0 +1 @@
|
||||
0.7.2-SNAPSHOT
|
2
pricenode/torrc
Normal file
2
pricenode/torrc
Normal file
@ -0,0 +1,2 @@
|
||||
HiddenServiceDir build/tor-hidden-service
|
||||
HiddenServicePort 80 127.0.0.1:8080
|
1
relay/Procfile
Normal file
1
relay/Procfile
Normal 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
36
relay/build.gradle
Normal 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
|
||||
}
|
127
relay/src/main/java/bisq/relay/RelayMain.java
Normal file
127
relay/src/main/java/bisq/relay/RelayMain.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
154
relay/src/main/java/bisq/relay/RelayService.java
Normal file
154
relay/src/main/java/bisq/relay/RelayService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
15
relay/src/main/resources/logback.xml
Normal file
15
relay/src/main/resources/logback.xml
Normal 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>
|
1
relay/src/main/resources/version.txt
Normal file
1
relay/src/main/resources/version.txt
Normal file
@ -0,0 +1 @@
|
||||
0.8.0
|
2
relay/torrc
Normal file
2
relay/torrc
Normal file
@ -0,0 +1,2 @@
|
||||
HiddenServiceDir build/tor-hidden-service
|
||||
HiddenServicePort 80 127.0.0.1:8080
|
23
seednode/.dockerignore
Normal file
23
seednode/.dockerignore
Normal 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
39
seednode/build.gradle
Normal 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
3
seednode/create_jar.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
./gradlew build -x test shadowJar
|
22
seednode/docker-compose.yml
Normal file
22
seednode/docker-compose.yml
Normal 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
21
seednode/docker/README.md
Normal 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
|
18
seednode/docker/development/Dockerfile
Normal file
18
seednode/docker/development/Dockerfile
Normal 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 . .
|
16
seednode/docker/prod/Dockerfile
Normal file
16
seednode/docker/prod/Dockerfile
Normal 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
5
seednode/docker/setup.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ "$SKIP_BUILD" != "true" ]; then
|
||||
./gradlew build
|
||||
fi
|
36
seednode/docker/startSeedNode.sh
Executable file
36
seednode/docker/startSeedNode.sh
Executable 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
|
46
seednode/src/main/java/bisq/seednode/SeedNode.java
Normal file
46
seednode/src/main/java/bisq/seednode/SeedNode.java
Normal 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();
|
||||
}
|
||||
}
|
100
seednode/src/main/java/bisq/seednode/SeedNodeMain.java
Normal file
100
seednode/src/main/java/bisq/seednode/SeedNodeMain.java
Normal 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();
|
||||
}
|
||||
}
|
19
seednode/src/main/resources/logback.xml
Normal file
19
seednode/src/main/resources/logback.xml
Normal 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>
|
@ -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
27
statsnode/build.gradle
Normal 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
|
71
statsnode/src/main/java/bisq/statistics/Statistics.java
Normal file
71
statsnode/src/main/java/bisq/statistics/Statistics.java
Normal 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();
|
||||
}
|
||||
}
|
93
statsnode/src/main/java/bisq/statistics/StatisticsMain.java
Normal file
93
statsnode/src/main/java/bisq/statistics/StatisticsMain.java
Normal 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();
|
||||
}
|
||||
}
|
18
statsnode/src/main/resources/logback.xml
Normal file
18
statsnode/src/main/resources/logback.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user