Merge branch 'master' into release/v1.9.17

This commit is contained in:
Alejandro García 2024-06-25 20:33:51 +00:00
commit a090f2aa27
No known key found for this signature in database
GPG Key ID: F806F422E222AA02
83 changed files with 16172 additions and 73 deletions

7
.gitignore vendored
View File

@ -35,3 +35,10 @@ deploy
/apitest/src/main/resources/dao-setup*
/.run
/regtest/data_dirs
/build-logic/app-start-plugin/out/*
/build-logic/docker-image-builder/out/*
/build-logic/gradle-tasks/out/*
/build-logic/packaging/out/*
/build-logic/regtest/out/*
/build-logic/toolchain-resolver/out/*
/build-logic/tor-binary/out/*

25
btcnodemonitor/README.md Normal file
View File

@ -0,0 +1,25 @@
## Bitcoin node monitor
This is a simple headless node with a http server which connects periodically to the Bisq-provided Bitcoin nodes and
disconnect quickly afterwards.
### Run Bitcoin node monitor
Run the Gradle task:
```sh
./gradlew btcnodemonitor:run
```
Or create a run scrip by:
```sh
./gradlew btcnodemonitor:startBisqApp
```
And then run:
```sh
./bisq-btcnodemonitor
```

View File

@ -0,0 +1,37 @@
plugins {
id 'bisq.application'
id 'bisq.gradle.app_start_plugin.AppStartPlugin'
}
mainClassName = 'bisq.btcnodemonitor.BtcNodeMonitorMain'
distTar.enabled = true
dependencies {
implementation enforcedPlatform(project(':platform'))
implementation project(':proto')
implementation project(':common')
implementation project(':core')
implementation project(':p2p')
annotationProcessor libs.lombok
compileOnly libs.javax.annotation
compileOnly libs.lombok
implementation libs.logback.classic
implementation libs.logback.core
implementation libs.google.guava
implementation libs.apache.commons.lang3
implementation libs.jetbrains.annotations
implementation libs.slf4j.api
implementation(libs.netlayer.tor.external) {
exclude(module: 'slf4j-api')
}
implementation(libs.bitcoinj) {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'slf4j-api')
}
implementation libs.spark.core
}

View File

@ -0,0 +1,66 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.btcnodemonitor;
import bisq.core.btc.nodes.BtcNodes;
import bisq.common.config.Config;
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
import bisq.btcnodemonitor.btc.PeerConncetionModel;
import bisq.btcnodemonitor.btc.PeerGroupService;
import bisq.btcnodemonitor.server.SimpleHttpServer;
import bisq.btcnodemonitor.socksProxy.ProxySetup;
@Slf4j
public class BtcNodeMonitor {
private final PeerGroupService peerGroupService;
private final ProxySetup proxySetup;
private final SimpleHttpServer simpleHttpServer;
public BtcNodeMonitor(Config config) {
PeerConncetionModel peerConncetionModel = new PeerConncetionModel(new BtcNodes().getProvidedBtcNodes(), this::onChange);
simpleHttpServer = new SimpleHttpServer(config, peerConncetionModel);
proxySetup = new ProxySetup(config);
peerGroupService = new PeerGroupService(config, peerConncetionModel);
}
public void onChange() {
simpleHttpServer.onChange();
}
public CompletableFuture<Void> start() {
return simpleHttpServer.start()
.thenCompose(nil -> proxySetup.createSocksProxy())
.thenAccept(peerGroupService::applySocks5Proxy)
.thenCompose(nil -> peerGroupService.start())
.thenRun(peerGroupService::connectToAll);
}
public CompletableFuture<Void> shutdown() {
return peerGroupService.shutdown()
.thenCompose(nil -> proxySetup.shutdown())
.thenCompose(nil -> simpleHttpServer.shutdown());
}
}

View File

@ -0,0 +1,73 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.btcnodemonitor;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
import bisq.common.setup.CommonSetup;
import bisq.common.setup.GracefulShutDownHandler;
import bisq.common.util.SingleThreadExecutorUtils;
import bisq.common.util.Utilities;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BtcNodeMonitorMain implements GracefulShutDownHandler {
public static void main(String[] args) {
new BtcNodeMonitorMain(args);
}
private final Config config;
@Getter
private final BtcNodeMonitor btcNodeMonitor;
public BtcNodeMonitorMain(String[] args) {
config = new Config("bisq_btc_node_monitor", Utilities.getUserDataDir(), args);
CommonSetup.setup(config, this);
configUserThread();
btcNodeMonitor = new BtcNodeMonitor(config);
btcNodeMonitor.start().join();
keepRunning();
}
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
btcNodeMonitor.shutdown().join();
System.exit(0);
resultHandler.handleResult();
}
private void keepRunning() {
try {
Thread.currentThread().setName("BtcNodeMonitorMain");
Thread.currentThread().join();
} catch (InterruptedException e) {
log.error("BtcNodeMonitorMain Thread interrupted", e);
gracefulShutDown(() -> {
});
}
}
private void configUserThread() {
UserThread.setExecutor(SingleThreadExecutorUtils.getSingleThreadExecutor("UserThread"));
}
}

View File

@ -0,0 +1,191 @@
/*
* 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.btcnodemonitor.btc;
import org.bitcoinj.core.Peer;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.VersionMessage;
import com.google.common.base.Joiner;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
@Getter
public class PeerConncetionInfo {
private final List<ConnectionAttempt> connectionAttempts = new ArrayList<>();
private final PeerAddress peerAddress;
private final Runnable onChangeHandler;
private Optional<ConnectionAttempt> currentConnectionAttempt = Optional.empty();
public PeerConncetionInfo(PeerAddress peerAddress, Runnable onChangeHandler) {
this.peerAddress = peerAddress;
this.onChangeHandler = onChangeHandler;
}
public ConnectionAttempt newConnectionAttempt(Peer peer) {
currentConnectionAttempt = Optional.of(new ConnectionAttempt(peer, onChangeHandler));
connectionAttempts.add(currentConnectionAttempt.get());
onChangeHandler.run();
return currentConnectionAttempt.get();
}
public String getAddress() {
InetAddress inetAddress = peerAddress.getAddr();
if (inetAddress != null) {
return inetAddress.getHostAddress();
} else {
return peerAddress.getHostname();
}
}
public String getShortId() {
return getAddress().substring(0, 12) + "...";
}
public int getNumConnectionAttempts() {
return connectionAttempts.size();
}
public int getNumConnections() {
return (int) connectionAttempts.stream().filter(e -> e.isConnected).count();
}
public int getNumDisconnections() {
return (int) connectionAttempts.stream().filter(e -> !e.isConnected).count();
}
public int getNumFailures() {
return (int) connectionAttempts.stream().filter(e -> e.exception.isPresent()).count();
}
public int getNumSuccess() {
return (int) connectionAttempts.stream().filter(e -> e.versionMessage.isPresent()).count();
}
public List<ConnectionAttempt> getReverseConnectionAttempts() {
List<ConnectionAttempt> reverseConnectionAttempts = new ArrayList<>(connectionAttempts);
Collections.reverse(reverseConnectionAttempts);
return reverseConnectionAttempts;
}
public Optional<ConnectionAttempt> getLastSuccessfulConnected() {
return getReverseConnectionAttempts().stream().filter(e -> e.versionMessage.isPresent()).findFirst();
}
public int getIndex(ConnectionAttempt connectionAttempt) {
return connectionAttempts.indexOf(connectionAttempt);
}
public long getLastSuccessfulConnectTime() {
return getReverseConnectionAttempts().stream().filter(e -> e.versionMessage.isPresent()).findFirst()
.map(ConnectionAttempt::getDurationUntilConnection)
.orElse(0L);
}
public double getAverageTimeToConnect() {
return connectionAttempts.stream().mapToLong(ConnectionAttempt::getDurationUntilConnection).average().orElse(0d);
}
public Optional<String> getLastExceptionMessage() {
return getLastAttemptWithException()
.flatMap(ConnectionAttempt::getException)
.map(Throwable::getMessage);
}
public Optional<ConnectionAttempt> getLastAttemptWithException() {
return getReverseConnectionAttempts().stream()
.filter(e -> e.exception.isPresent())
.findFirst();
}
public String getAllExceptionMessages() {
return Joiner.on(",\n")
.join(getReverseConnectionAttempts().stream()
.filter(e -> e.exception.isPresent())
.flatMap(e -> e.getException().stream())
.map(Throwable::getMessage)
.collect(Collectors.toList()));
}
public double getFailureRate() {
if (getNumConnectionAttempts() == 0) {
return 0;
}
return getNumFailures() / (double) getNumConnectionAttempts();
}
@Override
public String toString() {
return getShortId();
}
@Getter
public static class ConnectionAttempt {
private final Peer peer;
private final Runnable updateHandler;
private final long connectTs;
private boolean isConnected;
@Setter
private long connectionStartedTs;
@Setter
private long connectionSuccessTs;
@Setter
private long durationUntilConnection;
@Setter
private long durationUntilDisConnection;
@Setter
private long durationUntilFailure;
private Optional<Throwable> exception = Optional.empty();
private Optional<VersionMessage> versionMessage = Optional.empty();
public ConnectionAttempt(Peer peer, Runnable updateHandler) {
this.peer = peer;
this.updateHandler = updateHandler;
connectTs = System.currentTimeMillis();
}
public void onConnected() {
// We clone to avoid change of fields when disconnect happens
VersionMessage peerVersionMessage = peer.getPeerVersionMessage().duplicate();
versionMessage = Optional.of(peerVersionMessage);
isConnected = true;
updateHandler.run();
}
public void onDisconnected() {
isConnected = false;
updateHandler.run();
}
public void onException(Throwable exception) {
this.exception = Optional.of(exception);
isConnected = false;
updateHandler.run();
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.btcnodemonitor.btc;
import bisq.core.btc.nodes.BtcNodes;
import org.bitcoinj.core.PeerAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Getter;
@Getter
public class PeerConncetionModel {
private final Map<String, PeerConncetionInfo> map = new HashMap<>();
private final List<BtcNodes.BtcNode> providedBtcNodes;
private final Runnable onChangeHandler;
public PeerConncetionModel(List<BtcNodes.BtcNode> providedBtcNodes, Runnable onChangeHandler) {
this.providedBtcNodes = providedBtcNodes;
this.onChangeHandler = onChangeHandler;
}
public void fill(Set<PeerAddress> peerAddresses) {
map.clear();
map.putAll(peerAddresses.stream()
.map(peerAddress -> new PeerConncetionInfo(peerAddress, onChangeHandler))
.collect(Collectors.toMap(PeerConncetionInfo::getAddress, e -> e)));
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.btcnodemonitor.btc;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.util.SingleThreadExecutorUtils;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Peer;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VersionMessage;
import org.bitcoinj.net.BlockingClientManager;
import org.bitcoinj.net.ClientConnectionManager;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.net.SocketAddress;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@Slf4j
public class PeerConnection {
private final Context context;
private final int disconnectIntervalSec;
private final int reconnectIntervalSec;
private final ClientConnectionManager clientConnectionManager;
private final NetworkParameters params;
private final int connectTimeoutMillis;
private final int vMinRequiredProtocolVersion;
private final PeerConncetionInfo peerConncetionInfo;
private Optional<Timer> disconnectScheduler = Optional.empty();
private Optional<Timer> reconnectScheduler = Optional.empty();
private final AtomicBoolean shutdownCalled = new AtomicBoolean();
public PeerConnection(Context context,
PeerConncetionInfo peerConncetionInfo,
BlockingClientManager blockingClientManager,
int connectTimeoutMillis,
int disconnectIntervalSec,
int reconnectIntervalSec) {
this.context = context;
this.peerConncetionInfo = peerConncetionInfo;
this.clientConnectionManager = blockingClientManager;
this.connectTimeoutMillis = connectTimeoutMillis;
this.disconnectIntervalSec = disconnectIntervalSec;
this.reconnectIntervalSec = reconnectIntervalSec;
this.params = context.getParams();
vMinRequiredProtocolVersion = params.getProtocolVersionNum(NetworkParameters.ProtocolVersion.BLOOM_FILTER);
}
public CompletableFuture<Void> shutdown() {
shutdownCalled.set(true);
disconnectScheduler.ifPresent(Timer::stop);
reconnectScheduler.ifPresent(Timer::stop);
return CompletableFuture.runAsync(() -> {
log.info("shutdown {}", peerConncetionInfo);
Context.propagate(context);
disconnect();
}, SingleThreadExecutorUtils.getSingleThreadExecutor("shutdown-" + peerConncetionInfo.getShortId()));
}
public void connect() {
CompletableFuture.runAsync(() -> {
log.info("connect {}", peerConncetionInfo);
Context.propagate(context);
Peer peer = createPeer(peerConncetionInfo.getPeerAddress());
PeerConncetionInfo.ConnectionAttempt connectionAttempt = peerConncetionInfo.newConnectionAttempt(peer);
long ts = System.currentTimeMillis();
connectionAttempt.setConnectionStartedTs(ts);
try {
peer.addConnectedEventListener((peer1, peerCount) -> {
connectionAttempt.setDurationUntilConnection(System.currentTimeMillis() - ts);
connectionAttempt.setConnectionSuccessTs(System.currentTimeMillis());
connectionAttempt.onConnected();
startAutoDisconnectAndReconnect();
});
peer.addDisconnectedEventListener((peer1, peerCount) -> {
long passed = System.currentTimeMillis() - ts;
// Timeout is not handled as error in bitcoinj, but it simply disconnects
// If we had a successful connect before we got versionMessage set, otherwise its from an error.
if (connectionAttempt.getVersionMessage().isEmpty()) {
connectionAttempt.setDurationUntilFailure(passed);
connectionAttempt.onException(new RuntimeException("Connection failed"));
} else {
connectionAttempt.setDurationUntilDisConnection(passed);
connectionAttempt.onDisconnected();
}
startAutoDisconnectAndReconnect();
});
openConnection(peer).join();
} catch (Exception exception) {
log.warn("Error at opening connection to peer {}", peerConncetionInfo, exception);
connectionAttempt.setDurationUntilFailure(System.currentTimeMillis() - ts);
connectionAttempt.onException(exception);
startAutoDisconnectAndReconnect();
}
}, SingleThreadExecutorUtils.getSingleThreadExecutor("connect-" + peerConncetionInfo.getShortId()));
}
private CompletableFuture<Void> disconnect() {
return peerConncetionInfo.getCurrentConnectionAttempt()
.map(currentConnectionAttempt -> CompletableFuture.runAsync(() -> {
log.info("disconnect {}", peerConncetionInfo);
Context.propagate(context);
currentConnectionAttempt.getPeer().close();
},
SingleThreadExecutorUtils.getSingleThreadExecutor("disconnect-" + peerConncetionInfo.getShortId())))
.orElse(CompletableFuture.completedFuture(null));
}
private void startAutoDisconnectAndReconnect() {
if (shutdownCalled.get()) {
return;
}
disconnectScheduler.ifPresent(Timer::stop);
disconnectScheduler = Optional.of(UserThread.runAfter(() -> {
if (shutdownCalled.get()) {
return;
}
disconnect()
.thenRun(() -> {
if (shutdownCalled.get()) {
return;
}
reconnectScheduler.ifPresent(Timer::stop);
reconnectScheduler = Optional.of(UserThread.runAfter(() -> {
if (shutdownCalled.get()) {
return;
}
connect();
}, reconnectIntervalSec));
});
}, disconnectIntervalSec));
}
private Peer createPeer(PeerAddress address) {
Peer peer = new Peer(params, getVersionMessage(address), address, null, 0, 0);
peer.setMinProtocolVersion(vMinRequiredProtocolVersion);
peer.setSocketTimeout(connectTimeoutMillis);
return peer;
}
private CompletableFuture<SocketAddress> openConnection(Peer peer) {
CompletableFuture<SocketAddress> future = new CompletableFuture<>();
ListenableFuture<SocketAddress> listenableFuture = clientConnectionManager.openConnection(peer.getAddress().toSocketAddress(), peer);
Futures.addCallback(listenableFuture, new FutureCallback<>() {
@Override
public void onSuccess(SocketAddress result) {
future.complete(result);
}
@Override
public void onFailure(@NotNull Throwable throwable) {
future.completeExceptionally(throwable);
}
}, MoreExecutors.directExecutor());
return future;
}
private VersionMessage getVersionMessage(PeerAddress address) {
VersionMessage versionMessage = new VersionMessage(params, 0);
versionMessage.bestHeight = 0;
versionMessage.time = Utils.currentTimeSeconds();
versionMessage.receivingAddr = address;
versionMessage.receivingAddr.setParent(versionMessage);
return versionMessage;
}
}

View File

@ -0,0 +1,150 @@
/*
* 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.btcnodemonitor.btc;
import bisq.core.btc.nodes.BtcNodes;
import bisq.core.btc.nodes.BtcNodesRepository;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.btc.nodes.ProxySocketFactory;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.util.SingleThreadExecutorUtils;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.net.BlockingClientManager;
import org.bitcoinj.utils.Threading;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import java.time.Duration;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PeerGroupService {
private final NetworkParameters params;
private final LocalBitcoinNode localBitcoinNode;
private final Context context;
private final PeerConncetionModel peerConncetionModel;
private Set<PeerConnection> peerConnections;
private BlockingClientManager blockingClientManager;
public PeerGroupService(Config config, PeerConncetionModel peerConncetionModel) {
this.peerConncetionModel = peerConncetionModel;
params = Config.baseCurrencyNetworkParameters();
context = new Context(params);
PeerGroup.setIgnoreHttpSeeds(true);
Threading.USER_THREAD = UserThread.getExecutor();
localBitcoinNode = new LocalBitcoinNode(config);
}
public void applySocks5Proxy(Optional<Socks5Proxy> socks5Proxy) {
int connectTimeoutMillis;
int disconnectIntervalSec;
int reconnectIntervalSec;
Set<PeerAddress> peerAddresses;
if (localBitcoinNode.shouldBeUsed()) {
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), params.getPort());
peerAddresses = Set.of(new PeerAddress(address));
connectTimeoutMillis = 1000;
disconnectIntervalSec = 5;
reconnectIntervalSec = 5;
blockingClientManager = new BlockingClientManager();
} else {
BtcNodes btcNodes = new BtcNodes();
List<PeerAddress> peerAddressList = btcNodesToPeerAddress(btcNodes, socks5Proxy);
peerAddresses = new HashSet<>(peerAddressList);
connectTimeoutMillis = socks5Proxy.map(s -> 60_000).orElse(10_000);
disconnectIntervalSec = 2;
reconnectIntervalSec = 120;
if (socks5Proxy.isPresent()) {
InetSocketAddress inetSocketAddress = new InetSocketAddress(socks5Proxy.get().getInetAddress(), socks5Proxy.get().getPort());
Proxy proxy = new Proxy(Proxy.Type.SOCKS, inetSocketAddress);
ProxySocketFactory proxySocketFactory = new ProxySocketFactory(proxy);
blockingClientManager = new BlockingClientManager(proxySocketFactory);
} else {
blockingClientManager = new BlockingClientManager();
}
}
log.info("Using peer addresses {}", peerAddresses);
blockingClientManager.setConnectTimeoutMillis(connectTimeoutMillis);
peerConncetionModel.fill(peerAddresses);
Set<PeerConncetionInfo> peerConncetionInfoSet = new HashSet<>(peerConncetionModel.getMap().values());
peerConnections = peerConncetionInfoSet.stream()
.map(peerConncetionInfo -> new PeerConnection(context,
peerConncetionInfo,
blockingClientManager,
connectTimeoutMillis,
disconnectIntervalSec,
reconnectIntervalSec))
.collect(Collectors.toSet());
}
public CompletableFuture<Void> start() {
return CompletableFuture.runAsync(() -> {
log.info("start");
Context.propagate(context);
blockingClientManager.startAsync();
blockingClientManager.awaitRunning();
}, SingleThreadExecutorUtils.getSingleThreadExecutor("start"));
}
public void connectToAll() {
peerConnections.forEach(PeerConnection::connect);
}
public CompletableFuture<Void> shutdown() {
return CompletableFuture.runAsync(() -> {
log.info("shutdown");
Context.propagate(context);
CountDownLatch latch = new CountDownLatch(peerConnections.size());
peerConnections.forEach(e -> e.shutdown().thenRun(latch::countDown));
try {
latch.await(5, TimeUnit.SECONDS);
blockingClientManager.stopAsync();
blockingClientManager.awaitTerminated(Duration.ofSeconds(2));
} catch (Exception e) {
throw new RuntimeException(e);
}
}, SingleThreadExecutorUtils.getSingleThreadExecutor("shutdown"));
}
private List<PeerAddress> btcNodesToPeerAddress(BtcNodes btcNodes, Optional<Socks5Proxy> proxy) {
List<BtcNodes.BtcNode> nodes = btcNodes.getProvidedBtcNodes();
BtcNodesRepository repository = new BtcNodesRepository(nodes);
return repository.getPeerAddresses(proxy.orElse(null));
}
}

View File

@ -0,0 +1,82 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.btcnodemonitor.btc;
import com.google.common.base.Joiner;
import java.util.LinkedList;
import java.util.List;
/**
* Borrowed from VersionMessage and updated to be in sync with
* https://github.com/bitcoin/bitcoin/blob/b1ba1b178f501daa1afdd91f9efec34e5ec1e294/src/protocol.h#L324
*/
public class ServiceBits {
public static final int NODE_NONE = 0;
// NODE_NETWORK means that the node is capable of serving the complete block chain. It is currently
// set by all Bitcoin Core non pruned nodes, and is unset by SPV clients or other light clients.
public static final int NODE_NETWORK = 1 << 0;
// NODE_BLOOM means the node is capable and willing to handle bloom-filtered connections.
public static final int NODE_BLOOM = 1 << 2;
// NODE_WITNESS indicates that a node can be asked for blocks and transactions including
// witness data.
public static final int NODE_WITNESS = 1 << 3;
// NODE_COMPACT_FILTERS means the node will service basic block filter requests.
// See BIP157 and BIP158 for details on how this is implemented.
public static final int NODE_COMPACT_FILTERS = 1 << 6;
// NODE_NETWORK_LIMITED means the same as NODE_NETWORK with the limitation of only
// serving the last 288 (2 day) blocks
// See BIP159 for details on how this is implemented.
public static final int NODE_NETWORK_LIMITED = 1 << 10;
// NODE_P2P_V2 means the node supports BIP324 transport
public static final int NODE_P2P_V2 = 1 << 11;
public static String toString(long services) {
List<String> strings = new LinkedList<>();
if ((services & NODE_NETWORK) == NODE_NETWORK) {
strings.add("NETWORK");
services &= ~NODE_NETWORK;
}
if ((services & NODE_BLOOM) == NODE_BLOOM) {
strings.add("BLOOM");
services &= ~NODE_BLOOM;
}
if ((services & NODE_WITNESS) == NODE_WITNESS) {
strings.add("WITNESS");
services &= ~NODE_WITNESS;
}
if ((services & NODE_COMPACT_FILTERS) == NODE_COMPACT_FILTERS) {
strings.add("COMPACT_FILTERS");
services &= ~NODE_COMPACT_FILTERS;
}
if ((services & NODE_NETWORK_LIMITED) == NODE_NETWORK_LIMITED) {
strings.add("NETWORK_LIMITED");
services &= ~NODE_NETWORK_LIMITED;
}
if ((services & NODE_P2P_V2) == NODE_P2P_V2) {
strings.add("NODE_P2P_V2");
services &= ~NODE_P2P_V2;
}
if (services != 0)
strings.add("Unrecognized service bit: " + Long.toBinaryString(services));
return Joiner.on(", ").join(strings);
}
}

View File

@ -0,0 +1,276 @@
/*
* 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.btcnodemonitor.server;
import bisq.core.btc.nodes.BtcNodes;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.util.MathUtils;
import bisq.common.util.Profiler;
import bisq.common.util.SingleThreadExecutorUtils;
import org.bitcoinj.core.VersionMessage;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import bisq.btcnodemonitor.btc.PeerConncetionInfo;
import bisq.btcnodemonitor.btc.PeerConncetionModel;
import bisq.btcnodemonitor.btc.ServiceBits;
import spark.Spark;
@Slf4j
public class SimpleHttpServer {
private final static String CLOSE_TAG = "</font><br/>";
private final static String WARNING_ICON = "&#9888; ";
private final static String ALERT_ICON = "&#9760; "; // &#9889; &#9889;
private final Config config;
@Getter
private final List<BtcNodes.BtcNode> providedBtcNodes;
private final Map<String, BtcNodes.BtcNode> btcNodeByAddress;
private final int port;
private final PeerConncetionModel peerConncetionModel;
private final String started;
private String html;
private int requestCounter;
private String networkInfo;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public SimpleHttpServer(Config config, PeerConncetionModel peerConncetionModel) {
this.config = config;
this.peerConncetionModel = peerConncetionModel;
started = new Date().toString();
this.providedBtcNodes = peerConncetionModel.getProvidedBtcNodes();
BaseCurrencyNetwork network = config.baseCurrencyNetwork;
if (config.useTorForBtcMonitor) {
port = network.isMainnet() ? 8000 : 8001;
networkInfo = network.isMainnet() ? "TOR/MAIN_NET" : "TOR/REG_TEST";
btcNodeByAddress = providedBtcNodes.stream()
.filter(e -> e.getOnionAddress() != null)
.collect(Collectors.toMap(BtcNodes.BtcNode::getOnionAddress, e -> e));
} else {
port = network.isMainnet() ? 8080 : 8081;
networkInfo = network.isMainnet() ? "Clearnet/MAIN_NET" : "Clearnet/REG_TEST";
if (new LocalBitcoinNode(config).shouldBeUsed()) {
btcNodeByAddress = new HashMap<>();
btcNodeByAddress.put("127.0.0.1", new BtcNodes.BtcNode("localhost", null, "127.0.0.1",
Config.baseCurrencyNetworkParameters().getPort(), "n/a"));
} else {
if (network.isMainnet()) {
btcNodeByAddress = providedBtcNodes.stream()
.filter(e -> e.getAddress() != null)
.collect(Collectors.toMap(BtcNodes.BtcNode::getAddress, e -> e));
} else {
btcNodeByAddress = new HashMap<>();
}
}
}
html = "Monitor for Bitcoin nodes created for " + networkInfo;
}
public CompletableFuture<Void> start() {
html = "Monitor for Bitcoin nodes starting up for " + networkInfo;
return CompletableFuture.runAsync(() -> {
log.info("Server listen on {}", port);
Spark.port(port);
Spark.get("/", (req, res) -> {
log.info("Incoming request from: {}", req.userAgent());
return html;
});
}, SingleThreadExecutorUtils.getSingleThreadExecutor("SimpleHttpServer.start"));
}
public CompletableFuture<Void> shutdown() {
return CompletableFuture.runAsync(() -> {
log.info("shutDown");
Spark.stop();
log.info("shutDown completed");
});
}
public void onChange() {
StringBuilder sb = new StringBuilder();
sb.append("<result>" +
"<head>" +
"<style type=\"text/css\">" +
" a {" +
" text-decoration:none; color: black;" +
" }" +
" #info { color: #333333; } " +
" #warn { color: #ff7700; } " +
" #error { color: #ff0000; } " +
"table, th, td {border: 1px solid black;}" +
"</style></head>" +
"<body><h3>")
.append("Monitor for Bitcoin nodes using ").append(networkInfo)
.append(", started at: ").append(started).append("<br/>")
.append("System load: ").append(Profiler.getSystemLoad()).append("<br/>")
.append("<br/>").append("<table style=\"width:100%\">")
.append("<tr>")
.append("<th align=\"left\">Node operator</th>")
.append("<th align=\"left\">Connection attempts</th>")
.append("<th align=\"left\">Node info</th>").append("</tr>");
Map<String, PeerConncetionInfo> peersMap = peerConncetionModel.getMap();
btcNodeByAddress.values().stream()
.sorted(Comparator.comparing(BtcNodes.BtcNode::getId))
.forEach(btcNode -> {
sb.append("<tr valign=\"top\">");
String address = btcNode.getAddress();
PeerConncetionInfo peerConncetionInfo = peersMap.get(address);
if (peersMap.containsKey(address)) {
sb.append("<td>").append(getOperatorInfo(btcNode, address)).append("</td>")
.append("<td>").append(getConnectionInfo(peerConncetionInfo)).append("</td>")
.append("<td>").append(getNodeInfo(peerConncetionInfo)).append("</td>");
sb.append("</tr>");
return;
}
address = btcNode.getOnionAddress();
peerConncetionInfo = peersMap.get(address);
if (peersMap.containsKey(address)) {
sb.append("<td>").append(getOperatorInfo(btcNode, address)).append("</td>")
.append("<td>").append(getConnectionInfo(peerConncetionInfo)).append("</td>")
.append("<td>").append(getNodeInfo(peerConncetionInfo)).append("</td>");
} else {
/* sb.append("<td>").append(getOperatorInfo(btcNode, null)).append("</td>")
.append("<td>").append("n/a").append("</td>");*/
}
sb.append("</tr>");
});
sb.append("</table></body></result>");
html = sb.toString();
}
private String getOperatorInfo(BtcNodes.BtcNode btcNode, @Nullable String address) {
StringBuilder sb = new StringBuilder();
sb.append(btcNode.getId()).append("<br/>");
if (address != null) {
sb.append("Address: ").append(address).append("<br/>");
}
return sb.toString();
}
private String getConnectionInfo(PeerConncetionInfo info) {
StringBuilder sb = new StringBuilder();
sb.append("Num connection attempts: ").append(info.getNumConnectionAttempts()).append("<br/>");
double failureRate = info.getFailureRate();
String failureRateString = MathUtils.roundDouble(failureRate * 100, 2) + "%";
if (failureRate >= 0.5) {
failureRateString = asError(failureRateString, failureRateString);
} else if (failureRate >= 0.25) {
failureRateString = asWarn(failureRateString, failureRateString);
} else if (failureRate > 0.) {
failureRateString = asInfo(failureRateString, failureRateString);
}
sb.append("FailureRate (success/failures): ").append(failureRateString)
.append("(").append(info.getNumSuccess()).append(" / ")
.append(info.getNumFailures()).append(")").append("<br/>");
info.getLastExceptionMessage().ifPresent(errorMessage -> {
String allExceptionMessages = info.getAllExceptionMessages();
int indexOfLastError = info.getLastAttemptWithException().map(info::getIndex).orElse(-1);
String msg;
String value = "Last error (connection attempt " + indexOfLastError + "): " + errorMessage;
int tip = info.getConnectionAttempts().size() - 1;
if (indexOfLastError >= tip - 1) {
msg = asError(value, allExceptionMessages);
} else if (indexOfLastError >= tip - 10) {
msg = asWarn(value, allExceptionMessages);
} else {
msg = asInfo(value, allExceptionMessages);
;
}
sb.append(msg).append("<br/>");
});
sb.append("Duration to connect: ").append(MathUtils.roundDouble(info.getLastSuccessfulConnectTime() / 1000d, 2)).append(" sec").append("<br/>");
return sb.toString();
}
private String getNodeInfo(PeerConncetionInfo info) {
if (info.getLastSuccessfulConnected().isEmpty()) {
return "";
}
PeerConncetionInfo.ConnectionAttempt attempt = info.getLastSuccessfulConnected().get();
if (attempt.getVersionMessage().isEmpty()) {
return "";
}
int index = info.getIndex(attempt) + 1;
StringBuilder sb = new StringBuilder();
VersionMessage versionMessage = attempt.getVersionMessage().get();
long peerTime = versionMessage.time * 1000;
long passed = System.currentTimeMillis() - attempt.getConnectionSuccessTs();
String passedString = DurationFormatUtils.formatDurationWords(passed, true, true) + " ago";
// String passedString = MathUtils.roundDouble(passed / 1000d, 2) + " sec. ago";
if (passed > 300_000) {
passedString = asWarn(passedString, passedString);
}
sb.append("Result from connection attempt ").append(index).append(":<br/>");
sb.append("Connected ").append(passedString).append("<br/>");
sb.append("Block height: ").append(versionMessage.bestHeight).append("<br/>");
sb.append("Version: ").append(versionMessage.subVer.replace("/", "")).append(" (").append(versionMessage.clientVersion).append(")").append("<br/>");
String serviceBits = ServiceBits.toString(versionMessage.localServices);
sb.append("Services: ").append(serviceBits)
.append(" (").append(versionMessage.localServices).append(")").append("<br/>");
sb.append("Time: ").append(String.format(Locale.US, "%tF %tT", peerTime, peerTime));
return sb.toString();
}
private static String decorate(String style, String value, String tooltip) {
return "<b><a id=\"" + style + "\" href=\"#\" title=\"" + tooltip + "\">" + value + "</a></b>";
}
private static String asInfo(String value, String tooltip) {
return decorate("info", value, tooltip);
}
private static String asWarn(String value, String tooltip) {
return decorate("warn", value, tooltip);
}
private static String asError(String value, String tooltip) {
return decorate("error", value, tooltip);
}
}

View File

@ -0,0 +1,93 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.btcnodemonitor.socksProxy;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.network.NewTor;
import bisq.common.config.Config;
import bisq.common.util.SingleThreadExecutorUtils;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class ProxySetup {
@Getter
private final File torDir;
private final SocksProxyFactory socksProxyFactory;
private final Config config;
private NewTor torMode;
private Tor tor;
public ProxySetup(Config config) {
this.config = config;
socksProxyFactory = new SocksProxyFactory("127.0.0.1");
Socks5ProxyProvider socks5ProxyProvider = new Socks5ProxyProvider("", "");
socks5ProxyProvider.setSocks5ProxyInternal(socksProxyFactory);
String networkDirName = config.baseCurrencyNetwork.name().toLowerCase();
torDir = Paths.get(config.appDataDir.getPath(), networkDirName, "tor").toFile();
}
public CompletableFuture<Optional<Socks5Proxy>> createSocksProxy() {
log.info("createSocksProxy");
if (!config.useTorForBtcMonitor) {
return CompletableFuture.completedFuture(Optional.empty());
}
checkArgument(tor == null);
return CompletableFuture.supplyAsync(() -> {
torMode = new NewTor(torDir, null, "", ArrayList::new);
try {
// blocking
tor = torMode.getTor();
socksProxyFactory.setTor(tor);
return Optional.of(socksProxyFactory.getSocksProxy());
} catch (IOException | TorCtlException e) {
throw new RuntimeException(e);
}
}, SingleThreadExecutorUtils.getSingleThreadExecutor("ProxySetup.start"));
}
public CompletableFuture<Void> shutdown() {
log.warn("start shutdown");
return CompletableFuture.runAsync(() -> {
if (tor != null) {
tor.shutdown();
}
log.warn("shutdown completed");
})
.orTimeout(2, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.btcnodemonitor.socksProxy;
import bisq.network.p2p.network.Socks5ProxyInternalFactory;
import org.berndpruenster.netlayer.tor.Tor;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class SocksProxyFactory implements Socks5ProxyInternalFactory {
private final String torControlHost;
@Setter
@Nullable
private Tor tor;
private Socks5Proxy socksProxy;
public SocksProxyFactory(String torControlHost) {
this.torControlHost = torControlHost;
}
@Override
public Socks5Proxy getSocksProxy() {
if (tor == null) {
return null;
} else {
try {
if (socksProxy == null) {
socksProxy = tor.getProxy(torControlHost, null);
}
return socksProxy;
} catch (Throwable t) {
log.error("Error at getSocksProxy", t);
return null;
}
}
}
}

View File

@ -0,0 +1,17 @@
<?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{40}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="org.bitcoinj" level="INFO"/>
<logger name="spark.embeddedserver" level="WARN"/>
<logger name="org.eclipse.jetty.server" level="WARN"/>
<logger name="org.berndpruenster.netlayer.tor.Tor" level="WARN"/>
</configuration>

View File

@ -138,6 +138,7 @@ public class Config {
public static final String BM_ORACLE_NODE_PUB_KEY = "bmOracleNodePubKey";
public static final String BM_ORACLE_NODE_PRIV_KEY = "bmOracleNodePrivKey";
public static final String SEED_NODE_REPORTING_SERVER_URL = "seedNodeReportingServerUrl";
public static final String USE_TOR_FOR_BTC_MONITOR = "useTorForBtcMonitor";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -237,6 +238,7 @@ public class Config {
public final String bmOracleNodePubKey;
public final String bmOracleNodePrivKey;
public final String seedNodeReportingServerUrl;
public final boolean useTorForBtcMonitor;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@ -738,6 +740,12 @@ public class Config {
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> useTorForBtcMonitorOpt =
parser.accepts(USE_TOR_FOR_BTC_MONITOR, "If set to true BitcoinJ is routed over tor (socks 5 proxy) for Bitcoin monitor.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -864,6 +872,7 @@ public class Config {
this.bmOracleNodePubKey = options.valueOf(bmOracleNodePubKey);
this.bmOracleNodePrivKey = options.valueOf(bmOracleNodePrivKey);
this.seedNodeReportingServerUrl = options.valueOf(seedNodeReportingServerUrlOpt);
this.useTorForBtcMonitor = options.valueOf(useTorForBtcMonitorOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),

View File

@ -30,12 +30,16 @@ public class Profiler {
}
public static void printSystemLoad() {
log.info(getSystemLoad());
}
public static String getSystemLoad() {
Runtime runtime = Runtime.getRuntime();
long free = runtime.freeMemory();
long total = runtime.totalMemory();
long used = total - free;
log.info("Total memory: {}; Used memory: {}; Free memory: {}; Max memory: {}; No. of threads: {}",
return String.format("Total memory: %s; Used memory: %s; Free memory: %s; Max memory: %s; No. of threads: %s",
Utilities.readableFileSize(total),
Utilities.readableFileSize(used),
Utilities.readableFileSize(free),

View File

@ -178,5 +178,11 @@ public class BtcNodes {
", port='" + port + '\'' +
", operator='" + operator;
}
public String getId() {
String address = this.address == null ? "" : this.address + ", ";
String onionAddress = this.onionAddress == null ? "" : this.onionAddress;
return operator + ": [" + address + onionAddress + "]";
}
}
}

View File

@ -42,6 +42,10 @@ public class BtcNodesRepository {
this.nodes = nodes;
}
public List<PeerAddress> getPeerAddresses(@Nullable Socks5Proxy proxy) {
return getPeerAddresses(proxy, false);
}
public List<PeerAddress> getPeerAddresses(@Nullable Socks5Proxy proxy, boolean isUseClearNodesWithProxies) {
List<PeerAddress> result;
// We connect to onion nodes only in case we use Tor for BitcoinJ (default) to avoid privacy leaks at

View File

@ -282,7 +282,8 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene
if (!persistedBsqState.getBlocks().isEmpty()) {
int heightOfLastBlock = persistedBsqState.getLastBlock().getHeight();
if (heightOfLastBlock != chainHeightOfPersisted) {
log.warn("chainHeightOfPersisted must be same as heightOfLastBlock");
log.warn("chainHeightOfPersisted must be same as heightOfLastBlock. heightOfLastBlock={}, chainHeightOfPersisted={}",
heightOfLastBlock, chainHeightOfPersisted);
resyncDaoStateFromResources();
return;
}

View File

@ -962,11 +962,11 @@ portfolio.pending.step5_seller.sold=You have sold
portfolio.pending.step5_seller.received=You have received
tradeFeedbackWindow.title=Congratulations on completing your trade
tradeFeedbackWindow.msg.part1=We'd love to hear back from you about your experience. It'll help us to improve the software \
and to smooth out any rough edges. If you'd like to provide feedback, please fill out this short survey \
(no registration required) at:
tradeFeedbackWindow.msg.part2=If you have any questions, or experienced any problems, please get in touch with other users and contributors via the Bisq forum at:
tradeFeedbackWindow.msg.part3=Thanks for using Bisq!
tradeFeedbackWindow.msg.feedback=We'd love to hear back from you about your experience. It'll help us to improve the software \
and to smooth out any rough edges.\n\n\
If you'd like to provide feedback, if you have any questions, or experienced any problems, \
please get in touch with other users and contributors via the Bisq forum at:
tradeFeedbackWindow.msg.thanks=Thanks for using Bisq!
portfolio.pending.role=My role
portfolio.pending.tradeInformation=Trade information

View File

@ -7,4 +7,8 @@ xaebw6lpcckkyb2anyzyfynthebek2abqzyhchenfrdqlfktezgtrtqd.onion:8000 (@jester4042
33ol66i7crfrnypjlpd7kem3imo6zmjkvxdr6ipcdx3e2skbpcrfzuqd.onion:8000 (@jester4042)
runbtcsd42pwlfna32ibcrrykrcmozgv6x73sxjrdohkm55v5f6nh6ad.onion:8000 (@runbtc)
runbtcsdojm6hiyqotmhq7y5k4ws5bda6t3tv7y63c46rt7fx7lt7aid.onion:8000 (@runbtc)
<<<<<<< HEAD
runbtcsdomqb4rnqg2zokjk4zjzs7pcswzrcgow4zji2qt4toyiabhyd.onion:8000 (@runbtc)
=======
runbtcsdomqb4rnqg2zokjk4zjzs7pcswzrcgow4zji2qt4toyiabhyd.onion:8000 (@runbtc)
>>>>>>> master

View File

@ -320,7 +320,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
setupDevDummyPaymentAccounts();
}
maybeAddArsBlueMarketNotificationToQueue();
getShowAppScreen().set(true);
}
@ -904,20 +903,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
return settingsPresentation.getShowSettingsUpdatesNotification();
}
private void maybeAddArsBlueMarketNotificationToQueue() {
String key = "arsBlueMarketNotificationPopup";
if (DontShowAgainLookup.showAgain(key)) {
Popup popup = new Popup()
.headLine(Res.get("popup.arsBlueMarket.title"))
.information(Res.get("popup.arsBlueMarket.info"))
.actionButtonText(Res.get("shared.iUnderstand"))
.hideCloseButton()
.dontShowAgainId(key);
popup.setDisplayOrderPriority(1);
popupQueue.add(popup);
}
}
private void maybeShowPopupsFromQueue() {
if (!popupQueue.isEmpty()) {
Overlay<?> overlay = popupQueue.poll();

View File

@ -41,7 +41,6 @@ import bisq.desktop.main.presentation.AccountPresentation;
import bisq.core.locale.Res;
import bisq.core.user.DontShowAgainLookup;
import bisq.common.app.DevEnv;
import bisq.common.util.Utilities;
import javax.inject.Inject;
@ -273,14 +272,6 @@ public class AccountView extends ActivatableView<TabPane, Void> {
.backgroundInfo(Res.get("account.info.msg"))
.dontShowAgainId(key)
.show();
} else {
// news badge leads to the XMR subaddress info page (added in v1.9.2)
new Popup()
.headLine(Res.get("account.altcoin.popup.xmr.dataDirWarningHeadline"))
.backgroundInfo(Res.get("account.altcoin.popup.xmr.dataDirWarning"))
.dontShowAgainId("accountSubAddressInfo")
.width(700)
.show();
}
}

View File

@ -18,9 +18,11 @@
package bisq.desktop.main.account.content.altcoinaccounts;
import bisq.desktop.common.model.ActivatableDataModel;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.GUIUtil;
import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.AssetAccount;
@ -76,6 +78,16 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
protected void activate() {
user.getPaymentAccountsAsObservable().addListener(setChangeListener);
fillAndSortPaymentAccounts();
paymentAccounts.stream().filter(e -> e.getSingleTradeCurrency().getCode().equals("XMR"))
.findAny().ifPresent(e -> {
new Popup()
.headLine(Res.get("account.altcoin.popup.xmr.dataDirWarningHeadline"))
.backgroundInfo(Res.get("account.altcoin.popup.xmr.dataDirWarning"))
.dontShowAgainId("accountSubAddressInfo")
.width(700)
.show();
});
}
private void fillAndSortPaymentAccounts() {

View File

@ -18,16 +18,19 @@
package bisq.desktop.main.account.content.fiataccounts;
import bisq.desktop.common.model.ActivatableDataModel;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.AssetAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.user.User;
@ -123,6 +126,19 @@ class FiatAccountsDataModel extends ActivatableDataModel {
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
accountAgeWitnessService.signAndPublishSameNameAccounts();
if (paymentAccount.getSingleTradeCurrency().getCode().equals("ARS")) {
String key = "arsBlueMarketNotificationPopup";
if (DontShowAgainLookup.showAgain(key)) {
new Popup()
.headLine(Res.get("popup.arsBlueMarket.title"))
.information(Res.get("popup.arsBlueMarket.info"))
.actionButtonText(Res.get("shared.iUnderstand"))
.hideCloseButton()
.dontShowAgainId(key)
.show();
}
}
}
public void onUpdateAccount(PaymentAccount paymentAccount) {

View File

@ -809,7 +809,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
.show(),
1);
} else {
closeAndGoToOpenOffers();
close();
}
}
};

View File

@ -45,7 +45,7 @@ public class TradeFeedbackWindow extends Overlay<TradeFeedbackWindow> {
@Override
public void show() {
headLine(Res.get("tradeFeedbackWindow.title"));
message(Res.get("tradeFeedbackWindow.msg.part1"));
message(Res.get("tradeFeedbackWindow.msg.feedback"));
hideCloseButton();
actionButtonText(Res.get("shared.close"));
@ -56,25 +56,12 @@ public class TradeFeedbackWindow extends Overlay<TradeFeedbackWindow> {
protected void addMessage() {
super.addMessage();
HyperlinkWithIcon survey = addHyperlinkWithIcon(gridPane, ++rowIndex, "https://bisq.network/survey",
"https://bisq.network/survey");
GridPane.setMargin(survey, new Insets(-6, 0, 10, 0));
AutoTooltipLabel messageLabel2 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part2"));
messageLabel2.setMouseTransparent(true);
messageLabel2.setWrapText(true);
GridPane.setHalignment(messageLabel2, HPos.LEFT);
GridPane.setHgrow(messageLabel2, Priority.ALWAYS);
GridPane.setRowIndex(messageLabel2, ++rowIndex);
GridPane.setColumnIndex(messageLabel2, 0);
GridPane.setColumnSpan(messageLabel2, 2);
gridPane.getChildren().add(messageLabel2);
HyperlinkWithIcon forum = addHyperlinkWithIcon(gridPane, ++rowIndex, "https://bisq.community",
"https://bisq.community", 40);
GridPane.setMargin(forum, new Insets(-6, 0, 10, 0));
AutoTooltipLabel messageLabel3 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part3"));
AutoTooltipLabel messageLabel3 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.thanks"));
messageLabel3.setMouseTransparent(true);
messageLabel3.setWrapText(true);
GridPane.setHalignment(messageLabel3, HPos.LEFT);

View File

@ -50,7 +50,9 @@ public class AccountPresentation {
preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener<? super String, ? super Boolean>) change -> {
if (change.getKey().equals(ACCOUNT_NEWS)) {
showNotification.set(!change.wasAdded());
// devs enable this when a news badge is required
// showNotification.set(!change.wasAdded());
showNotification.set(false);
}
});
}
@ -65,26 +67,26 @@ public class AccountPresentation {
public void setup() {
// devs enable this when a news badge is required
showNotification.set(preferences.showAgain(ACCOUNT_NEWS));
//showNotification.set(preferences.showAgain(ACCOUNT_NEWS));
showNotification.set(false);
}
public void showOneTimeAccountSigningPopup(String key, String s) {
showOneTimeAccountSigningPopup(key, s, null);
public void showOneTimeAccountSigningPopup(String key, String message) {
showOneTimeAccountSigningPopup(key, message, null);
}
public void showOneTimeAccountSigningPopup(String key, String s, String optionalParam) {
public void showOneTimeAccountSigningPopup(String key, String message, String optionalParam) {
if (!DevEnv.isDevMode()) {
DontShowAgainLookup.dontShowAgain(ACCOUNT_NEWS, false);
showNotification.set(true);
DontShowAgainLookup.dontShowAgain(key, true);
String message = optionalParam != null ?
Res.get(s, optionalParam, Res.get("popup.accountSigning.generalInformation")) :
Res.get(s, Res.get("popup.accountSigning.generalInformation"));
String information = optionalParam != null ?
Res.get(message, optionalParam, Res.get("popup.accountSigning.generalInformation")) :
Res.get(message, Res.get("popup.accountSigning.generalInformation"));
new Popup().information(message)
.show();
new Popup().information(information).show();
}
}
}

View File

@ -1,5 +1,5 @@
[versions]
apache-commons-lang3 = { strictly = '3.11' }
apache-commons-lang3 = { strictly = '3.14.0' }
apache-httpclient = { strictly = '4.5.12' }
apache-httpcore = { strictly = '4.4.13' }
@ -17,6 +17,7 @@ fontawesomefx-materialdesign-font = { strictly = '2.0.26-9.1.2' }
fxmisc-easybind = { strictly = '1.0.3' }
glassfish-jaxb-lib = { strictly = '3.0.2' }
google-findbugs = { strictly = '3.0.2' }
google-gson = { strictly = '2.8.6' }
google-guava = { strictly = '30.1.1-jre' }
@ -25,9 +26,10 @@ google-guice = { strictly = '5.0.1' }
grpc = { strictly = '1.42.1' }
hamcrest = { strictly = '2.2' }
jackson = { strictly = '2.12.1' }
jackson = { strictly = '2.17.1' }
javax-annotation = { strictly = '1.2' }
jcsv = { strictly = '1.4.0' }
jersey-lib = { strictly = '3.0.4' }
jetbrains-annotations = { strictly = '13.0' }
jfoenix = { strictly = '9.0.10' }
jopt = { strictly = '5.0.4' }
@ -43,9 +45,12 @@ natpryce-make-it-easy = { strictly = '4.0.1' }
netlayer = { strictly = '2b459dc' }
openjfx-javafx-plugin = { strictly = '0.0.10' }
protobuf = { strictly = '3.19.1' }
pushy = { strictly = '0.13.10' }
qrgen = { strictly = '1.3' }
javacv = { strictly = '1.5.10' }
slf4j = { strictly = '1.7.30' }
spark = { strictly = '2.9.4' }
swagger-lib = { strictly = '2.2.0' }
[libraries]
apache-httpclient = { module = "org.apache.httpcomponents:httpclient", version.ref = "apache-httpclient" }
@ -66,6 +71,7 @@ fontawesomefx-materialdesign-font = { module = "de.jensd:fontawesomefx-materiald
fxmisc-easybind = { module = "org.fxmisc.easybind:easybind", version.ref = "fxmisc-easybind" }
glassfish-jaxb = { module = 'org.glassfish.jaxb:jaxb-runtime', version.ref = 'glassfish-jaxb-lib' }
google-findbugs = { module = "com.google.code.findbugs:jsr305", version.ref = "google-findbugs" }
google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" }
google-guava = { module = "com.google.guava:guava", version.ref = "google-guava" }
@ -75,6 +81,7 @@ grpc-core = { module = "io.grpc:grpc-core", version.ref = "grpc" }
grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" }
grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
grpc-auth = { module = "io.grpc:grpc-auth", version.ref = "grpc" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
@ -84,6 +91,9 @@ jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", ver
javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation" }
jcsv = { module = "com.googlecode.jcsv:jcsv", version.ref = "jcsv" }
jersey-container-jdk-http = { module = 'org.glassfish.jersey.containers:jersey-container-jdk-http', version.ref = 'jersey-lib' }
jersey-media-json-jackson = { module = 'org.glassfish.jersey.media:jersey-media-json-jackson', version.ref = 'jersey-lib' }
jersey-inject-jersey-hk2 = { module = 'org.glassfish.jersey.inject:jersey-hk2', version.ref = 'jersey-lib' }
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
jfoenix = { module = "com.jfoenix:jfoenix", version.ref = "jfoenix" }
jopt = { module = "net.sf.jopt-simple:jopt-simple", version.ref = "jopt" }
@ -108,9 +118,15 @@ netlayer-tor-native = { module = "com.github.bisq-network.netlayer:tor.native",
openjfx-javafx-plugin = { module = "org.openjfx:javafx-plugin", version.ref = "openjfx-javafx-plugin" }
protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
pushy = { module = "com.turo:pushy", version.ref = "pushy" }
qrgen = { module = "net.glxn:qrgen", version.ref = "qrgen" }
javacv = { module = "org.bytedeco:javacv-platform", version.ref = "javacv" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
spark-core = { module = "com.sparkjava:spark-core", version.ref = "spark" }
swagger = { module = 'io.swagger.core.v3:swagger-jaxrs2-jakarta', version.ref = 'swagger-lib' }
[bundles]
jersey-libs = ['jersey-container-jdk-http', 'jersey-media-json-jackson', 'jersey-inject-jersey-hk2']
[plugins]
gradle-javacpp-platform = { id = "org.bytedeco.gradle-javacpp-platform", version.ref = "javacv" }

File diff suppressed because it is too large Load Diff

44
inventory/README.md Normal file
View File

@ -0,0 +1,44 @@
## Inventory monitor
This is a simple headless node with a http server which requests periodically from the seed nodes their inventory (not
the full data but just the information about their network data).
### Run inventory monitor
Run the Gradle task:
```sh
./gradlew inventory:run
```
Or create a run scrip by:
```sh
./gradlew inventory:startBisqApp
```
And then run:
```sh
./bisq-inventory
```
### Customize inventory monitor
To configure it with custom parameters append optional program arguments in the following order:
Arguments: `port cleanupTorFiles intervalSec shutdownIntervalDays useLocalhostForP2P network`
If `shutdownIntervalDays` is 0 the app will not be shut down. Otherwise, it's the number of days for shutting down (run
script need to ensure the node gets started again).
Values: `Integer 0|1 Integer Integer 0|1 BTC_MAINNET|BTC_TESTNET|BTC_REGTEST`
Example for localhost, regtest on port 8080: `8080 0 10 5 1 BTC_REGTEST`
Example for production node with cleanupTorFiles=true: `80 1`
Append the program arguments to the run script:
```sh
./bisq-inventory 8080 0 10 1 BTC_REGTEST
```

31
inventory/build.gradle Normal file
View File

@ -0,0 +1,31 @@
plugins {
id 'bisq.application'
id 'bisq.gradle.app_start_plugin.AppStartPlugin'
}
mainClassName = 'bisq.inventory.InventoryMonitorMain'
distTar.enabled = true
dependencies {
implementation enforcedPlatform(project(':platform'))
implementation project(':common')
implementation project(':proto')
implementation project(':p2p')
implementation project(':core')
implementation libs.protobuf.java
annotationProcessor libs.lombok
compileOnly libs.lombok
implementation libs.jetbrains.annotations
implementation libs.logback.classic
implementation libs.logback.core
implementation libs.google.guava
implementation libs.slf4j.api
implementation(libs.google.guice) {
exclude(module: 'guava')
}
implementation libs.spark.core
implementation libs.pushy
implementation libs.commons.codec
implementation libs.grpc.auth
}

View File

@ -0,0 +1,288 @@
/*
* 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.inventory;
import bisq.core.app.TorSetup;
import bisq.core.network.p2p.inventory.GetInventoryRequestManager;
import bisq.core.network.p2p.inventory.model.Average;
import bisq.core.network.p2p.inventory.model.DeviationSeverity;
import bisq.core.network.p2p.inventory.model.InventoryItem;
import bisq.core.network.p2p.inventory.model.RequestInfo;
import bisq.core.network.p2p.seed.DefaultSeedNodeRepository;
import bisq.core.proto.network.CoreNetworkProtoResolver;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.NetworkNodeProvider;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.SetupListener;
import bisq.common.UserThread;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.file.JsonFileManager;
import bisq.common.util.Tuple2;
import java.time.Clock;
import java.io.BufferedReader;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
@Slf4j
public class InventoryMonitor implements SetupListener {
private final Map<NodeAddress, JsonFileManager> jsonFileManagerByNodeAddress = new HashMap<>();
private final Map<NodeAddress, List<RequestInfo>> requestInfoListByNode = new HashMap<>();
private final File appDir;
private final boolean useLocalhostForP2P;
private final int intervalSec;
private final NetworkNode networkNode;
private final GetInventoryRequestManager getInventoryRequestManager;
private final List<NodeAddress> seedNodes;
private InventoryWebServer inventoryWebServer;
private int requestCounter = 0;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public InventoryMonitor(File appDir,
boolean useLocalhostForP2P,
BaseCurrencyNetwork network,
int intervalSec,
boolean cleanupTorFiles) {
this.appDir = appDir;
this.useLocalhostForP2P = useLocalhostForP2P;
this.intervalSec = intervalSec;
File torDir = new File(appDir, "tor");
if (!torDir.exists() && !torDir.mkdir()) {
log.warn("make torDir failed");
}
networkNode = getNetworkNode(torDir);
getInventoryRequestManager = new GetInventoryRequestManager(networkNode);
String networkName = network.name().toLowerCase();
String fileName = network.isMainnet() ? "inv_" + networkName : networkName;
seedNodes = new ArrayList<>(DefaultSeedNodeRepository.getSeedNodeAddressesFromPropertyFile(fileName));
addJsonFileManagers(seedNodes);
BufferedReader bufferedReader = DefaultSeedNodeRepository.readSeedNodePropertyFile(fileName).orElseThrow();
inventoryWebServer = new InventoryWebServer(seedNodes, bufferedReader);
TorSetup torSetup = new TorSetup(torDir);
if (cleanupTorFiles) {
torSetup.cleanupTorFiles(() -> log.info("Tor directory cleaned up"), log::error);
}
}
void start(int port) {
networkNode.start(this);
inventoryWebServer.start(port);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void shutDown(Runnable shutDownCompleteHandler) {
networkNode.shutDown(shutDownCompleteHandler);
inventoryWebServer.shutDown();
jsonFileManagerByNodeAddress.values().forEach(JsonFileManager::shutDown);
}
///////////////////////////////////////////////////////////////////////////////////////////
// SetupListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onTorNodeReady() {
UserThread.runPeriodically(this::requestFromAllSeeds, intervalSec);
requestFromAllSeeds();
}
@Override
public void onHiddenServicePublished() {
}
@Override
public void onSetupFailed(Throwable throwable) {
}
@Override
public void onRequestCustomBridges() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void requestFromAllSeeds() {
requestCounter++;
seedNodes.forEach(nodeAddress -> {
RequestInfo requestInfo = new RequestInfo(System.currentTimeMillis());
new Thread(() -> {
Thread.currentThread().setName("request @ " + getShortAddress(nodeAddress, useLocalhostForP2P));
getInventoryRequestManager.request(nodeAddress,
result -> processResponse(nodeAddress, requestInfo, result, null),
errorMessage -> processResponse(nodeAddress, requestInfo, null, errorMessage));
}).start();
});
}
private void processResponse(NodeAddress nodeAddress,
RequestInfo requestInfo,
@Nullable Map<InventoryItem, String> result,
@Nullable String errorMessage) {
if (errorMessage != null && !errorMessage.isEmpty()) {
log.warn("Error at connection to peer {}: {}", nodeAddress, errorMessage);
requestInfo.setErrorMessage(errorMessage);
} else {
requestInfo.setResponseTime(System.currentTimeMillis());
}
boolean ignoreDeviationAtStartup;
if (result != null) {
log.info("nodeAddress={}, result={}", nodeAddress, result.toString());
// If seed just started up we ignore the deviation as it can be expected that seed is still syncing
// DAO state/blocks. P2P data should be ready but as we received it from other seeds it is not that
// valuable information either, so we apply the ignore to all data.
if (result.containsKey(InventoryItem.jvmStartTime)) {
String jvmStartTimeString = result.get(InventoryItem.jvmStartTime);
long jvmStartTime = Long.parseLong(jvmStartTimeString);
ignoreDeviationAtStartup = jvmStartTime < TimeUnit.MINUTES.toMillis(2);
} else {
ignoreDeviationAtStartup = false;
}
} else {
ignoreDeviationAtStartup = false;
}
requestInfoListByNode.putIfAbsent(nodeAddress, new ArrayList<>());
List<RequestInfo> requestInfoList = requestInfoListByNode.get(nodeAddress);
// We create average of all nodes latest results. It might be that the nodes last result is
// from a previous request as the response has not arrived yet.
//TODO might be not a good idea to use the last result if its not a recent one. a faulty node would distort
// the average calculation.
// As we add at the end our own result the average is excluding our own value
Collection<List<RequestInfo>> requestInfoListByNodeValues = requestInfoListByNode.values();
Set<RequestInfo> requestInfoSet = requestInfoListByNodeValues.stream()
.filter(list -> !list.isEmpty())
.map(list -> list.get(list.size() - 1))
.collect(Collectors.toSet());
Map<InventoryItem, Double> averageValues = Average.of(requestInfoSet);
String daoStateChainHeight = result != null &&
result.containsKey(InventoryItem.daoStateChainHeight) ?
result.get(InventoryItem.daoStateChainHeight) :
null;
List.of(InventoryItem.values()).forEach(inventoryItem -> {
String value = result != null ? result.get(inventoryItem) : null;
Tuple2<Double, Double> tuple = inventoryItem.getDeviationAndAverage(averageValues, value);
Double deviation = tuple != null ? tuple.first : null;
Double average = tuple != null ? tuple.second : null;
DeviationSeverity deviationSeverity = ignoreDeviationAtStartup ? DeviationSeverity.IGNORED :
inventoryItem.getDeviationSeverity(deviation,
requestInfoListByNodeValues,
value,
daoStateChainHeight);
int endIndex = Math.max(0, requestInfoList.size() - 1);
int deviationTolerance = inventoryItem.getDeviationTolerance();
int fromIndex = Math.max(0, endIndex - deviationTolerance);
List<DeviationSeverity> lastDeviationSeverityEntries = requestInfoList.subList(fromIndex, endIndex).stream()
.filter(e -> e.getDataMap().containsKey(inventoryItem))
.map(e -> e.getDataMap().get(inventoryItem).getDeviationSeverity())
.collect(Collectors.toList());
long numWarnings = lastDeviationSeverityEntries.stream()
.filter(e -> e == DeviationSeverity.WARN)
.count();
long numAlerts = lastDeviationSeverityEntries.stream()
.filter(e -> e == DeviationSeverity.ALERT)
.count();
boolean persistentWarning = numWarnings == deviationTolerance;
boolean persistentAlert = numAlerts == deviationTolerance;
RequestInfo.Data data = new RequestInfo.Data(value, average, deviation, deviationSeverity, persistentWarning, persistentAlert);
requestInfo.getDataMap().put(inventoryItem, data);
});
requestInfoList.add(requestInfo);
inventoryWebServer.onNewRequestInfo(requestInfoListByNode, requestCounter);
String json = JsonUtil.objectToJson(requestInfo);
jsonFileManagerByNodeAddress.get(nodeAddress).writeToDisc(json, String.valueOf(requestInfo.getRequestStartTime()));
}
private void addJsonFileManagers(List<NodeAddress> seedNodes) {
File jsonDir = new File(appDir, "json");
if (!jsonDir.exists() && !jsonDir.mkdir()) {
log.warn("make jsonDir failed");
}
seedNodes.forEach(nodeAddress -> {
JsonFileManager jsonFileManager = new JsonFileManager(new File(jsonDir, getShortAddress(nodeAddress, useLocalhostForP2P)));
jsonFileManagerByNodeAddress.put(nodeAddress, jsonFileManager);
});
}
private NetworkNode getNetworkNode(File torDir) {
CoreNetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone());
return new NetworkNodeProvider(networkProtoResolver,
() -> null,
null,
12,
useLocalhostForP2P,
9999,
torDir,
null,
"",
"",
-1,
"",
null,
false,
false)
.get();
}
private String getShortAddress(NodeAddress nodeAddress, boolean useLocalhostForP2P) {
return useLocalhostForP2P ?
nodeAddress.getFullAddress().replace(":", "_") :
nodeAddress.getFullAddress().substring(0, 10);
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.inventory;
import bisq.core.locale.Res;
import bisq.common.UserThread;
import bisq.common.app.AsciiLogo;
import bisq.common.app.Log;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.util.SingleThreadExecutorUtils;
import bisq.common.util.Utilities;
import java.nio.file.Paths;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import ch.qos.logback.classic.Level;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.app.BisqExecutable.EXIT_SUCCESS;
import sun.misc.Signal;
@Slf4j
public class InventoryMonitorMain {
private static InventoryMonitor inventoryMonitor;
private static boolean stopped;
// Example prog args:
// 8080 0 10 5 1 BTC_REGTEST
public static void main(String[] args) {
// Default values
int port = 80;
boolean cleanupTorFiles = false;
int intervalSec = 120;
int shutdownIntervalDays = 5;
boolean useLocalhostForP2P = false;
BaseCurrencyNetwork network = BaseCurrencyNetwork.BTC_MAINNET;
/* port = 8080;
useLocalhostForP2P = true;
network = BaseCurrencyNetwork.BTC_REGTEST;*/
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
if (args.length > 1) {
cleanupTorFiles = args[1].equals("1");
}
if (args.length > 2) {
intervalSec = Integer.parseInt(args[2]);
}
if (args.length > 3) {
shutdownIntervalDays = Integer.parseInt(args[3]);
}
if (args.length > 4) {
useLocalhostForP2P = args[4].equals("1");
}
if (args.length > 5) {
network = BaseCurrencyNetwork.valueOf(args[5]);
}
String appName = "bisq-inventory-monitor-" + network;
File appDir = new File(Utilities.getUserDataDir(), appName);
if (!appDir.exists() && !appDir.mkdir()) {
log.warn("make appDir failed");
}
setup(network, appDir);
inventoryMonitor = new InventoryMonitor(appDir, useLocalhostForP2P, network, intervalSec, cleanupTorFiles);
inventoryMonitor.start(port);
// We shut down after 5 days to avoid potential memory leak issue.
// The start script will restart the app.
if (shutdownIntervalDays > 0) {
UserThread.runAfter(InventoryMonitorMain::shutDown, TimeUnit.DAYS.toSeconds(shutdownIntervalDays));
}
keepRunning();
}
private static void setup(BaseCurrencyNetwork network, File appDir) {
String logPath = Paths.get(appDir.getPath(), "bisq").toString();
Log.setup(logPath);
Log.setLevel(Level.INFO);
AsciiLogo.showAsciiLogo();
Version.setBaseCryptoNetworkId(network.ordinal());
Res.setup(); // Used for some formatting in the webserver
// We do not set any capabilities as we don't want to receive any network data beside our response.
// We also do not use capabilities for the request/response messages as we only connect to seeds nodes
ExecutorService executorService = SingleThreadExecutorUtils.getSingleThreadExecutor(InventoryMonitorMain.class);
UserThread.setExecutor(executorService);
Signal.handle(new Signal("INT"), signal -> UserThread.execute(InventoryMonitorMain::shutDown));
Signal.handle(new Signal("TERM"), signal -> UserThread.execute(InventoryMonitorMain::shutDown));
}
private static void shutDown() {
stopped = true;
inventoryMonitor.shutDown(() -> System.exit(EXIT_SUCCESS));
}
private static void keepRunning() {
while (!stopped) {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
}

View File

@ -0,0 +1,552 @@
/*
* 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.inventory;
import bisq.core.network.p2p.inventory.model.DeviationByIntegerDiff;
import bisq.core.network.p2p.inventory.model.DeviationByPercentage;
import bisq.core.network.p2p.inventory.model.DeviationSeverity;
import bisq.core.network.p2p.inventory.model.InventoryItem;
import bisq.core.network.p2p.inventory.model.RequestInfo;
import bisq.core.util.FormattingUtils;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.util.MathUtils;
import bisq.common.util.Utilities;
import com.google.common.base.Joiner;
import java.io.BufferedReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import spark.Spark;
@Slf4j
public class InventoryWebServer {
private final static String CLOSE_TAG = "</font><br/>";
private final static String WARNING_ICON = "&#9888; ";
private final static String ALERT_ICON = "&#9760; "; // &#9889; &#9889;
private final List<NodeAddress> seedNodes;
private final Map<String, String> operatorByNodeAddress = new HashMap<>();
private String html;
private int requestCounter;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public InventoryWebServer(List<NodeAddress> seedNodes, BufferedReader seedNodeFile) {
this.seedNodes = seedNodes;
setupOperatorMap(seedNodeFile);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void start(int port) {
Spark.port(port);
Spark.get("/", (req, res) -> {
log.info("Incoming request from: {}", req.userAgent());
return html == null ? "Starting up..." : html;
});
}
public void shutDown() {
Spark.stop();
}
public void onNewRequestInfo(Map<NodeAddress, List<RequestInfo>> requestInfoListByNode, int requestCounter) {
this.requestCounter = requestCounter;
html = generateHtml(requestInfoListByNode);
}
///////////////////////////////////////////////////////////////////////////////////////////
// HTML
///////////////////////////////////////////////////////////////////////////////////////////
private String generateHtml(Map<NodeAddress, List<RequestInfo>> map) {
StringBuilder html = new StringBuilder();
html.append("<html>" +
"<head>" +
"<style type=\"text/css\">" +
" a {" +
" text-decoration:none; color: black;" +
" }" +
" #warn { color: #ff7700; } " +
" #alert { color: #ff0000; } " +
"table, th, td {border: 1px solid black;}" +
"</style></head>" +
"<body><h3>")
.append("Current time: ").append(new Date().toString()).append("<br/>")
.append("Request cycle: ").append(requestCounter).append("<br/>")
.append("Version/commit: ").append(Version.VERSION).append(" / ").append("TODO RequestInfo.COMMIT_HASH").append("<br/>")
.append("<table style=\"width:100%\">")
.append("<tr>")
.append("<th align=\"left\">Seed node info</th>")
.append("<th align=\"left\">Request info</th>")
.append("<th align=\"left\">Data inventory</th>")
.append("<th align=\"left\">DAO data</th>")
.append("<th align=\"left\">Network info</th>").append("</tr>");
seedNodes.forEach(seedNode -> {
html.append("<tr valign=\"top\">");
if (map.containsKey(seedNode) && !map.get(seedNode).isEmpty()) {
List<RequestInfo> list = map.get(seedNode);
int numRequests = list.size();
RequestInfo requestInfo = list.get(numRequests - 1);
html.append("<td>").append(getSeedNodeInfo(seedNode, requestInfo)).append("</td>")
.append("<td>").append(getRequestInfo(seedNode, requestInfo, numRequests, map)).append("</td>")
.append("<td>").append(getDataInfo(seedNode, requestInfo, map)).append("</td>")
.append("<td>").append(getDaoInfo(seedNode, requestInfo, map)).append("</td>")
.append("<td>").append(getNetworkInfo(seedNode, requestInfo, map)).append("</td>");
} else {
html.append("<td>").append(getSeedNodeInfo(seedNode, null)).append("</td>")
.append("<td>").append("n/a").append("</td>")
.append("<td>").append("n/a").append("</td>")
.append("<td>").append("n/a").append("</td>")
.append("<td>").append("n/a").append("</td>");
}
html.append("</tr>");
});
html.append("</table></body></html>");
return html.toString();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Sub sections
///////////////////////////////////////////////////////////////////////////////////////////
private String getSeedNodeInfo(NodeAddress nodeAddress,
@Nullable RequestInfo requestInfo) {
StringBuilder sb = new StringBuilder();
String operator = operatorByNodeAddress.get(nodeAddress.getFullAddress());
sb.append("Operator: ").append(operator).append("<br/>");
String address = nodeAddress.getFullAddress();
String filteredSeeds = requestInfo != null ? requestInfo.getValue(InventoryItem.filteredSeeds) : null;
if (filteredSeeds != null && filteredSeeds.contains(address)) {
sb.append(getColorTagByDeviationSeverity(DeviationSeverity.ALERT)).append("Node address: ")
.append(address).append(" (is filtered!)").append(CLOSE_TAG);
} else {
sb.append("Node address: ").append(address).append("<br/>");
}
if (requestInfo != null) {
sb.append("Version: ").append(requestInfo.getDisplayValue(InventoryItem.version)).append("<br/>");
sb.append("Commit hash: ").append(requestInfo.getDisplayValue(InventoryItem.commitHash)).append("<br/>");
String memory = requestInfo.getValue(InventoryItem.usedMemory);
String memoryString = memory != null ? Utilities.readableFileSize(Long.parseLong(memory)) : "n/a";
sb.append("Memory used: ")
.append(memoryString)
.append("<br/>");
String jvmStartTimeString = requestInfo.getValue(InventoryItem.jvmStartTime);
long jvmStartTime = jvmStartTimeString != null ? Long.parseLong(jvmStartTimeString) : 0;
sb.append("Node started at: ")
.append(new Date(jvmStartTime).toString())
.append("<br/>");
String duration = jvmStartTime > 0 ?
FormattingUtils.formatDurationAsWords(System.currentTimeMillis() - jvmStartTime,
true, true) :
"n/a";
sb.append("Run duration: ").append(duration).append("<br/>");
String filteredSeedNodes = requestInfo.getDisplayValue(InventoryItem.filteredSeeds)
.replace(System.getProperty("line.separator"), "<br/>");
if (filteredSeedNodes.isEmpty()) {
filteredSeedNodes = "-";
}
sb.append("Filtered seed nodes: ")
.append(filteredSeedNodes)
.append("<br/>");
}
return sb.toString();
}
private String getRequestInfo(NodeAddress seedNode,
RequestInfo requestInfo,
int numRequests,
Map<NodeAddress, List<RequestInfo>> map) {
StringBuilder sb = new StringBuilder();
DeviationSeverity deviationSeverity = numRequests == requestCounter ?
DeviationSeverity.OK :
requestCounter - numRequests > 4 ?
DeviationSeverity.ALERT :
DeviationSeverity.WARN;
sb.append("Number of requests: ").append(getColorTagByDeviationSeverity(deviationSeverity))
.append(numRequests).append(CLOSE_TAG);
DeviationSeverity rrtDeviationSeverity = DeviationSeverity.OK;
String rrtString = "n/a";
if (requestInfo.getResponseTime() > 0) {
long rrt = requestInfo.getResponseTime() - requestInfo.getRequestStartTime();
if (rrt > 20_000) {
rrtDeviationSeverity = DeviationSeverity.ALERT;
} else if (rrt > 10_000) {
rrtDeviationSeverity = DeviationSeverity.WARN;
}
rrtString = MathUtils.roundDouble(rrt / 1000d, 3) + " sec";
}
sb.append("Round trip time: ").append(getColorTagByDeviationSeverity(rrtDeviationSeverity))
.append(rrtString).append(CLOSE_TAG);
Date requestStartTime = new Date(requestInfo.getRequestStartTime());
sb.append("Requested at: ").append(requestStartTime).append("<br/>");
String responseTime = requestInfo.getResponseTime() > 0 ?
new Date(requestInfo.getResponseTime()).toString() :
"n/a";
sb.append("Response received at: ").append(responseTime).append("<br/>");
sb.append(getErrorMsgLine(seedNode, requestInfo, map));
sb.append(getLastSuccessfulResponseLine(seedNode, map));
return sb.toString();
}
private String getDataInfo(NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
StringBuilder sb = new StringBuilder();
sb.append(getLine(InventoryItem.OfferPayload, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.MailboxStoragePayload, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.TradeStatistics3, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.AccountAgeWitness, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.SignedWitness, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.Alert, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.Filter, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.Mediator, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.RefundAgent, seedNode, requestInfo, map));
return sb.toString();
}
private String getDaoInfo(NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
StringBuilder sb = new StringBuilder();
sb.append(getLine("Number of BSQ blocks: ", InventoryItem.numBsqBlocks, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.TempProposalPayload, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.ProposalPayload, seedNode, requestInfo, map));
sb.append(getLine(InventoryItem.BlindVotePayload, seedNode, requestInfo, map));
sb.append(getLine("DAO state block height: ", InventoryItem.daoStateChainHeight, seedNode, requestInfo, map));
sb.append(getLine("DAO state hash: ", InventoryItem.daoStateHash, seedNode, requestInfo, map));
// The hash for proposal changes only at first block of blind vote phase but as we do not want to initialize the
// dao domain we cannot check that. But we also don't need that as we can just compare that all hashes at all
// blocks from all seeds are the same. Same for blindVoteHash.
sb.append(getLine("Proposal state hash: ", InventoryItem.proposalHash, seedNode, requestInfo, map));
sb.append(getLine("Blind vote state hash: ", InventoryItem.blindVoteHash, seedNode, requestInfo, map));
return sb.toString();
}
private String getNetworkInfo(NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
StringBuilder sb = new StringBuilder();
sb.append(getLine("Max. connections: ",
InventoryItem.maxConnections, seedNode, requestInfo, map));
sb.append(getLine("Number of connections: ",
InventoryItem.numConnections, seedNode, requestInfo, map));
sb.append(getLine("Peak number of connections: ",
InventoryItem.peakNumConnections, seedNode, requestInfo, map));
sb.append(getLine("Number of 'All connections lost' events: ",
InventoryItem.numAllConnectionsLostEvents, seedNode, requestInfo, map));
sb.append(getLine("Sent messages/sec: ",
InventoryItem.sentMessagesPerSec, seedNode, requestInfo, map, this::getRounded));
sb.append(getLine("Received messages/sec: ",
InventoryItem.receivedMessagesPerSec, seedNode, requestInfo, map, this::getRounded));
sb.append(getLine("Sent kB/sec: ",
InventoryItem.sentBytesPerSec, seedNode, requestInfo, map, this::getKbRounded));
sb.append(getLine("Received kB/sec: ",
InventoryItem.receivedBytesPerSec, seedNode, requestInfo, map, this::getKbRounded));
sb.append(getLine("Sent data: ",
InventoryItem.sentBytes, seedNode, requestInfo, map,
value -> Utilities.readableFileSize(Long.parseLong(value))));
sb.append(getLine("Received data: ",
InventoryItem.receivedBytes, seedNode, requestInfo, map,
value -> Utilities.readableFileSize(Long.parseLong(value))));
return sb.toString();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private String getLine(InventoryItem inventoryItem,
NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
return getLine(getTitle(inventoryItem),
inventoryItem,
seedNode,
requestInfo,
map);
}
private String getLine(String title,
InventoryItem inventoryItem,
NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
return getLine(title,
inventoryItem,
seedNode,
requestInfo,
map,
null);
}
private String getLine(String title,
InventoryItem inventoryItem,
NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map,
@Nullable Function<String, String> formatter) {
String displayValue = requestInfo.getDisplayValue(inventoryItem);
String value = requestInfo.getValue(inventoryItem);
if (formatter != null && value != null) {
displayValue = formatter.apply(value);
}
String deviationAsPercentString = "";
DeviationSeverity deviationSeverity = DeviationSeverity.OK;
if (requestInfo.getDataMap().containsKey(inventoryItem)) {
RequestInfo.Data data = requestInfo.getDataMap().get(inventoryItem);
deviationAsPercentString = getDeviationAsPercentString(inventoryItem, data);
deviationSeverity = data.getDeviationSeverity();
}
List<RequestInfo> requestInfoList = map.get(seedNode);
String historicalWarnings = "";
String historicalAlerts = "";
List<String> warningsAtRequestNumber = new ArrayList<>();
List<String> alertsAtRequestNumber = new ArrayList<>();
if (requestInfoList != null) {
for (int i = 0; i < requestInfoList.size(); i++) {
RequestInfo reqInfo = requestInfoList.get(i);
Map<InventoryItem, RequestInfo.Data> deviationInfoMap = reqInfo.getDataMap();
if (deviationInfoMap.containsKey(inventoryItem)) {
RequestInfo.Data data = deviationInfoMap.get(inventoryItem);
String deviationAsPercent = getDeviationAsPercentString(inventoryItem, data);
if (data.isPersistentWarning()) {
warningsAtRequestNumber.add((i + 1) + deviationAsPercent);
} else if (data.isPersistentAlert()) {
alertsAtRequestNumber.add((i + 1) + deviationAsPercent);
}
}
}
if (!warningsAtRequestNumber.isEmpty()) {
historicalWarnings = warningsAtRequestNumber.size() + " repeated warning(s) at request(s) " +
Joiner.on(", ").join(warningsAtRequestNumber);
}
if (!alertsAtRequestNumber.isEmpty()) {
historicalAlerts = alertsAtRequestNumber.size() + " repeated alert(s) at request(s): " +
Joiner.on(", ").join(alertsAtRequestNumber);
}
}
String historicalWarningsHtml = warningsAtRequestNumber.isEmpty() ? "" :
", <b><a id=\"warn\" href=\"#\" title=\"" + historicalWarnings + "\">" + WARNING_ICON +
warningsAtRequestNumber.size() + "</a></b>";
String historicalAlertsHtml = alertsAtRequestNumber.isEmpty() ? "" :
", <b><a id=\"alert\" href=\"#\" title=\"" + historicalAlerts + "\">" + ALERT_ICON +
alertsAtRequestNumber.size() + "</a></b>";
return title +
getColorTagByDeviationSeverity(deviationSeverity) +
displayValue +
deviationAsPercentString +
historicalWarningsHtml +
historicalAlertsHtml +
CLOSE_TAG;
}
private String getDeviationAsPercentString(InventoryItem inventoryItem, RequestInfo.Data data) {
Double deviation = data.getDeviation();
if (deviation == null || deviation == 1) {
return "";
}
if (inventoryItem.getDeviationType() instanceof DeviationByPercentage) {
return getDeviationInRoundedPercent(deviation);
} else if (inventoryItem.getDeviationType() instanceof DeviationByIntegerDiff) {
// For larger numbers like chain height we need to show all decimals as diff can be very small
return getDeviationInExactPercent(deviation);
} else {
return "";
}
}
private String getDeviationInRoundedPercent(double deviation) {
return " (" + MathUtils.roundDouble(100 * deviation, 2) + " %)";
}
private String getDeviationInExactPercent(double deviation) {
return " (" + 100 * deviation + " %)";
}
private String getColorTagByDeviationSeverity(@Nullable DeviationSeverity deviationSeverity) {
if (deviationSeverity == null) {
return "<font color=\"black\">";
}
switch (deviationSeverity) {
case WARN:
return "<font color=\"#0000cc\">";
case ALERT:
return "<font color=\"#cc0000\">";
case IGNORED:
return "<font color=\"#333333\">";
case OK:
default:
return "<font color=\"black\">";
}
}
private String getTitle(InventoryItem inventoryItem) {
return "Number of " + inventoryItem.getKey() + ": ";
}
private String getRounded(String value) {
return String.valueOf(MathUtils.roundDouble(Double.parseDouble(value), 2));
}
private String getKbRounded(String bytes) {
return String.valueOf(MathUtils.roundDouble(Double.parseDouble(bytes) / 1000, 2));
}
private void setupOperatorMap(BufferedReader seedNodeFile) {
seedNodeFile.lines().forEach(line -> {
if (!line.startsWith("#")) {
String[] strings = line.split(" \\(@");
String node = strings.length > 0 ? strings[0] : "n/a";
String operator = strings.length > 1 ? strings[1].replace(")", "") : "n/a";
operatorByNodeAddress.put(node, operator);
}
});
}
// We use here a bit diff. model as with other historical data alerts/warnings as we do not store it in the data
// object as we do with normal inventoryItems. So the historical error msg are not available in the json file.
// If we need it we have to move that handling here to the InventoryMonitor and change the data model to support the
// missing data for error messages.
private String getErrorMsgLine(NodeAddress seedNode,
RequestInfo requestInfo,
Map<NodeAddress, List<RequestInfo>> map) {
String errorMessage = requestInfo.hasError() ? requestInfo.getErrorMessage() : "-";
List<RequestInfo> requestInfoList = map.get(seedNode);
List<String> errorsAtRequestNumber = new ArrayList<>();
String historicalErrorsHtml = "";
if (requestInfoList != null) {
for (int i = 0; i < requestInfoList.size(); i++) {
RequestInfo requestInfo1 = requestInfoList.get(i);
// We ignore old errors as at startup timeouts are expected and each node restarts once a day
long duration = System.currentTimeMillis() - requestInfo1.getRequestStartTime();
if (requestInfo1.getRequestStartTime() > 0 && duration > TimeUnit.HOURS.toMillis(24)) {
continue;
}
if (requestInfo1.hasError()) {
errorsAtRequestNumber.add((i + 1) + " (" + requestInfo1.getErrorMessage() + ")");
}
}
if (!errorsAtRequestNumber.isEmpty()) {
String errorIcon;
String type;
String style;
if (errorsAtRequestNumber.size() > 4) {
errorIcon = ALERT_ICON;
type = "alert";
style = "alert";
} else {
errorIcon = WARNING_ICON;
type = "warning";
style = "warn";
}
String historicalAlerts = errorsAtRequestNumber.size() + " repeated " + type + "(s) at request(s): " +
Joiner.on(", ").join(errorsAtRequestNumber);
historicalErrorsHtml = errorsAtRequestNumber.isEmpty() ? "" :
", <b><a id=\"" + style + "\" href=\"#\" title=\"" + historicalAlerts + "\">" + errorIcon +
errorsAtRequestNumber.size() + "</a></b>";
}
}
DeviationSeverity deviationSeverity = requestInfo.hasError() ?
errorsAtRequestNumber.size() > 4 ? DeviationSeverity.ALERT : DeviationSeverity.WARN
: DeviationSeverity.OK;
return "Error message: " +
getColorTagByDeviationSeverity(deviationSeverity) +
errorMessage +
historicalErrorsHtml +
CLOSE_TAG;
}
private String getLastSuccessfulResponseLine(NodeAddress seedNode,
Map<NodeAddress, List<RequestInfo>> map) {
long newestResponseTime = 0;
List<RequestInfo> requestInfoList = map.get(seedNode);
if (requestInfoList != null) {
for (int i = 0; i < requestInfoList.size(); i++) {
RequestInfo requestInfo1 = requestInfoList.get(i);
newestResponseTime = Math.max(newestResponseTime, requestInfo1.getResponseTime());
}
}
String responseMessage = newestResponseTime > 0 ? new Date(newestResponseTime).toString() : "none";
return "Last response received: " +
responseMessage +
CLOSE_TAG;
}
}

View File

@ -0,0 +1,10 @@
# nodeaddress.onion:port [(@owner,@backup)]
devinv3rhon24gqf5v6ondoqgyrbzyqihzyouzv7ptltsewhfmox2zqd.onion:8000 (@devinbileck)
devinsn2teu33efff62bnvwbxmfgbfjlgqsu3ad4b4fudx3a725eqnyd.onion:8000 (@devinbileck)
devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbileck)
sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy)
sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy)
sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy)
y6mvobc6tfp7l3rq2rae7hayvkmy35su33cun2mfdbzd4uw536pqtlyd.onion:8000 (@jester4042)
xaebw6lpcckkyb2anyzyfynthebek2abqzyhchenfrdqlfktezgtrtqd.onion:8000 (@jester4042)
33ol66i7crfrnypjlpd7kem3imo6zmjkvxdr6ipcdx3e2skbpcrfzuqd.onion:8000 (@jester4042)

View File

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

View File

@ -17,12 +17,12 @@
package bisq.network;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.Socks5ProxyInternalFactory;
import bisq.common.config.Config;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import com.google.inject.Inject;
import javax.inject.Named;
@ -49,7 +49,7 @@ public class Socks5ProxyProvider {
private static final Logger log = LoggerFactory.getLogger(Socks5ProxyProvider.class);
@Nullable
private NetworkNode socks5ProxyInternalFactory;
private Socks5ProxyInternalFactory socks5ProxyInternalFactory;
// proxy used for btc network
@Nullable
@ -91,7 +91,7 @@ public class Socks5ProxyProvider {
return socks5ProxyInternalFactory.getSocksProxy();
}
public void setSocks5ProxyInternal(@Nullable NetworkNode bisqSocks5ProxyFactory) {
public void setSocks5ProxyInternal(@Nullable Socks5ProxyInternalFactory bisqSocks5ProxyFactory) {
this.socks5ProxyInternalFactory = bisqSocks5ProxyFactory;
}

View File

@ -19,9 +19,6 @@ package bisq.network.p2p.network;
import java.util.List;
import org.jetbrains.annotations.Nullable;
public interface BridgeAddressProvider {
@Nullable
List<String> getBridgeAddresses();
}

View File

@ -64,7 +64,7 @@ import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
// Run in UserThread
public abstract class NetworkNode implements MessageListener {
public abstract class NetworkNode implements MessageListener, Socks5ProxyInternalFactory {
private static final Logger log = LoggerFactory.getLogger(NetworkNode.class);
private static final int CREATE_SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(120);
@ -277,6 +277,7 @@ public abstract class NetworkNode implements MessageListener {
}
}
@Override
@Nullable
public Socks5Proxy getSocksProxy() {
return null;

View File

@ -0,0 +1,24 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.network.p2p.network;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
public interface Socks5ProxyInternalFactory {
Socks5Proxy getSocksProxy();
}

View File

@ -104,6 +104,7 @@ public class TorNetworkNode extends NetworkNode {
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
}
@Override
public Socks5Proxy getSocksProxy() {
try {
String stream = null;

48
restapi/README.md Normal file
View File

@ -0,0 +1,48 @@
# Rest API node
Simple headless node with a Rest API to provide access to Bisq network data as well as Bisq DAO data.
It is used for Bisq 2 to request data about the DAO state as well as account age and account witness data for reputation use cases.
To run 'RestApiMain' you need to have Bitcoin node running and have 'blocknotify' in the `bitcoin.conf` set up.
### Run Rest API node
Run the Gradle task:
```sh
./gradlew restapi:run
```
Or create a run scrip by:
```sh
./gradlew restapi:startBisqApp
```
And then run:
```sh
./bisq-restapi
```
### Customize with program arguments
Example program arguments for running at localhost with Regtest:
```sh
./bisq-restapi \
--baseCurrencyNetwork=BTC_REGTEST \
--useDevPrivilegeKeys=true \
--useLocalhostForP2P=true \
--nodePort=3333 \
--appName=bisq-BTC_REGTEST_restapi \
--fullDaoNode=true \
--rpcUser=[RPC USER] \
--rpcPassword=[RPC PW] \
--rpcPort=18443 \
--rpcBlockNotificationPort=5123
```

36
restapi/build.gradle Normal file
View File

@ -0,0 +1,36 @@
plugins {
id 'bisq.application'
id 'bisq.gradle.app_start_plugin.AppStartPlugin'
}
mainClassName = 'bisq.restapi.RestApiMain'
distTar.enabled = false
dependencies {
implementation project(':common')
implementation project(':p2p')
implementation project(':core')
annotationProcessor libs.lombok
compileOnly libs.lombok
implementation libs.slf4j.api
implementation(libs.google.guice) {
exclude(module: 'guava')
}
implementation libs.google.guava
implementation libs.google.guice
implementation libs.glassfish.jaxb
implementation libs.bundles.jersey.libs
implementation libs.swagger
implementation libs.logback.core
implementation libs.logback.classic
compileOnly libs.lombok
annotationProcessor libs.lombok
implementation libs.slf4j.api
testAnnotationProcessor libs.lombok
testCompileOnly libs.lombok
}

View File

@ -0,0 +1,218 @@
/*
* 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.restapi;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.app.TorSetup;
import bisq.core.app.misc.AppSetupWithP2PAndDAO;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.core.dao.SignVerifyService;
import bisq.core.dao.governance.bond.reputation.BondedReputationRepository;
import bisq.core.dao.governance.bond.role.BondedRolesRepository;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.user.Cookie;
import bisq.core.user.CookieKey;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.P2PServiceListener;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
import com.google.inject.Key;
import com.google.inject.name.Names;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
//todo not sure if the restart handling from seed nodes is required
@Slf4j
public class RestApi extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private Timer checkConnectionLossTime;
@Getter
private DaoStateService daoStateService;
@Getter
private BondedReputationRepository bondedReputationRepository;
@Getter
private AccountAgeWitnessService accountAgeWitnessService;
@Getter
private BondedRolesRepository bondedRolesRepository;
@Getter
private SignVerifyService signVerifyService;
public RestApi() {
super("Bisq Rest Api", "bisq_restapi", "bisq_restapi", Version.VERSION);
}
public Config getConfig() {
return config;
}
@Override
protected void doExecute() {
super.doExecute();
checkMemory(config, this);
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
onApplicationLaunched();
} catch (Exception e) {
e.printStackTrace();
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new ModuleForAppWithP2p(config);
}
@Override
protected void applyInjector() {
super.applyInjector();
injector.getInstance(DaoStateSnapshotService.class).setDaoRequiresRestartHandler(this::gracefulShutDown);
}
@Override
protected void startApplication() {
super.startApplication();
Cookie cookie = injector.getInstance(User.class).getCookie();
cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> {
if (wasCleanTorDirSet) {
injector.getInstance(TorSetup.class).cleanupTorFiles(() -> {
log.info("Tor directory reset");
cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART);
}, log::error);
}
});
injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false);
injector.getInstance(AppSetupWithP2PAndDAO.class).start();
daoStateService = injector.getInstance(DaoStateService.class);
accountAgeWitnessService = injector.getInstance(AccountAgeWitnessService.class);
bondedReputationRepository = injector.getInstance(BondedReputationRepository.class);
bondedRolesRepository = injector.getInstance(BondedRolesRepository.class);
signVerifyService = injector.getInstance(SignVerifyService.class);
injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
@Override
public void onDataReceived() {
// Do nothing
}
@Override
public void onNoSeedNodeAvailable() {
// Do nothing
}
@Override
public void onNoPeersAvailable() {
// Do nothing
}
@Override
public void onUpdatedDataReceived() {
// Do nothing
}
@Override
public void onTorNodeReady() {
// Do nothing
}
@Override
public void onHiddenServicePublished() {
boolean preventPeriodicShutdownAtSeedNode = injector.getInstance(Key.get(boolean.class,
Names.named(Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE)));
if (!preventPeriodicShutdownAtSeedNode) {
startShutDownInterval(RestApi.this);
}
UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
accountAgeWitnessService.onAllServicesInitialized();
}
@Override
public void onSetupFailed(Throwable throwable) {
// Do nothing
}
@Override
public void onRequestCustomBridges() {
// Do nothing
}
});
}
private void setupConnectionLossCheck() {
// For dev testing (usually on BTC_REGTEST) we don't want to get the seed shut
// down as it is normal that the seed is the only actively running node.
if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_REGTEST) {
return;
}
if (checkConnectionLossTime != null) {
return;
}
checkConnectionLossTime = UserThread.runPeriodically(() -> {
if (injector.getInstance(PeerManager.class).getNumAllConnectionsLostEvents() > 1) {
// We set a flag to clear tor cache files at re-start. We cannot clear it now as Tor is used and
// that can cause problems.
injector.getInstance(User.class).getCookie().putAsBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART, true);
shutDown(this);
}
}, CHECK_CONNECTION_LOSS_SEC);
}
public void gracefulShutDown() {
gracefulShutDown(() -> {
});
}
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
super.gracefulShutDown(resultHandler);
}
}

View File

@ -0,0 +1,129 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi;
import bisq.common.config.Config;
import java.net.URI;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import bisq.restapi.endpoints.AccountAgeApi;
import bisq.restapi.endpoints.BondedReputationApi;
import bisq.restapi.endpoints.BondedRoleVerificationApi;
import bisq.restapi.endpoints.ProofOfBurnApi;
import bisq.restapi.endpoints.SignedWitnessApi;
import bisq.restapi.error.CustomExceptionMapper;
import bisq.restapi.error.StatusException;
import bisq.restapi.swagger.StaticFileHandler;
import bisq.restapi.swagger.SwaggerResolution;
import com.sun.net.httpserver.HttpServer;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
/**
* Application to start and config the rest service.
* This creates a rest service for clients to connect and for users to browse the documentation.
* <p>
* Swagger doc are available at <a href="http://localhost:8082/doc/v1/index.html">REST API documentation</a>
*/
@Slf4j
public class RestApiMain extends ResourceConfig {
@Getter
private static String baseUrl;
public static void main(String[] args) throws Exception {
RestApiMain daoNodeApplication = new RestApiMain();
daoNodeApplication.start(args, config -> {
daoNodeApplication
.register(CustomExceptionMapper.class)
.register(StatusException.StatusExceptionMapper.class)
.register(ProofOfBurnApi.class)
.register(BondedReputationApi.class)
.register(BondedRoleVerificationApi.class)
.register(AccountAgeApi.class)
.register(SignedWitnessApi.class)
.register(SwaggerResolution.class);
daoNodeApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort);
});
}
@Getter
private final RestApi restApi;
private HttpServer httpServer;
public RestApiMain() {
restApi = new RestApi();
}
private void start(String[] args, Consumer<Config> configConsumer) {
new Thread(() -> {
restApi.execute(args);
configConsumer.accept(restApi.getConfig());
try {
// Keep running
Thread.currentThread().setName("daoNodeThread");
Thread.currentThread().join();
} catch (InterruptedException e) {
log.error("daoNodeThread interrupted", e);
shutDown();
}
}).start();
}
private void startServer(String url, int port) {
baseUrl = url + ":" + port + "/api/v1";
try {
httpServer = JdkHttpServerFactory.createHttpServer(URI.create(baseUrl), this);
httpServer.createContext("/doc", new StaticFileHandler("/doc/v1/"));
Runtime.getRuntime().addShutdownHook(new Thread(this::shutDown));
log.info("Server started at {}.", baseUrl);
} catch (Exception e1) {
log.error("Exception at startServer:", e1);
}
// block and wait shut down signal, like CTRL+C
try {
Thread.currentThread().setName("serverThread");
Thread.currentThread().join();
} catch (InterruptedException e) {
log.error("serverThread interrupted", e);
System.exit(1);
}
shutDown();
}
private void shutDown() {
stopServer();
restApi.gracefulShutDown();
}
private void stopServer() {
if (httpServer != null) {
httpServer.stop(1);
}
}
}

View File

@ -0,0 +1,50 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.dto;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Minimal data required for Bisq 2 bonded reputation use case.
* Need to be in sync with the Bisq 2 BondedReputationDto class.
*/
@Getter
@Slf4j
@ToString
@Schema(title = "BondedReputation")
public class BondedReputationDto {
private final long amount;
private final long time;
private final String hash;
private final int blockHeight;
private final int lockTime;
public BondedReputationDto(long amount, long time, String hash, int blockHeight, int lockTime) {
this.amount = amount;
this.time = time;
this.hash = hash;
this.blockHeight = blockHeight;
this.lockTime = lockTime;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.restapi.dto;
import lombok.Getter;
import javax.annotation.Nullable;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Minimal data required for Bisq 2 bonded roles use case.
* Need to be in sync with the Bisq 2 BondedRoleDto class.
*/
@Getter
@Schema(title = "BondedRoleVerification")
public class BondedRoleVerificationDto {
@Nullable
private final String errorMessage;
public BondedRoleVerificationDto() {
errorMessage = null;
}
public BondedRoleVerificationDto(String errorMessage) {
this.errorMessage = errorMessage;
}
}

View File

@ -0,0 +1,46 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.dto;
import lombok.Getter;
import lombok.ToString;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Minimal data required for Bisq 2 proof of burn use case.
* Need to be in sync with the Bisq 2 ProofOfBurnDto class.
*/
@Getter
@ToString
@Schema(title = "ProofOfBurn")
public class ProofOfBurnDto {
private final long amount;
private final long time;
private final String hash;
private final int blockHeight;
public ProofOfBurnDto(long amount, long time, String hash, int blockHeight) {
this.amount = amount;
this.time = time;
this.hash = hash;
this.blockHeight = blockHeight;
}
}

View File

@ -0,0 +1,83 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.endpoints;
import bisq.core.account.witness.AccountAgeWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.restapi.RestApi;
import bisq.restapi.RestApiMain;
import bisq.restapi.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the account age date from a given hash as hex string.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/account-age/get-date/dd75a7175c7c83fe9a4729e36b85f5fbc44e29ae">Request with hash</a>
*/
@Slf4j
@Path("/account-age")
@Produces(MediaType.TEXT_PLAIN)
@Tag(name = "Account age API")
public class AccountAgeApi {
private static final String DESC_HASH = "The hash of the account age witness as hex string";
private final RestApi restApi;
private final AccountAgeWitnessService accountAgeWitnessService;
public AccountAgeApi(@Context Application application) {
restApi = ((RestApiMain) application).getRestApi();
accountAgeWitnessService = checkNotNull(restApi.getAccountAgeWitnessService());
}
@Operation(description = "Request the account age date")
@ApiResponse(responseCode = "200", description = "The account age date",
content = {@Content(mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(allOf = ProofOfBurnDto.class))}
)
@GET
@Path("get-date/{hash}")
public Long getDate(@Parameter(description = DESC_HASH)
@PathParam("hash")
String hash) {
long result = accountAgeWitnessService.getWitnessByHashAsHex(hash)
.map(AccountAgeWitness::getDate)
.orElse(-1L);
log.info("Account age for hash {}: {} ({})", hash, result, new Date(result));
return result;
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.restapi.endpoints;
import bisq.core.dao.governance.bond.reputation.BondedReputationRepository;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.common.util.Hex;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import bisq.restapi.RestApi;
import bisq.restapi.RestApiMain;
import bisq.restapi.dto.BondedReputationDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the bonded reputation data from a given block height.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/bonded-reputation/get-bonded-reputation/0">Request with block height 0</a>
*/
@Slf4j
@Path("/bonded-reputation")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Bonded reputation API")
public class BondedReputationApi {
private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the bonded reputation data";
private final BondedReputationRepository bondedReputationRepository;
private final DaoStateService daoStateService;
public BondedReputationApi(@Context Application application) {
RestApi restApi = ((RestApiMain) application).getRestApi();
daoStateService = restApi.getDaoStateService();
bondedReputationRepository = restApi.getBondedReputationRepository();
}
@Operation(description = "Request the bonded reputation data")
@ApiResponse(responseCode = "200", description = "The bonded reputation data",
content = {@Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(allOf = BondedReputationDto.class))}
)
@GET
@Path("get-bonded-reputation/{block-height}")
public List<BondedReputationDto> getBondedReputation(@Parameter(description = DESC_BLOCK_HEIGHT)
@PathParam("block-height")
int fromBlockHeight) {
// We only consider lock time with at least 50 000 blocks as valid
List<BondedReputationDto> result = bondedReputationRepository.getActiveBonds().stream()
.filter(bondedReputation -> bondedReputation.getLockTime() >= 50_000)
.map(bondedReputation -> {
Optional<Tx> optionalTx = daoStateService.getTx(bondedReputation.getLockupTxId());
if (optionalTx.isEmpty()) {
return null;
}
Tx tx = optionalTx.get();
int blockHeight = tx.getBlockHeight();
if (blockHeight >= fromBlockHeight) {
return new BondedReputationDto(bondedReputation.getAmount(),
tx.getTime(),
Hex.encode(bondedReputation.getBondedAsset().getHash()),
blockHeight,
bondedReputation.getLockTime()
);
} else {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
log.info("BondedReputation result list from block height {}: {}", fromBlockHeight, result);
return result;
}
}

View File

@ -0,0 +1,107 @@
/*
* 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.restapi.endpoints;
import bisq.core.dao.SignVerifyService;
import bisq.core.dao.governance.bond.BondState;
import bisq.core.dao.governance.bond.role.BondedRolesRepository;
import bisq.core.dao.state.DaoStateService;
import bisq.common.util.Base64;
import bisq.common.util.Hex;
import java.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import bisq.restapi.RestApi;
import bisq.restapi.RestApiMain;
import bisq.restapi.dto.BondedRoleVerificationDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the bonded role verification for the given parameters.
* Used for bonded roles in Bisq 2.
* <a href="http://localhost:8082/api/v1/bonded-roles/get-bonded-role-verification">Request the verification of a bonded role with the provided parameters</a>
*/
@Slf4j
@Path("/bonded-role-verification")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Bonded role API")
public class BondedRoleVerificationApi {
private final DaoStateService daoStateService;
private final BondedRolesRepository bondedRolesRepository;
private final SignVerifyService signVerifyService;
public BondedRoleVerificationApi(@Context Application application) {
RestApi restApi = ((RestApiMain) application).getRestApi();
daoStateService = restApi.getDaoStateService();
bondedRolesRepository = restApi.getBondedRolesRepository();
signVerifyService = restApi.getSignVerifyService();
}
@Operation(description = "Request the verification of a bonded role with the provided parameters")
@ApiResponse(responseCode = "200", description = "A BondedRoleVerification result object",
content = {@Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(allOf = BondedRoleVerificationDto.class))}
)
@GET
@Path("get-bonded-role-verification/{bond-user-name}/{role-type}/{profile-id}/{signature}")
public BondedRoleVerificationDto getBondedRoleVerification(@PathParam("bond-user-name") String bondUserName,
@PathParam("role-type") String roleType,
@PathParam("profile-id") String profileId,
@PathParam("signature") String signature) {
log.info("Received request for verifying a bonded role. bondUserName={}, roleType={}, profileId={}, signature={}",
bondUserName, roleType, profileId, signature);
String signatureBase64 = Base64.encode(Hex.decode(signature));
return bondedRolesRepository.getAcceptedBonds().stream()
.filter(bondedRole -> bondedRole.getBondedAsset().getBondedRoleType().name().equals(roleType))
.filter(bondedRole -> bondedRole.getBondedAsset().getName().equals(bondUserName))
.filter(bondedRole -> bondedRole.getBondState() == BondState.LOCKUP_TX_CONFIRMED)
.flatMap(bondedRole -> daoStateService.getTx(bondedRole.getLockupTxId()).stream())
.map(tx -> tx.getTxInputs().get(0))
.map(txInput -> {
try {
signVerifyService.verify(profileId, txInput.getPubKey(), signatureBase64);
log.info("Successfully verified bonded role");
return new BondedRoleVerificationDto();
} catch (SignatureException e) {
return new BondedRoleVerificationDto("Signature verification failed.");
}
})
.findAny()
.orElseGet(() -> {
String errorMessage = "Did not find a bonded role matching the parameters";
log.warn(errorMessage);
return new BondedRoleVerificationDto(errorMessage);
});
}
}

View File

@ -0,0 +1,89 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.endpoints;
import bisq.core.dao.governance.proofofburn.ProofOfBurnService;
import bisq.core.dao.state.DaoStateService;
import bisq.common.util.Hex;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.restapi.RestApi;
import bisq.restapi.RestApiMain;
import bisq.restapi.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the proof of burn data from a given block height.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/proof-of-burn/get-proof-of-burn/0">Request with block height 0</a>
*/
@Slf4j
@Path("/proof-of-burn")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Proof of burn API")
public class ProofOfBurnApi {
private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the proof of burn data";
private final DaoStateService daoStateService;
public ProofOfBurnApi(@Context Application application) {
RestApi restApi = ((RestApiMain) application).getRestApi();
daoStateService = checkNotNull(restApi.getDaoStateService());
}
@Operation(description = "Request the proof of burn data")
@ApiResponse(responseCode = "200", description = "The proof of burn data",
content = {@Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(allOf = ProofOfBurnDto.class))}
)
@GET
@Path("get-proof-of-burn/{block-height}")
public List<ProofOfBurnDto> getProofOfBurn(@Parameter(description = DESC_BLOCK_HEIGHT)
@PathParam("block-height")
int fromBlockHeight) {
List<ProofOfBurnDto> result = daoStateService.getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() >= fromBlockHeight)
.map(tx -> new ProofOfBurnDto(tx.getBurntBsq(),
tx.getTime(),
Hex.encode(ProofOfBurnService.getHashFromOpReturnData(tx)),
tx.getBlockHeight()))
.collect(Collectors.toList());
log.info("ProofOfBurn result list from block height {}: {}", fromBlockHeight, result);
return result;
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.restapi.endpoints;
import bisq.core.account.witness.AccountAgeWitnessService;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.restapi.RestApi;
import bisq.restapi.RestApiMain;
import bisq.restapi.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the signed witness date from a given hash as hex string.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/signed-witness/get-date/dd75a7175c7c83fe9a4729e36b85f5fbc44e29ae">Request with hash</a>
*/
@Slf4j
@Path("/signed-witness")
@Produces(MediaType.TEXT_PLAIN)
@Tag(name = "Signed witness API")
public class SignedWitnessApi {
private static final String DESC_HASH = "The hash of the signed account age witness as hex string";
private final AccountAgeWitnessService accountAgeWitnessService;
public SignedWitnessApi(@Context Application application) {
RestApi restApi = ((RestApiMain) application).getRestApi();
accountAgeWitnessService = checkNotNull(restApi.getAccountAgeWitnessService());
}
@Operation(description = "Request the signed witness date")
@ApiResponse(responseCode = "200", description = "The signed witness date",
content = {@Content(mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(allOf = ProofOfBurnDto.class))}
)
@GET
@Path("get-date/{hash}")
public Long getDate(@Parameter(description = DESC_HASH)
@PathParam("hash")
String hash) {
long result = accountAgeWitnessService.getWitnessByHashAsHex(hash)
.map(accountAgeWitnessService::getWitnessSignDate)
.orElse(-1L);
log.info("SignedWitness sign date for hash {}: {} ({})", hash, result, new Date(result));
return result;
}
}

View File

@ -0,0 +1,38 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.error;
import lombok.extern.slf4j.Slf4j;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Slf4j
@Provider
public class CustomExceptionMapper implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
log.error("Exception: ", exception);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorMessage(exception.getMessage()))
.build();
}
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.error;
import lombok.Getter;
@Getter
public class ErrorMessage {
private final String error;
public ErrorMessage(String error) {
this.error = error;
}
}

View File

@ -0,0 +1,55 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.error;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Slf4j
@Provider
public class StatusException extends RuntimeException {
@Getter
@Setter
protected Response.Status httpStatus;
public StatusException() {
}
public StatusException(Response.Status httpStatus, String message) {
super(message);
this.httpStatus = httpStatus;
}
public static class StatusExceptionMapper implements ExceptionMapper<StatusException> {
@Override
public Response toResponse(StatusException exception) {
log.error("Exception: ", exception);
return Response.status(exception.getHttpStatus())
.entity(new ErrorMessage(exception.getMessage()))
.build();
}
}
}

View File

@ -0,0 +1,108 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.swagger;
import java.net.URI;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
/**
* JDK Server needs handler for serving files, will change in JDK 18
* Currently this is only to serve the swagger-ui content to the client.
* So any call to this handler must begin with api/v1. We keep v1 in case
* we will have incompatible changes in the future.
* This handler is limited to html, css, json and javascript files.
*/
@Slf4j
public class StaticFileHandler implements HttpHandler {
private static final String NOT_FOUND = "404 (Not Found)\n";
public static final String[] VALID_SUFFIX = {".html", ".json", ".css", ".js"};
@Getter
protected final String rootContext;
public StaticFileHandler(String rootContext) {
this.rootContext = rootContext;
}
public void handle(HttpExchange exchange) throws IOException {
URI uri = exchange.getRequestURI();
log.debug("requesting: " + uri.getPath());
String filename = uri.getPath();
if (filename == null || !filename.startsWith(rootContext) ||
Arrays.stream(VALID_SUFFIX).noneMatch(filename::endsWith)) {
respond404(exchange);
return;
}
// resource loading without leading slash
String resourceName = filename.replace("..", "");
if (filename.charAt(0) == '/') {
resourceName = filename.substring(1);
}
// we are using getResourceAsStream to ultimately prevent load from parent directories
try (InputStream resource = getClass().getClassLoader().getResourceAsStream(resourceName)) {
if (resource == null) {
respond404(exchange);
return;
}
log.debug("sending: " + resourceName);
// Object exists and is a file: accept with response code 200.
String mime = "text/html";
if (resourceName.endsWith(".js")) mime = "application/javascript";
if (resourceName.endsWith(".json")) mime = "application/json";
if (resourceName.endsWith(".css")) mime = "text/css";
if (resourceName.endsWith(".png")) mime = "image/png";
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", mime);
headers.add("Cache-Control", "max-age=3600"); // cache static content on browser for 3600 seconds
exchange.sendResponseHeaders(200, 0);
try (OutputStream outputStream = exchange.getResponseBody()) {
byte[] buffer = new byte[0x10000];
int count;
while ((count = resource.read(buffer)) >= 0) {
outputStream.write(buffer, 0, count);
}
}
}
}
private void respond404(HttpExchange exchange) throws IOException {
// Object does not exist or is not a file: reject with 404 error.
exchange.sendResponseHeaders(404, NOT_FOUND.length());
try (OutputStream outputStream = exchange.getResponseBody()) {
outputStream.write(NOT_FOUND.getBytes());
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.restapi.swagger;
import lombok.extern.slf4j.Slf4j;
import bisq.restapi.RestApiMain;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.integration.SwaggerConfiguration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
@Slf4j
@Path("openapi.json")
@Produces(MediaType.APPLICATION_JSON)
@Hidden
public class SwaggerResolution {
private static String swaggerJson;
@GET
public String swagIt(@Context Application application) {
if (swaggerJson == null) {
try {
OpenAPI api = new OpenAPI();
Info info = new Info()
.title("Bisq DAO node REST API")
.description("This is the rest API description for the Bisq DAO node, For more Information about Bisq, see https://bisq.network")
.license(new License()
.name("GNU Affero General Public License")
.url("https://github.com/bisq-network/bisq2/blob/main/LICENSE"));
api.info(info).addServersItem(new Server().url(RestApiMain.getBaseUrl()));
SwaggerConfiguration configuration = new SwaggerConfiguration().openAPI(api);
Reader reader = new Reader(configuration);
OpenAPI openAPI = reader.read(application.getClasses());
swaggerJson = Json.pretty(openAPI);
} catch (Exception exception) {
log.error("", exception);
throw new RuntimeException(exception);
}
}
return swaggerJson;
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View File

@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

View File

@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css"/>
<link rel="stylesheet" type="text/css" href="index.css"/>
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32"/>
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16"/>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script src="./swagger-initializer.js" charset="UTF-8"></script>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,20 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/api/v1/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
//</editor-fold>
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -21,13 +21,16 @@ toolchainManagement {
include 'proto'
include 'assets'
include 'btcnodemonitor'
include 'common'
include 'p2p'
include 'core'
include 'cli'
include 'daemon'
include 'desktop'
include 'inventory'
include 'persistence'
include 'restapi'
include 'seednode'
include 'statsnode'
include 'apitest'