mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge pull request #6228 from chimp1984/use-bisq2-rest-infrastructure-for-daonode
Use bisq2 rest infrastructure for daonode
This commit is contained in:
commit
b794312184
18
build.gradle
18
build.gradle
@ -54,6 +54,7 @@ configure(subprojects) {
|
||||
javafxVersion = '16'
|
||||
javaxAnnotationVersion = '1.2'
|
||||
jcsvVersion = '1.4.0'
|
||||
jerseyVersion = '3.0.4'
|
||||
jetbrainsAnnotationsVersion = '13.0'
|
||||
jfoenixVersion = '9.0.10'
|
||||
joptVersion = '5.0.4'
|
||||
@ -76,6 +77,7 @@ configure(subprojects) {
|
||||
slf4jVersion = '1.7.30'
|
||||
sparkVersion = '2.5.2'
|
||||
springBootVersion = '2.5.6'
|
||||
swaggerVersion = '2.2.0'
|
||||
|
||||
os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
|
||||
}
|
||||
@ -691,7 +693,7 @@ configure(project(':seednode')) {
|
||||
configure(project(':daonode')) {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
mainClassName = 'bisq.daonode.DaoNodeMain'
|
||||
mainClassName = 'bisq.daoNode.DaoNodeRestApiApplication'
|
||||
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
@ -709,11 +711,17 @@ configure(project(':daonode')) {
|
||||
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.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"
|
||||
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"
|
||||
|
||||
|
@ -128,6 +128,8 @@ public class Config {
|
||||
public static final String BTC_FEES_TS = "bitcoinFeesTs";
|
||||
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
|
||||
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
|
||||
public static final int UNSPECIFIED_PORT = -1;
|
||||
@ -218,6 +220,8 @@ public class Config {
|
||||
public final boolean preventPeriodicShutdownAtSeedNode;
|
||||
public final boolean republishMailboxEntries;
|
||||
public final boolean bypassMempoolValidation;
|
||||
public final String daoNodeApiUrl;
|
||||
public final int daoNodeApiPort;
|
||||
|
||||
// Properties derived from options but not exposed as options themselves
|
||||
public final File torDir;
|
||||
@ -273,7 +277,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> configFileOpt =
|
||||
parser.accepts(CONFIG_FILE, format("Specify configuration file. " +
|
||||
"Relative paths will be prefixed by %s location.", APP_DATA_DIR))
|
||||
"Relative paths will be prefixed by %s location.", APP_DATA_DIR))
|
||||
.withRequiredArg()
|
||||
.ofType(String.class)
|
||||
.defaultsTo(DEFAULT_CONFIG_FILE_NAME);
|
||||
@ -346,7 +350,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> ignoreLocalBtcNodeOpt =
|
||||
parser.accepts(IGNORE_LOCAL_BTC_NODE,
|
||||
"If set to true a Bitcoin Core node running locally will be ignored")
|
||||
"If set to true a Bitcoin Core node running locally will be ignored")
|
||||
.withRequiredArg()
|
||||
.ofType(Boolean.class)
|
||||
.defaultsTo(false);
|
||||
@ -366,21 +370,21 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> useDevModeOpt =
|
||||
parser.accepts(USE_DEV_MODE,
|
||||
"Enables dev mode which is used for convenience for developer testing")
|
||||
"Enables dev mode which is used for convenience for developer testing")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> useDevModeHeaderOpt =
|
||||
parser.accepts(USE_DEV_MODE_HEADER,
|
||||
"Use dev mode css scheme to distinguish dev instances.")
|
||||
"Use dev mode css scheme to distinguish dev instances.")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> useDevPrivilegeKeysOpt =
|
||||
parser.accepts(USE_DEV_PRIVILEGE_KEYS, "If set to true all privileged features requiring a private " +
|
||||
"key to be enabled are overridden by a dev key pair (This is for developers only!)")
|
||||
"key to be enabled are overridden by a dev key pair (This is for developers only!)")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
@ -393,9 +397,9 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> ignoreDevMsgOpt =
|
||||
parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " +
|
||||
"network_messages from bisq developers are ignored (Global " +
|
||||
"alert, Version update alert, Filters for offers, nodes or " +
|
||||
"trading account data)")
|
||||
"network_messages from bisq developers are ignored (Global " +
|
||||
"alert, Version update alert, Filters for offers, nodes or " +
|
||||
"trading account data)")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
@ -408,7 +412,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> seedNodesOpt =
|
||||
parser.accepts(SEED_NODES, "Override hard coded seed nodes as comma separated list e.g. " +
|
||||
"'rxdkppp3vicnbgqt.onion:8002,mfla72c4igh5ta2t.onion:8002'")
|
||||
"'rxdkppp3vicnbgqt.onion:8002,mfla72c4igh5ta2t.onion:8002'")
|
||||
.withRequiredArg()
|
||||
.withValuesSeparatedBy(',')
|
||||
.describedAs("host:port[,...]");
|
||||
@ -440,29 +444,29 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> socks5ProxyHttpAddressOpt =
|
||||
parser.accepts(SOCKS_5_PROXY_HTTP_ADDRESS,
|
||||
"A proxy address to be used for Http requests (should be non-Tor)")
|
||||
"A proxy address to be used for Http requests (should be non-Tor)")
|
||||
.withRequiredArg()
|
||||
.describedAs("host:port")
|
||||
.defaultsTo("");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Path> torrcFileOpt =
|
||||
parser.accepts(TORRC_FILE, "An existing torrc-file to be sourced for Tor. Note that torrc-entries, " +
|
||||
"which are critical to Bisq's correct operation, cannot be overwritten.")
|
||||
"which are critical to Bisq's correct operation, cannot be overwritten.")
|
||||
.withRequiredArg()
|
||||
.describedAs("File")
|
||||
.withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE));
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> torrcOptionsOpt =
|
||||
parser.accepts(TORRC_OPTIONS, "A list of torrc-entries to amend to Bisq's torrc. Note that " +
|
||||
"torrc-entries, which are critical to Bisq's flawless operation, cannot be overwritten. " +
|
||||
"[torrc options line, torrc option, ...]")
|
||||
"torrc-entries, which are critical to Bisq's flawless operation, cannot be overwritten. " +
|
||||
"[torrc options line, torrc option, ...]")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$"))
|
||||
.defaultsTo("");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Integer> torControlPortOpt =
|
||||
parser.accepts(TOR_CONTROL_PORT,
|
||||
"The control port of an already running Tor service to be used by Bisq.")
|
||||
"The control port of an already running Tor service to be used by Bisq.")
|
||||
.availableUnless(TORRC_FILE, TORRC_OPTIONS)
|
||||
.withRequiredArg()
|
||||
.ofType(int.class)
|
||||
@ -477,7 +481,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Path> torControlCookieFileOpt =
|
||||
parser.accepts(TOR_CONTROL_COOKIE_FILE, "The cookie file for authenticating against the already " +
|
||||
"running Tor service. Use in conjunction with --" + TOR_CONTROL_USE_SAFE_COOKIE_AUTH)
|
||||
"running Tor service. Use in conjunction with --" + TOR_CONTROL_USE_SAFE_COOKIE_AUTH)
|
||||
.availableIf(TOR_CONTROL_PORT)
|
||||
.availableUnless(TOR_CONTROL_PASSWORD)
|
||||
.withRequiredArg()
|
||||
@ -486,7 +490,7 @@ public class Config {
|
||||
|
||||
OptionSpecBuilder torControlUseSafeCookieAuthOpt =
|
||||
parser.accepts(TOR_CONTROL_USE_SAFE_COOKIE_AUTH,
|
||||
"Use the SafeCookie method when authenticating to the already running Tor service.")
|
||||
"Use the SafeCookie method when authenticating to the already running Tor service.")
|
||||
.availableIf(TOR_CONTROL_COOKIE_FILE);
|
||||
|
||||
OptionSpecBuilder torStreamIsolationOpt =
|
||||
@ -532,21 +536,21 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> socks5DiscoverModeOpt =
|
||||
parser.accepts(SOCKS5_DISCOVER_MODE, "Specify discovery mode for Bitcoin nodes. " +
|
||||
"One or more of: [ADDR, DNS, ONION, ALL] (comma separated, they get OR'd together).")
|
||||
"One or more of: [ADDR, DNS, ONION, ALL] (comma separated, they get OR'd together).")
|
||||
.withRequiredArg()
|
||||
.describedAs("mode[,...]")
|
||||
.defaultsTo("ALL");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> useAllProvidedNodesOpt =
|
||||
parser.accepts(USE_ALL_PROVIDED_NODES,
|
||||
"Set to true if connection of bitcoin nodes should include clear net nodes")
|
||||
"Set to true if connection of bitcoin nodes should include clear net nodes")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> userAgentOpt =
|
||||
parser.accepts(USER_AGENT,
|
||||
"User agent at btc node connections")
|
||||
"User agent at btc node connections")
|
||||
.withRequiredArg()
|
||||
.defaultsTo("Bisq");
|
||||
|
||||
@ -585,21 +589,21 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> rpcBlockNotificationHostOpt =
|
||||
parser.accepts(RPC_BLOCK_NOTIFICATION_HOST,
|
||||
"Bitcoind rpc accepted incoming host for block notifications")
|
||||
"Bitcoind rpc accepted incoming host for block notifications")
|
||||
.withRequiredArg()
|
||||
.defaultsTo("");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> dumpBlockchainDataOpt =
|
||||
parser.accepts(DUMP_BLOCKCHAIN_DATA, "If set to true the blockchain data " +
|
||||
"from RPC requests to Bitcoin Core are stored as json file in the data dir.")
|
||||
"from RPC requests to Bitcoin Core are stored as json file in the data dir.")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> fullDaoNodeOpt =
|
||||
parser.accepts(FULL_DAO_NODE, "If set to true the node requests the blockchain data via RPC requests " +
|
||||
"from Bitcoin Core and provide the validated BSQ txs to the network. It requires that the " +
|
||||
"other RPC properties are set as well.")
|
||||
"from Bitcoin Core and provide the validated BSQ txs to the network. It requires that the " +
|
||||
"other RPC properties are set as well.")
|
||||
.withRequiredArg()
|
||||
.ofType(Boolean.class)
|
||||
.defaultsTo(DEFAULT_FULL_DAO_NODE);
|
||||
@ -611,7 +615,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Integer> genesisBlockHeightOpt =
|
||||
parser.accepts(GENESIS_BLOCK_HEIGHT,
|
||||
"Genesis transaction block height when not using the hard coded one")
|
||||
"Genesis transaction block height when not using the hard coded one")
|
||||
.withRequiredArg()
|
||||
.ofType(int.class)
|
||||
.defaultsTo(-1);
|
||||
@ -636,7 +640,7 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> allowFaultyDelayedTxsOpt =
|
||||
parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " +
|
||||
"payout transactions")
|
||||
"payout transactions")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
@ -654,25 +658,36 @@ public class Config {
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> preventPeriodicShutdownAtSeedNodeOpt =
|
||||
parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE,
|
||||
"Prevents periodic shutdown at seed nodes")
|
||||
"Prevents periodic shutdown at seed nodes")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> republishMailboxEntriesOpt =
|
||||
parser.accepts(REPUBLISH_MAILBOX_ENTRIES,
|
||||
"Republish mailbox messages at startup")
|
||||
"Republish mailbox messages at startup")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> bypassMempoolValidationOpt =
|
||||
parser.accepts(BYPASS_MEMPOOL_VALIDATION,
|
||||
"Prevents mempool check of trade parameters")
|
||||
"Prevents mempool check of trade parameters")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.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 {
|
||||
CompositeOptionSet options = new CompositeOptionSet();
|
||||
|
||||
@ -791,6 +806,8 @@ public class Config {
|
||||
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
|
||||
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
|
||||
this.daoNodeApiPort = options.valueOf(daoNodeApiPortOpt);
|
||||
} catch (OptionException ex) {
|
||||
throw new ConfigException("problem parsing option '%s': %s",
|
||||
ex.options().get(0),
|
||||
|
@ -17,49 +17,184 @@
|
||||
|
||||
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.ExecutableForAppWithP2p;
|
||||
import bisq.core.app.misc.ModuleForAppWithP2p;
|
||||
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.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 bisq.daonode.service.DaoNodeService;
|
||||
//todo not sure if the restart handling from seed nodes is required
|
||||
|
||||
@Slf4j
|
||||
public class DaoNode {
|
||||
@Setter
|
||||
private Injector injector;
|
||||
private AppSetup appSetup;
|
||||
private DaoNodeService daoNodeService;
|
||||
private GetInventoryRequestHandler getInventoryRequestHandler;
|
||||
public class DaoNode extends ExecutableForAppWithP2p {
|
||||
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
|
||||
|
||||
private Timer checkConnectionLossTime;
|
||||
@Getter
|
||||
private DaoStateService daoStateService;
|
||||
|
||||
public DaoNode() {
|
||||
super("Bisq Dao Node", "bisq-dao-node", "bisq_dao_node", Version.VERSION);
|
||||
}
|
||||
|
||||
public void startApplication(int restServerPort) {
|
||||
appSetup = injector.getInstance(AppSetupWithP2PAndDAO.class);
|
||||
public Config getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute() {
|
||||
super.doExecute();
|
||||
|
||||
checkMemory(config, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void launchApplication() {
|
||||
UserThread.execute(() -> {
|
||||
try {
|
||||
onApplicationLaunched();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We continue with a series of synchronous execution tasks
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected AppModule getModule() {
|
||||
return new ModuleForAppWithP2p(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyInjector() {
|
||||
super.applyInjector();
|
||||
|
||||
injector.getInstance(DaoStateSnapshotService.class).setDaoRequiresRestartHandler(this::gracefulShutDown);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startApplication() {
|
||||
super.startApplication();
|
||||
|
||||
Cookie cookie = injector.getInstance(User.class).getCookie();
|
||||
cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> {
|
||||
if (wasCleanTorDirSet) {
|
||||
injector.getInstance(TorSetup.class).cleanupTorFiles(() -> {
|
||||
log.info("Tor directory reset");
|
||||
cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART);
|
||||
}, log::error);
|
||||
}
|
||||
});
|
||||
|
||||
// todo should run as full dao node when in production
|
||||
injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false);
|
||||
injector.getInstance(AppSetupWithP2PAndDAO.class).start();
|
||||
|
||||
appSetup.start();
|
||||
daoStateService = injector.getInstance(DaoStateService.class);
|
||||
|
||||
getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class);
|
||||
DaoStateService daoStateService = injector.getInstance(DaoStateService.class);
|
||||
injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
|
||||
@Override
|
||||
public void onDataReceived() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
daoNodeService = new DaoNodeService(daoStateService);
|
||||
daoNodeService.start(restServerPort);
|
||||
@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(DaoNode.this);
|
||||
}
|
||||
UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestCustomBridges() {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
getInventoryRequestHandler.shutDown();
|
||||
daoNodeService.shutDown();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
71
daonode/src/main/java/bisq/daonode/SwaggerResolution.java
Normal file
71
daonode/src/main/java/bisq/daonode/SwaggerResolution.java
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.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;
|
||||
}
|
||||
}
|
@ -19,13 +19,18 @@ package bisq.daonode.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* Minimal data required for Bisq 2 reputation use case.
|
||||
* Need to be in sync with the Bisq 2 ProofOfBurnDto class.
|
||||
*/
|
||||
@Getter
|
||||
@Schema(title = "ProofOfBurn")
|
||||
public class ProofOfBurnDto {
|
||||
private String txId;
|
||||
private final String txId;
|
||||
private final long burnedAmount;
|
||||
private final int blockHeight;
|
||||
private final long time;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -15,10 +15,24 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.daonode.web.jdk.handler;
|
||||
package bisq.daonode.error;
|
||||
|
||||
public class ResourcePathElement {
|
||||
public static String DAONODE = "daonode";
|
||||
public static final String BLOCKHEIGHT = "blockheight";
|
||||
public static final String PROOFOFBURN = "proofofburn";
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
@Slf4j
|
||||
@Provider
|
||||
public class CustomExceptionMapper implements ExceptionMapper<Exception> {
|
||||
@Override
|
||||
public Response toResponse(Exception exception) {
|
||||
log.error("", exception);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorMessage(exception.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -15,11 +15,16 @@
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
108
daonode/src/main/java/bisq/daonode/util/StaticFileHandler.java
Normal file
108
daonode/src/main/java/bisq/daonode/util/StaticFileHandler.java
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
BIN
daonode/src/main/resources/doc/v1/favicon-16x16.png
Normal file
BIN
daonode/src/main/resources/doc/v1/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 B |
BIN
daonode/src/main/resources/doc/v1/favicon-32x32.png
Normal file
BIN
daonode/src/main/resources/doc/v1/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 628 B |
16
daonode/src/main/resources/doc/v1/index.css
Normal file
16
daonode/src/main/resources/doc/v1/index.css
Normal 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;
|
||||
}
|
19
daonode/src/main/resources/doc/v1/index.html
Normal file
19
daonode/src/main/resources/doc/v1/index.html
Normal 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>
|
80
daonode/src/main/resources/doc/v1/oauth2-redirect.html
Normal file
80
daonode/src/main/resources/doc/v1/oauth2-redirect.html
Normal 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>
|
20
daonode/src/main/resources/doc/v1/swagger-initializer.js
Normal file
20
daonode/src/main/resources/doc/v1/swagger-initializer.js
Normal 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>
|
||||
};
|
3
daonode/src/main/resources/doc/v1/swagger-ui-bundle.js
Normal file
3
daonode/src/main/resources/doc/v1/swagger-ui-bundle.js
Normal file
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
11092
daonode/src/main/resources/doc/v1/swagger-ui.css
Normal file
11092
daonode/src/main/resources/doc/v1/swagger-ui.css
Normal file
File diff suppressed because it is too large
Load Diff
2
daonode/src/main/resources/doc/v1/swagger-ui.js
Normal file
2
daonode/src/main/resources/doc/v1/swagger-ui.js
Normal file
File diff suppressed because one or more lines are too long
@ -14,12 +14,16 @@ Adjust the following command with tha arguments above and execute it twice:
|
||||
|
||||
The first execution should automatically update:
|
||||
|
||||
- `bisq/gradle/wrapper/gradle-wrapper.properties`
|
||||
- `bisq/gradle/wrapper/gradle-wrapper.properties`
|
||||
|
||||
The second execution should then update:
|
||||
|
||||
- `bisq/gradle/wrapper/gradle-wrapper.jar`
|
||||
- `bisq/gradlew`
|
||||
- `bisq/gradlew.bat`
|
||||
- `bisq/gradle/wrapper/gradle-wrapper.jar`
|
||||
- `bisq/gradlew`
|
||||
- `bisq/gradlew.bat`
|
||||
|
||||
The four updated files are ready to be committed.
|
||||
|
||||
To update the verification-metadata file run:
|
||||
|
||||
- `./gradlew --write-verification-metadata sha256`
|
||||
|
Loading…
Reference in New Issue
Block a user