mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
2nd attempt at a tiny daonode rest service
Based on chimp1984's https://github.com/bisq-network/bisq/pull/6184
This commit is contained in:
parent
cee00f5b9c
commit
c70810edd7
44
build.gradle
44
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'
|
||||
|
65
daonode/src/main/java/bisq/daonode/DaoNode.java
Normal file
65
daonode/src/main/java/bisq/daonode/DaoNode.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
215
daonode/src/main/java/bisq/daonode/DaoNodeMain.java
Normal file
215
daonode/src/main/java/bisq/daonode/DaoNodeMain.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
41
daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java
Normal file
41
daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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.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);
|
||||
}
|
||||
}
|
||||
}
|
8
daonode/src/main/java/bisq/daonode/web/WebServer.java
Normal file
8
daonode/src/main/java/bisq/daonode/web/WebServer.java
Normal file
@ -0,0 +1,8 @@
|
||||
package bisq.daonode.web;
|
||||
|
||||
public interface WebServer {
|
||||
|
||||
void start();
|
||||
|
||||
void stop(int delay);
|
||||
}
|
132
daonode/src/main/java/bisq/daonode/web/jdk/JdkServer.java
Normal file
132
daonode/src/main/java/bisq/daonode/web/jdk/JdkServer.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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:<port>/daonode/proofofburn/blockheight/<blockheight-value>
|
||||
*
|
||||
* 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<ProofOfBurnDto> 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<ProofOfBurnDto> 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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<String, String> 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<URI, String[]> 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<String, String> getParametersByName() throws URISyntaxException {
|
||||
Map<String, String> 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<Integer> 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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
15
daonode/src/main/resources/logback.xml
Normal file
15
daonode/src/main/resources/logback.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="TRACE">
|
||||
<appender-ref ref="CONSOLE_APPENDER"/>
|
||||
</root>
|
||||
|
||||
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
|
||||
|
||||
</configuration>
|
20
daonode/src/test/resources/logback.xml
Normal file
20
daonode/src/test/resources/logback.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!--
|
||||
The :daemon & :cli jars contain their own logback.xml config files, which causes chatty logback startup.
|
||||
To avoid chatty logback msgs during its configuration, pass logback.configurationFile as a system property:
|
||||
-Dlogback.configurationFile=apitest/build/resources/main/logback.xml
|
||||
The gradle build file takes care of adding this system property to the bisq-apitest script.
|
||||
-->
|
||||
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE_APPENDER"/>
|
||||
</root>
|
||||
|
||||
<logger name="io.grpc.netty" level="WARN"/>
|
||||
</configuration>
|
@ -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
|
||||
|
@ -11,6 +11,7 @@ include 'pricenode'
|
||||
include 'relay'
|
||||
include 'seednode'
|
||||
include 'statsnode'
|
||||
include 'daonode'
|
||||
include 'inventory'
|
||||
include 'apitest'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user