diff --git a/build.gradle b/build.gradle
index dd0376ba0c..b1d8e371d5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -103,6 +103,7 @@ configure([project(':cli'),
project(':seednode'),
project(':statsnode'),
project(':pricenode'),
+ project(':daonode'),
project(':inventory'),
project(':apitest')]) {
@@ -686,6 +687,49 @@ configure(project(':seednode')) {
}
}
+configure(project(':daonode')) {
+ apply plugin: 'com.github.johnrengelman.shadow'
+
+ mainClassName = 'bisq.daonode.DaoNodeMain'
+
+ dependencies {
+ implementation project(':common')
+ implementation project(':proto')
+ implementation project(':p2p')
+ implementation project(':core')
+ implementation "com.google.guava:guava:$guavaVersion"
+ implementation("com.google.inject:guice:$guiceVersion") {
+ exclude(module: 'guava')
+ }
+
+ annotationProcessor "org.projectlombok:lombok:$lombokVersion"
+ compileOnly "org.projectlombok:lombok:$lombokVersion"
+ implementation "ch.qos.logback:logback-classic:$logbackVersion"
+ implementation "ch.qos.logback:logback-core:$logbackVersion"
+ implementation "org.slf4j:slf4j-api:$slf4jVersion"
+
+ implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+ implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
+
+ implementation "com.google.protobuf:protobuf-java-util:$protobufVersion"
+
+ testImplementation "org.mockito:mockito-core:$mockitoVersion"
+
+ testCompileOnly "org.projectlombok:lombok:$lombokVersion"
+ testImplementation "org.apache.httpcomponents:httpclient:$httpclientVersion"
+ testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
+ testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
+ }
+
+ test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+ }
+}
configure(project(':statsnode')) {
mainClassName = 'bisq.statistics.StatisticsMain'
diff --git a/daonode/src/main/java/bisq/daonode/DaoNode.java b/daonode/src/main/java/bisq/daonode/DaoNode.java
new file mode 100644
index 0000000000..8180e0714c
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/DaoNode.java
@@ -0,0 +1,65 @@
+/*
+ * 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 .
+ */
+
+package bisq.daonode;
+
+import bisq.core.app.misc.AppSetup;
+import bisq.core.app.misc.AppSetupWithP2PAndDAO;
+import bisq.core.dao.state.DaoStateService;
+import bisq.core.network.p2p.inventory.GetInventoryRequestHandler;
+import bisq.core.user.Preferences;
+
+import com.google.inject.Injector;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+
+
+import bisq.daonode.service.DaoNodeService;
+
+@Slf4j
+public class DaoNode {
+ @Setter
+ private Injector injector;
+ private AppSetup appSetup;
+ private DaoNodeService daoNodeService;
+ private GetInventoryRequestHandler getInventoryRequestHandler;
+
+ public DaoNode() {
+ }
+
+ public void startApplication(int restServerPort) {
+ appSetup = injector.getInstance(AppSetupWithP2PAndDAO.class);
+
+ // todo should run as full dao node when in production
+ injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false);
+
+ appSetup.start();
+
+ getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class);
+ DaoStateService daoStateService = injector.getInstance(DaoStateService.class);
+
+ daoNodeService = new DaoNodeService(daoStateService);
+ daoNodeService.start(restServerPort);
+ }
+
+ public void shutDown() {
+ getInventoryRequestHandler.shutDown();
+ daoNodeService.shutDown();
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/DaoNodeMain.java b/daonode/src/main/java/bisq/daonode/DaoNodeMain.java
new file mode 100644
index 0000000000..3c37e859a0
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/DaoNodeMain.java
@@ -0,0 +1,215 @@
+/*
+ * 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 .
+ */
+
+package bisq.daonode;
+
+
+import bisq.core.app.TorSetup;
+import bisq.core.app.misc.ExecutableForAppWithP2p;
+import bisq.core.app.misc.ModuleForAppWithP2p;
+import bisq.core.dao.state.DaoStateSnapshotService;
+import bisq.core.user.Cookie;
+import bisq.core.user.CookieKey;
+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.DevEnv;
+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.extern.slf4j.Slf4j;
+//todo not sure if the restart handling from seed nodes is required
+
+@Slf4j
+public class DaoNodeMain extends ExecutableForAppWithP2p {
+ private static final long CHECK_CONNECTION_LOSS_SEC = 30;
+ private static final int DEFAULT_REST_SERVER_PORT = 8080;
+ private static final String VERSION = "1.8.4";
+
+ private DaoNode daoNode;
+ private Timer checkConnectionLossTime;
+ private int restServerPort = DEFAULT_REST_SERVER_PORT;
+
+ public DaoNodeMain() {
+ super("Bisq Daonode", "bisq-daonode", "bisq_daonode", VERSION);
+ }
+
+ public static void main(String[] args) {
+ System.out.println("DaoNode.VERSION: " + VERSION);
+
+ new DaoNodeMain().execute(args);
+ }
+
+ @Override
+ protected void doExecute() {
+ super.doExecute();
+
+ checkMemory(config, this);
+
+ keepRunning();
+ }
+
+ @Override
+ protected void addCapabilities() {
+ }
+
+ @Override
+ protected void launchApplication() {
+ UserThread.execute(() -> {
+ try {
+ daoNode = new DaoNode();
+ onApplicationLaunched();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @Override
+ protected void onApplicationLaunched() {
+ super.onApplicationLaunched();
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // We continue with a series of synchronous execution tasks
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected AppModule getModule() {
+ return new ModuleForAppWithP2p(config);
+ }
+
+ @Override
+ protected void applyInjector() {
+ super.applyInjector();
+
+ daoNode.setInjector(injector);
+
+ if (DevEnv.isDaoActivated()) {
+ 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);
+ }
+ });
+
+ //todo add program arg for port
+ daoNode.startApplication(restServerPort);
+
+ 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(DaoNodeMain.this);
+ }
+ UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
+ }
+
+ @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);
+
+ }
+
+ private void gracefulShutDown() {
+ gracefulShutDown(() -> {
+ });
+ }
+
+ @Override
+ public void gracefulShutDown(ResultHandler resultHandler) {
+ daoNode.shutDown();
+ super.gracefulShutDown(resultHandler);
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java b/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java
new file mode 100644
index 0000000000..c0a8f1bdc0
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java
@@ -0,0 +1,41 @@
+/*
+ * 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 .
+ */
+
+package bisq.daonode.dto;
+
+import lombok.Getter;
+
+/**
+ * Minimal data required for Bisq 2 reputation use case.
+ * Need to be in sync with the Bisq 2 ProofOfBurnDto class.
+ */
+@Getter
+public class ProofOfBurnDto {
+ private String txId;
+ private final long burnedAmount;
+ private final int blockHeight;
+ private final long time;
+ private final String hash;
+
+ public ProofOfBurnDto(String txId, long burnedAmount, int blockHeight, long time, String hash) {
+ this.txId = txId;
+ this.burnedAmount = burnedAmount;
+ this.blockHeight = blockHeight;
+ this.time = time;
+ this.hash = hash;
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/service/DaoNodeService.java b/daonode/src/main/java/bisq/daonode/service/DaoNodeService.java
new file mode 100644
index 0000000000..f37fbb6c7e
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/service/DaoNodeService.java
@@ -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 .
+ */
+
+package bisq.daonode.service;
+
+import bisq.core.dao.state.DaoStateService;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+
+import bisq.daonode.web.WebServer;
+import bisq.daonode.web.jdk.JdkServer;
+
+// Todo We should limit usage to localhost as its not intended at that stage to be used
+// as a public API, but rather be used by Bisq 2 bridge clients or BSQ explorer nodes,
+// both running in a localhost environment. As long that holds, we do not require a high
+// level of protection against malicious usage.
+
+// TODO This JDK http server is a super simple implementation. We might use some other
+// lightweight REST server framework.
+@Slf4j
+public class DaoNodeService {
+ private WebServer webServer;
+ private DaoStateService daoStateService;
+
+ public DaoNodeService(DaoStateService daoStateService) {
+ this.daoStateService = daoStateService;
+ }
+
+ public void start(int port) {
+ try {
+ webServer = new JdkServer(port, daoStateService);
+ webServer.start();
+ } catch (Throwable t) {
+ log.error(t.toString());
+ }
+ }
+
+ public void shutDown() {
+ if (webServer != null) {
+ webServer.stop(0);
+ }
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/WebServer.java b/daonode/src/main/java/bisq/daonode/web/WebServer.java
new file mode 100644
index 0000000000..6173032b22
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/WebServer.java
@@ -0,0 +1,8 @@
+package bisq.daonode.web;
+
+public interface WebServer {
+
+ void start();
+
+ void stop(int delay);
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/JdkServer.java b/daonode/src/main/java/bisq/daonode/web/jdk/JdkServer.java
new file mode 100644
index 0000000000..901d5d6f4d
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/JdkServer.java
@@ -0,0 +1,132 @@
+package bisq.daonode.web.jdk;
+
+import bisq.core.dao.state.DaoStateService;
+
+import bisq.common.util.Utilities;
+
+import java.net.InetSocketAddress;
+
+import java.io.IOException;
+
+import java.util.concurrent.Executor;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.daonode.web.jdk.handler.ResourcePathElement.DAONODE;
+
+
+
+import bisq.daonode.web.WebServer;
+import bisq.daonode.web.jdk.handler.RestHandler;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+// https://dev.to/piczmar_0/framework-less-rest-api-in-java-1jbl
+
+// https://doc.networknt.com/getting-started/light-rest-4j
+// https://github.com/piczmar/pure-java-rest-api
+// https://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api
+// https://www.programcreek.com/java-api-examples/index.php?api=com.sun.net.httpserver.HttpServer
+
+/**
+ * From https://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api
+ *
+ * Note that this is, in contrary to what some developers think, absolutely not forbidden
+ * by the well known FAQ Why Developers Should Not Write Programs That Call 'sun' Packages.
+ * That FAQ concerns the sun.* package (such as sun.misc.BASE64Encoder) for internal usage
+ * by the Oracle JRE (which would thus kill your application when you run it on a different
+ * JRE), not the com.sun.* package. Sun/Oracle also just develop software on top of the
+ * Java SE API themselves like as every other company such as Apache and so on. Moreover,
+ * this specific HttpServer must be present in every JDK so there is absolutely no means
+ * of "portability" issue like as would happen with sun.* package. Using com.sun.* classes
+ * is only discouraged (but not forbidden) when it concerns an implementation of a certain
+ * Java API, such as GlassFish (Java EE impl), Mojarra (JSF impl), Jersey (JAX-RS impl), etc.
+ */
+@Slf4j
+public class JdkServer extends HttpServer implements WebServer {
+
+ public static void main(String[] args) throws InterruptedException {
+ WebServer webServer = new JdkServer(8080, null);
+ webServer.start();
+ Thread.sleep(40000);
+ webServer.stop(0);
+ }
+
+ private final int port;
+ private final DaoStateService daoStateService;
+
+ private HttpServer server;
+
+ public JdkServer(int port, DaoStateService daoStateService) {
+ this.port = port;
+ this.daoStateService = daoStateService;
+ configure();
+ }
+
+ private void configure() {
+ try {
+ this.server = HttpServer.create(new InetSocketAddress(port), 0);
+ // As use case is intended for a 1 client environment we can stick with a single thread.
+ setExecutor(Utilities.getSingleThreadExecutor("DaoNode-API"));
+ // Map all request URLs starting with "/daonode" to a single RestHandler.
+ // The RestHandler will pass valid requests on to an appropriate handler.
+ createContext("/" + DAONODE, new RestHandler(daoStateService));
+ } catch (IOException ex) {
+ log.error(ex.toString());
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public void bind(InetSocketAddress addr, int backlog) throws IOException {
+ server.bind(addr, backlog);
+ }
+
+ @Override
+ public void start() {
+ server.start();
+ }
+
+ @Override
+ public void setExecutor(Executor executor) {
+ server.setExecutor(executor);
+ }
+
+ @Override
+ public Executor getExecutor() {
+ return server.getExecutor();
+ }
+
+ @Override
+ public HttpContext createContext(String path, HttpHandler handler) {
+ return server.createContext(path, handler);
+ }
+
+ @Override
+ public HttpContext createContext(String path) {
+ return server.createContext(path);
+ }
+
+ @Override
+ public void removeContext(String path) throws IllegalArgumentException {
+ server.removeContext(path);
+ }
+
+ @Override
+ public void removeContext(HttpContext context) {
+ server.removeContext(context);
+ }
+
+ @Override
+ public InetSocketAddress getAddress() {
+ return server.getAddress();
+ }
+
+ @Override
+ public void stop(int delay) {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/handler/GetProofOfBurnHandler.java b/daonode/src/main/java/bisq/daonode/web/jdk/handler/GetProofOfBurnHandler.java
new file mode 100644
index 0000000000..b5c00be621
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/handler/GetProofOfBurnHandler.java
@@ -0,0 +1,99 @@
+package bisq.daonode.web.jdk.handler;
+
+import bisq.core.dao.state.DaoStateService;
+import bisq.core.dao.state.model.blockchain.Tx;
+
+import bisq.common.util.Hex;
+
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.daonode.web.jdk.handler.HandlerUtil.sendResponse;
+import static bisq.daonode.web.jdk.handler.HandlerUtil.toJson;
+import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapErrorResponse;
+import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapResponse;
+import static bisq.daonode.web.jdk.handler.ResourcePathElement.BLOCKHEIGHT;
+
+
+
+import bisq.daonode.dto.ProofOfBurnDto;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+
+/**
+ * Handles daonode/proofofburn requests. Request URLs must match:
+ * http://localhost:/daonode/proofofburn/blockheight/
+ *
+ * Example: http://localhost:8080/daonode/proofofburn/blockheight/731270
+ */
+@Slf4j
+class GetProofOfBurnHandler implements HttpHandler {
+
+ private final DaoStateService daoStateService;
+ private final RequestSpec requestSpec;
+
+ /**
+ * A new handler instance must be used for each request. We do not want to parse
+ * details from each request URI more than once; they are passed to this constructor
+ * from the RestHandler via the RequestSpec argument.
+ *
+ * @param daoStateService DaoStateService singleton
+ * @param requestSpec RESTful request details, including parsed URL parameters
+ */
+ public GetProofOfBurnHandler(DaoStateService daoStateService, RequestSpec requestSpec) {
+ this.daoStateService = daoStateService;
+ this.requestSpec = requestSpec;
+ }
+
+ @Override
+ public void handle(HttpExchange httpExchange) throws IOException {
+ try {
+ if (daoStateService == null) {
+ log.warn("DAO Node daoStateService is null; OK during web server dev/test.");
+ sendResponse(httpExchange, wrapResponse("[]"));
+ } else {
+ int blockHeight = requestSpec.getIntParam(BLOCKHEIGHT);
+ log.info("Requesting POB for blockheight {}.", blockHeight);
+ List data = getProofOfBurnDtoList(blockHeight);
+ if (data != null) {
+ sendResponse(httpExchange, wrapResponse(toJson(data)));
+ } else {
+ log.error("DAO Node Proof of Burn data for blockHeight {} is null.", blockHeight);
+ sendResponse(500,
+ httpExchange,
+ wrapErrorResponse(toJson("DAO Node proof of burn data is null.")));
+ }
+ }
+ } catch (RuntimeException ex) {
+ sendResponse(500,
+ httpExchange,
+ wrapErrorResponse(toJson(ex.getMessage())));
+ }
+ }
+
+ private List getProofOfBurnDtoList(int fromBlockHeight) {
+ return daoStateService.getProofOfBurnTxs().stream()
+ .filter(tx -> tx.getBlockHeight() >= fromBlockHeight)
+ .map(tx -> new ProofOfBurnDto(tx.getId(),
+ tx.getBurntBsq(),
+ tx.getBlockHeight(),
+ tx.getTime(),
+ getHash(tx)))
+ .collect(Collectors.toList());
+ }
+
+ // We strip out the version bytes
+ private String getHash(Tx tx) {
+ byte[] opReturnData = tx.getLastTxOutput().getOpReturnData();
+ if (opReturnData == null) {
+ return "";
+ }
+ return Hex.encode(Arrays.copyOfRange(opReturnData, 2, 22));
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/handler/HandlerUtil.java b/daonode/src/main/java/bisq/daonode/web/jdk/handler/HandlerUtil.java
new file mode 100644
index 0000000000..8b60ecb987
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/handler/HandlerUtil.java
@@ -0,0 +1,51 @@
+package bisq.daonode.web.jdk.handler;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import lombok.SneakyThrows;
+
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+
+
+import com.sun.net.httpserver.HttpExchange;
+
+class HandlerUtil {
+
+ static void setDefaultResponseHeaders(HttpExchange httpExchange) {
+ httpExchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
+ }
+
+ static void sendResponse(HttpExchange httpExchange, String response) throws IOException {
+ sendResponse(200, httpExchange, response);
+ }
+
+ static void sendResponse(int status, HttpExchange httpExchange, String response) throws IOException {
+ setDefaultResponseHeaders(httpExchange);
+
+ byte[] responseBytes = response.getBytes(UTF_8);
+ httpExchange.sendResponseHeaders(status, responseBytes.length);
+ OutputStream os = httpExchange.getResponseBody();
+ os.write(responseBytes);
+ os.close();
+ }
+
+ // TODO make as function toWhat?
+ static String wrapResponse(String jsonData) {
+ return format("{\"data\":%s}", jsonData);
+ }
+
+ // TODO make as function toErrorWhat?
+ static String wrapErrorResponse(String jsonError) {
+ return format("{\"error\":%s}", jsonError);
+ }
+
+ @SneakyThrows
+ static String toJson(Object object) {
+ return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(object);
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/handler/RequestSpec.java b/daonode/src/main/java/bisq/daonode/web/jdk/handler/RequestSpec.java
new file mode 100644
index 0000000000..c96ce16319
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/handler/RequestSpec.java
@@ -0,0 +1,117 @@
+package bisq.daonode.web.jdk.handler;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import static java.lang.String.format;
+import static java.lang.System.arraycopy;
+
+
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * REST request URI parser to extract parameter names and values from an HttpExchange.
+ *
+ * Splits the HttpExchange's request URI into a String[] of pathElements identifying a
+ * (REST) service name, a resource name, and any parameter/value pairs.
+ *
+ * This class is limited to URIs adhering a specific pattern:
+ * pathElements[0] = service-name, e.g., daonode
+ * pathElements[1] = resource-name, e.g., proofofburn
+ * pathElements[2, 3...N, N+1] = param-name/value pairs.
+ *
+ * For example, request URL http://localhost:8080/daonode/proofofburn/blockheight/731270
+ * identifies service-name "daonode", resource-name "proofofburn", and one parameter
+ * "blockheight" with value 731270.
+ */
+@Getter
+@Slf4j
+class RequestSpec {
+
+ private final HttpExchange httpExchange;
+ private final String method;
+ private final URI requestURI;
+ private final String[] pathElements;
+ private final String serviceName;
+ private final String resourceName;
+ private final Map parametersByName;
+
+ public RequestSpec(HttpExchange httpExchange) {
+ this.httpExchange = httpExchange;
+ this.method = httpExchange.getRequestMethod();
+ this.requestURI = httpExchange.getRequestURI();
+ this.pathElements = toPathElements.apply(requestURI);
+ this.serviceName = pathElements[0];
+ this.resourceName = pathElements[1];
+ try {
+ this.parametersByName = getParametersByName();
+ } catch (URISyntaxException ex) {
+ // OK to throw ex in this constructor?
+ log.error(ex.toString());
+ throw new IllegalArgumentException(ex.toString());
+ }
+ }
+
+ public boolean isRequestingResource(String resourceName) {
+ return this.resourceName.equalsIgnoreCase(resourceName);
+ }
+
+ public String getStringParam(String paramName) {
+ if (parametersByName.containsKey(paramName))
+ return parametersByName.get(paramName);
+ else
+ throw new IllegalArgumentException(format("Parameter '%s' not found.", paramName));
+ }
+
+ public int getIntParam(String paramName) {
+ if (parametersByName.containsKey(paramName)) {
+ var value = parametersByName.get(paramName);
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException(
+ format("Parameter '%s' value '%s' is not a number.",
+ paramName,
+ value));
+ }
+ } else {
+ throw new IllegalArgumentException(format("Parameter '%s' not found.", paramName));
+ }
+ }
+
+ private final Function toPathElements = (uri) -> {
+ String[] raw = uri.getPath().split("/");
+ String[] elements = new String[raw.length - 1];
+ arraycopy(raw, 1, elements, 0, elements.length);
+ return elements;
+ };
+
+ private Map getParametersByName() throws URISyntaxException {
+ Map params = new HashMap<>();
+ if (pathElements.length == 2)
+ return params; // There are no parameter name/value pairs in url.
+
+ // All pathElements beyond index 1 should be param-name/value pairs, and
+ // a param-value must follow each param-name.
+ Predicate paramValueExists = (i) -> (i + 1) < pathElements.length;
+ for (int i = 2; i < pathElements.length; i++) {
+ String name = pathElements[i];
+ if (paramValueExists.test(i))
+ params.put(name, pathElements[++i]);
+ else
+ throw new URISyntaxException(requestURI.getPath(),
+ format("No value found for parameter with name '%s'.", name),
+ -1);
+ }
+ return params;
+ }
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/handler/ResourcePathElement.java b/daonode/src/main/java/bisq/daonode/web/jdk/handler/ResourcePathElement.java
new file mode 100644
index 0000000000..8413acd071
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/handler/ResourcePathElement.java
@@ -0,0 +1,7 @@
+package bisq.daonode.web.jdk.handler;
+
+public class ResourcePathElement {
+ public static String DAONODE = "daonode";
+ public static final String BLOCKHEIGHT = "blockheight";
+ public static final String PROOFOFBURN = "proofofburn";
+}
diff --git a/daonode/src/main/java/bisq/daonode/web/jdk/handler/RestHandler.java b/daonode/src/main/java/bisq/daonode/web/jdk/handler/RestHandler.java
new file mode 100644
index 0000000000..c11701bc39
--- /dev/null
+++ b/daonode/src/main/java/bisq/daonode/web/jdk/handler/RestHandler.java
@@ -0,0 +1,61 @@
+package bisq.daonode.web.jdk.handler;
+
+import bisq.core.dao.state.DaoStateService;
+
+import java.io.IOException;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.daonode.web.jdk.handler.HandlerUtil.sendResponse;
+import static bisq.daonode.web.jdk.handler.HandlerUtil.toJson;
+import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapErrorResponse;
+import static bisq.daonode.web.jdk.handler.ResourcePathElement.PROOFOFBURN;
+
+
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * All HTTP requests are routed to a singleton RestHandler, then passed on to
+ * appropriate sub handler instances.
+ */
+@Slf4j
+public class RestHandler implements HttpHandler {
+
+ private final DaoStateService daoStateService;
+
+ public RestHandler(DaoStateService daoStateService) {
+ this.daoStateService = daoStateService;
+ }
+
+ @Override
+ public void handle(HttpExchange httpExchange) throws IOException {
+ try {
+ if (httpExchange == null)
+ throw new NullPointerException("HttpExchange cannot be null.");
+
+ // Parse the request URI details here, and pass them to a new
+ // sub handler instance.
+ RequestSpec requestSpec = new RequestSpec(httpExchange);
+
+ // Atm we only use GET, no plans yet for allow POST for DaoNode API.
+ if (!requestSpec.getMethod().equals("GET")) {
+ sendResponse(405,
+ httpExchange,
+ wrapErrorResponse(toJson("Forbidden HTTP method " + requestSpec.getMethod())));
+ } else if (requestSpec.isRequestingResource(PROOFOFBURN)) {
+ new GetProofOfBurnHandler(daoStateService, requestSpec).handle(httpExchange);
+ } else {
+ sendResponse(404, httpExchange, wrapErrorResponse(toJson("Not Found")));
+ }
+
+ } catch (RuntimeException ex) {
+ sendResponse(500,
+ httpExchange,
+ wrapErrorResponse(toJson(ex.getMessage())));
+ } finally {
+ httpExchange.close();
+ }
+ }
+}
diff --git a/daonode/src/main/resources/logback.xml b/daonode/src/main/resources/logback.xml
new file mode 100644
index 0000000000..914b9d3d4b
--- /dev/null
+++ b/daonode/src/main/resources/logback.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)
+
+
+
+
+
+
+
+
+
+
diff --git a/daonode/src/test/resources/logback.xml b/daonode/src/test/resources/logback.xml
new file mode 100644
index 0000000000..28279faa11
--- /dev/null
+++ b/daonode/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
index 38fb1df208..e41d50899a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,3 +3,6 @@ systemProp.org.gradle.internal.http.socketTimeout=120000
# Makes Y/N user prompts more readable during installer packaging
org.gradle.console=plain
+
+# org.gradle.dependency.verification.console=verbose
+org.gradle.dependency.verification=lenient
diff --git a/settings.gradle b/settings.gradle
index 1827ef9c4f..2c3dbef662 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -11,6 +11,7 @@ include 'pricenode'
include 'relay'
include 'seednode'
include 'statsnode'
+include 'daonode'
include 'inventory'
include 'apitest'