Merge pull request #6228 from chimp1984/use-bisq2-rest-infrastructure-for-daonode

Use bisq2 rest infrastructure for daonode
This commit is contained in:
Christoph Atteneder 2022-06-20 20:38:06 +02:00 committed by GitHub
commit b794312184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 11947 additions and 891 deletions

View File

@ -54,6 +54,7 @@ configure(subprojects) {
javafxVersion = '16' javafxVersion = '16'
javaxAnnotationVersion = '1.2' javaxAnnotationVersion = '1.2'
jcsvVersion = '1.4.0' jcsvVersion = '1.4.0'
jerseyVersion = '3.0.4'
jetbrainsAnnotationsVersion = '13.0' jetbrainsAnnotationsVersion = '13.0'
jfoenixVersion = '9.0.10' jfoenixVersion = '9.0.10'
joptVersion = '5.0.4' joptVersion = '5.0.4'
@ -76,6 +77,7 @@ configure(subprojects) {
slf4jVersion = '1.7.30' slf4jVersion = '1.7.30'
sparkVersion = '2.5.2' sparkVersion = '2.5.2'
springBootVersion = '2.5.6' springBootVersion = '2.5.6'
swaggerVersion = '2.2.0'
os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
} }
@ -691,7 +693,7 @@ configure(project(':seednode')) {
configure(project(':daonode')) { configure(project(':daonode')) {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
mainClassName = 'bisq.daonode.DaoNodeMain' mainClassName = 'bisq.daoNode.DaoNodeRestApiApplication'
dependencies { dependencies {
implementation project(':common') implementation project(':common')
@ -709,11 +711,17 @@ configure(project(':daonode')) {
implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") /* implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
implementation "com.google.protobuf:protobuf-java-util:$protobufVersion" implementation "com.google.protobuf:protobuf-java-util:$protobufVersion"*/
implementation("org.glassfish.jersey.containers:jersey-container-jdk-http:$jerseyVersion")
implementation("org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion")
implementation("org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion")
implementation("org.glassfish.jaxb:jaxb-runtime:3.0.2")
implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion")
testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion"

View File

@ -128,6 +128,8 @@ public class Config {
public static final String BTC_FEES_TS = "bitcoinFeesTs"; public static final String BTC_FEES_TS = "bitcoinFeesTs";
public static final String BTC_FEE_INFO = "bitcoinFeeInfo"; public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String DAO_NODE_API_URL = "daoNodeApiUrl";
public static final String DAO_NODE_API_PORT = "daoNodeApiPort";
// Default values for certain options // Default values for certain options
public static final int UNSPECIFIED_PORT = -1; public static final int UNSPECIFIED_PORT = -1;
@ -218,6 +220,8 @@ public class Config {
public final boolean preventPeriodicShutdownAtSeedNode; public final boolean preventPeriodicShutdownAtSeedNode;
public final boolean republishMailboxEntries; public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation; public final boolean bypassMempoolValidation;
public final String daoNodeApiUrl;
public final int daoNodeApiPort;
// Properties derived from options but not exposed as options themselves // Properties derived from options but not exposed as options themselves
public final File torDir; public final File torDir;
@ -673,6 +677,17 @@ public class Config {
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<String> daoNodeApiUrlOpt =
parser.accepts(DAO_NODE_API_URL, "Dao node API url")
.withRequiredArg()
.defaultsTo("http://localhost");
ArgumentAcceptingOptionSpec<Integer> daoNodeApiPortOpt =
parser.accepts(DAO_NODE_API_PORT, "Dao node API port")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(8082);
try { try {
CompositeOptionSet options = new CompositeOptionSet(); CompositeOptionSet options = new CompositeOptionSet();
@ -791,6 +806,8 @@ public class Config {
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt); this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
this.daoNodeApiPort = options.valueOf(daoNodeApiPortOpt);
} catch (OptionException ex) { } catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s", throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0), ex.options().get(0),

View File

@ -17,49 +17,184 @@
package bisq.daonode; package bisq.daonode;
import bisq.core.app.misc.AppSetup;
import bisq.core.app.TorSetup;
import bisq.core.app.misc.AppSetupWithP2PAndDAO; import bisq.core.app.misc.AppSetupWithP2PAndDAO;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.DaoStateService;
import bisq.core.network.p2p.inventory.GetInventoryRequestHandler; 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.Preferences;
import bisq.core.user.User;
import com.google.inject.Injector; import bisq.network.p2p.P2PService;
import bisq.network.p2p.P2PServiceListener;
import bisq.network.p2p.peers.PeerManager;
import lombok.Setter; 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; import lombok.extern.slf4j.Slf4j;
//todo not sure if the restart handling from seed nodes is required
import bisq.daonode.service.DaoNodeService;
@Slf4j @Slf4j
public class DaoNode { public class DaoNode extends ExecutableForAppWithP2p {
@Setter private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private Injector injector;
private AppSetup appSetup; private Timer checkConnectionLossTime;
private DaoNodeService daoNodeService; @Getter
private GetInventoryRequestHandler getInventoryRequestHandler; private DaoStateService daoStateService;
public DaoNode() { public DaoNode() {
super("Bisq Dao Node", "bisq-dao-node", "bisq_dao_node", Version.VERSION);
} }
public void startApplication(int restServerPort) { public Config getConfig() {
appSetup = injector.getInstance(AppSetupWithP2PAndDAO.class); 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);
}
});
// todo should run as full dao node when in production
injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false); injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false);
injector.getInstance(AppSetupWithP2PAndDAO.class).start();
appSetup.start(); daoStateService = injector.getInstance(DaoStateService.class);
getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class); injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
DaoStateService daoStateService = injector.getInstance(DaoStateService.class); @Override
public void onDataReceived() {
daoNodeService = new DaoNodeService(daoStateService); // Do nothing
daoNodeService.start(restServerPort);
} }
public void shutDown() { @Override
getInventoryRequestHandler.shutDown(); public void onNoSeedNodeAvailable() {
daoNodeService.shutDown(); // 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(DaoNode.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) {
super.gracefulShutDown(resultHandler);
} }
} }

View File

@ -1,215 +0,0 @@
/*
* 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);
}
}

View File

@ -0,0 +1,121 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode;
import bisq.common.config.Config;
import java.net.URI;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import bisq.daonode.endpoints.ProofOfBurnApi;
import bisq.daonode.error.CustomExceptionMapper;
import bisq.daonode.error.StatusException;
import bisq.daonode.util.StaticFileHandler;
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 DaoNodeRestApiApplication extends ResourceConfig {
@Getter
private static String baseUrl;
public static void main(String[] args) throws Exception {
DaoNodeRestApiApplication daoNodeRestApiApplication = new DaoNodeRestApiApplication();
daoNodeRestApiApplication.startDaoNode(args, config -> {
daoNodeRestApiApplication
.register(CustomExceptionMapper.class)
.register(StatusException.StatusExceptionMapper.class)
.register(ProofOfBurnApi.class)
.register(SwaggerResolution.class);
daoNodeRestApiApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort);
});
}
@Getter
private final DaoNode daoNode;
private HttpServer httpServer;
public DaoNodeRestApiApplication() {
daoNode = new DaoNode();
}
private void startDaoNode(String[] args, Consumer<Config> configConsumer) {
new Thread(() -> {
daoNode.execute(args);
configConsumer.accept(daoNode.getConfig());
try {
// Keep running
Thread.currentThread().setName("daoNodeThread");
Thread.currentThread().join();
} catch (InterruptedException e) {
log.error("daoNodeThread interrupted", e);
e.printStackTrace();
shutDown();
}
}).start();
}
private void startServer(String url, int port) {
baseUrl = url + ":" + port + "/api/v1";
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);
// 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() {
if (daoNode != null) {
daoNode.gracefulShutDown(this::stopServer);
} else {
stopServer();
}
}
private void stopServer() {
if (httpServer != null) {
httpServer.stop(1);
}
}
}

View File

@ -0,0 +1,71 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode;
import lombok.extern.slf4j.Slf4j;
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(DaoNodeRestApiApplication.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

@ -19,13 +19,18 @@ package bisq.daonode.dto;
import lombok.Getter; import lombok.Getter;
import io.swagger.v3.oas.annotations.media.Schema;
/** /**
* Minimal data required for Bisq 2 reputation use case. * Minimal data required for Bisq 2 reputation use case.
* Need to be in sync with the Bisq 2 ProofOfBurnDto class. * Need to be in sync with the Bisq 2 ProofOfBurnDto class.
*/ */
@Getter @Getter
@Schema(title = "ProofOfBurn")
public class ProofOfBurnDto { public class ProofOfBurnDto {
private String txId; private final String txId;
private final long burnedAmount; private final long burnedAmount;
private final int blockHeight; private final int blockHeight;
private final long time; private final long time;

View File

@ -0,0 +1,91 @@
/*
* 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.endpoints;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.common.util.Hex;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.daonode.DaoNode;
import bisq.daonode.DaoNodeRestApiApplication;
import bisq.daonode.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;
@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 DaoNode daoNode;
public ProofOfBurnApi(@Context Application application) {
daoNode = ((DaoNodeRestApiApplication) application).getDaoNode();
}
@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 blockHeight) {
return checkNotNull(daoNode.getDaoStateService()).getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() >= blockHeight)
.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));
}
}

View File

@ -15,10 +15,24 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>. * along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/ */
package bisq.daonode.web.jdk.handler; package bisq.daonode.error;
public class ResourcePathElement { import lombok.extern.slf4j.Slf4j;
public static String DAONODE = "daonode";
public static final String BLOCKHEIGHT = "blockheight";
public static final String PROOFOFBURN = "proofofburn";
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);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorMessage(exception.getMessage()))
.build();
}
} }

View File

@ -15,11 +15,16 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>. * along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/ */
package bisq.daonode.web; package bisq.daonode.error;
public interface WebServer { import lombok.Getter;
void start(); @Getter
public class ErrorMessage {
private final String error;
void stop(int delay); 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.daonode.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);
return Response.status(exception.getHttpStatus())
.entity(new ErrorMessage(exception.getMessage()))
.build();
}
}
}

View File

@ -1,59 +0,0 @@
/*
* 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);
}
}
}

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.daonode.util;
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

@ -1,149 +0,0 @@
/*
* 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.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);
}
}
}

View File

@ -1,116 +0,0 @@
/*
* 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.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));
}
}

View File

@ -1,68 +0,0 @@
/*
* 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.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);
}
}

View File

@ -1,134 +0,0 @@
/*
* 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.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;
}
}

View File

@ -1,78 +0,0 @@
/*
* 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.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 static java.util.Objects.requireNonNull;
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 {
requireNonNull(httpExchange, "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();
}
}
}

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

@ -23,3 +23,7 @@ The second execution should then update:
- `bisq/gradlew.bat` - `bisq/gradlew.bat`
The four updated files are ready to be committed. The four updated files are ready to be committed.
To update the verification-metadata file run:
- `./gradlew --write-verification-metadata sha256`