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;
@ -273,7 +277,7 @@ public class Config {
ArgumentAcceptingOptionSpec<String> configFileOpt = ArgumentAcceptingOptionSpec<String> configFileOpt =
parser.accepts(CONFIG_FILE, format("Specify configuration file. " + 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() .withRequiredArg()
.ofType(String.class) .ofType(String.class)
.defaultsTo(DEFAULT_CONFIG_FILE_NAME); .defaultsTo(DEFAULT_CONFIG_FILE_NAME);
@ -346,7 +350,7 @@ public class Config {
ArgumentAcceptingOptionSpec<Boolean> ignoreLocalBtcNodeOpt = ArgumentAcceptingOptionSpec<Boolean> ignoreLocalBtcNodeOpt =
parser.accepts(IGNORE_LOCAL_BTC_NODE, 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() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(false); .defaultsTo(false);
@ -366,21 +370,21 @@ public class Config {
ArgumentAcceptingOptionSpec<Boolean> useDevModeOpt = ArgumentAcceptingOptionSpec<Boolean> useDevModeOpt =
parser.accepts(USE_DEV_MODE, 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() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> useDevModeHeaderOpt = ArgumentAcceptingOptionSpec<Boolean> useDevModeHeaderOpt =
parser.accepts(USE_DEV_MODE_HEADER, 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() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> useDevPrivilegeKeysOpt = ArgumentAcceptingOptionSpec<Boolean> useDevPrivilegeKeysOpt =
parser.accepts(USE_DEV_PRIVILEGE_KEYS, "If set to true all privileged features requiring a private " + 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() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
@ -393,9 +397,9 @@ public class Config {
ArgumentAcceptingOptionSpec<Boolean> ignoreDevMsgOpt = ArgumentAcceptingOptionSpec<Boolean> ignoreDevMsgOpt =
parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " + parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " +
"network_messages from bisq developers are ignored (Global " + "network_messages from bisq developers are ignored (Global " +
"alert, Version update alert, Filters for offers, nodes or " + "alert, Version update alert, Filters for offers, nodes or " +
"trading account data)") "trading account data)")
.withRequiredArg() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
@ -408,7 +412,7 @@ public class Config {
ArgumentAcceptingOptionSpec<String> seedNodesOpt = ArgumentAcceptingOptionSpec<String> seedNodesOpt =
parser.accepts(SEED_NODES, "Override hard coded seed nodes as comma separated list e.g. " + 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() .withRequiredArg()
.withValuesSeparatedBy(',') .withValuesSeparatedBy(',')
.describedAs("host:port[,...]"); .describedAs("host:port[,...]");
@ -440,29 +444,29 @@ public class Config {
ArgumentAcceptingOptionSpec<String> socks5ProxyHttpAddressOpt = ArgumentAcceptingOptionSpec<String> socks5ProxyHttpAddressOpt =
parser.accepts(SOCKS_5_PROXY_HTTP_ADDRESS, 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() .withRequiredArg()
.describedAs("host:port") .describedAs("host:port")
.defaultsTo(""); .defaultsTo("");
ArgumentAcceptingOptionSpec<Path> torrcFileOpt = ArgumentAcceptingOptionSpec<Path> torrcFileOpt =
parser.accepts(TORRC_FILE, "An existing torrc-file to be sourced for Tor. Note that torrc-entries, " + 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() .withRequiredArg()
.describedAs("File") .describedAs("File")
.withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE));
ArgumentAcceptingOptionSpec<String> torrcOptionsOpt = ArgumentAcceptingOptionSpec<String> torrcOptionsOpt =
parser.accepts(TORRC_OPTIONS, "A list of torrc-entries to amend to Bisq's torrc. Note that " + 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-entries, which are critical to Bisq's flawless operation, cannot be overwritten. " +
"[torrc options line, torrc option, ...]") "[torrc options line, torrc option, ...]")
.withRequiredArg() .withRequiredArg()
.withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$")) .withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$"))
.defaultsTo(""); .defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> torControlPortOpt = ArgumentAcceptingOptionSpec<Integer> torControlPortOpt =
parser.accepts(TOR_CONTROL_PORT, 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) .availableUnless(TORRC_FILE, TORRC_OPTIONS)
.withRequiredArg() .withRequiredArg()
.ofType(int.class) .ofType(int.class)
@ -477,7 +481,7 @@ public class Config {
ArgumentAcceptingOptionSpec<Path> torControlCookieFileOpt = ArgumentAcceptingOptionSpec<Path> torControlCookieFileOpt =
parser.accepts(TOR_CONTROL_COOKIE_FILE, "The cookie file for authenticating against the already " + 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) .availableIf(TOR_CONTROL_PORT)
.availableUnless(TOR_CONTROL_PASSWORD) .availableUnless(TOR_CONTROL_PASSWORD)
.withRequiredArg() .withRequiredArg()
@ -486,7 +490,7 @@ public class Config {
OptionSpecBuilder torControlUseSafeCookieAuthOpt = OptionSpecBuilder torControlUseSafeCookieAuthOpt =
parser.accepts(TOR_CONTROL_USE_SAFE_COOKIE_AUTH, 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); .availableIf(TOR_CONTROL_COOKIE_FILE);
OptionSpecBuilder torStreamIsolationOpt = OptionSpecBuilder torStreamIsolationOpt =
@ -532,21 +536,21 @@ public class Config {
ArgumentAcceptingOptionSpec<String> socks5DiscoverModeOpt = ArgumentAcceptingOptionSpec<String> socks5DiscoverModeOpt =
parser.accepts(SOCKS5_DISCOVER_MODE, "Specify discovery mode for Bitcoin nodes. " + 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() .withRequiredArg()
.describedAs("mode[,...]") .describedAs("mode[,...]")
.defaultsTo("ALL"); .defaultsTo("ALL");
ArgumentAcceptingOptionSpec<Boolean> useAllProvidedNodesOpt = ArgumentAcceptingOptionSpec<Boolean> useAllProvidedNodesOpt =
parser.accepts(USE_ALL_PROVIDED_NODES, 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() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<String> userAgentOpt = ArgumentAcceptingOptionSpec<String> userAgentOpt =
parser.accepts(USER_AGENT, parser.accepts(USER_AGENT,
"User agent at btc node connections") "User agent at btc node connections")
.withRequiredArg() .withRequiredArg()
.defaultsTo("Bisq"); .defaultsTo("Bisq");
@ -585,21 +589,21 @@ public class Config {
ArgumentAcceptingOptionSpec<String> rpcBlockNotificationHostOpt = ArgumentAcceptingOptionSpec<String> rpcBlockNotificationHostOpt =
parser.accepts(RPC_BLOCK_NOTIFICATION_HOST, parser.accepts(RPC_BLOCK_NOTIFICATION_HOST,
"Bitcoind rpc accepted incoming host for block notifications") "Bitcoind rpc accepted incoming host for block notifications")
.withRequiredArg() .withRequiredArg()
.defaultsTo(""); .defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> dumpBlockchainDataOpt = ArgumentAcceptingOptionSpec<Boolean> dumpBlockchainDataOpt =
parser.accepts(DUMP_BLOCKCHAIN_DATA, "If set to true the blockchain data " + 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() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> fullDaoNodeOpt = ArgumentAcceptingOptionSpec<Boolean> fullDaoNodeOpt =
parser.accepts(FULL_DAO_NODE, "If set to true the node requests the blockchain data via RPC requests " + 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 " + "from Bitcoin Core and provide the validated BSQ txs to the network. It requires that the " +
"other RPC properties are set as well.") "other RPC properties are set as well.")
.withRequiredArg() .withRequiredArg()
.ofType(Boolean.class) .ofType(Boolean.class)
.defaultsTo(DEFAULT_FULL_DAO_NODE); .defaultsTo(DEFAULT_FULL_DAO_NODE);
@ -611,7 +615,7 @@ public class Config {
ArgumentAcceptingOptionSpec<Integer> genesisBlockHeightOpt = ArgumentAcceptingOptionSpec<Integer> genesisBlockHeightOpt =
parser.accepts(GENESIS_BLOCK_HEIGHT, 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() .withRequiredArg()
.ofType(int.class) .ofType(int.class)
.defaultsTo(-1); .defaultsTo(-1);
@ -636,7 +640,7 @@ public class Config {
ArgumentAcceptingOptionSpec<Boolean> allowFaultyDelayedTxsOpt = ArgumentAcceptingOptionSpec<Boolean> allowFaultyDelayedTxsOpt =
parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " + parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " +
"payout transactions") "payout transactions")
.withRequiredArg() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
@ -654,25 +658,36 @@ public class Config {
ArgumentAcceptingOptionSpec<Boolean> preventPeriodicShutdownAtSeedNodeOpt = ArgumentAcceptingOptionSpec<Boolean> preventPeriodicShutdownAtSeedNodeOpt =
parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE, parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE,
"Prevents periodic shutdown at seed nodes") "Prevents periodic shutdown at seed nodes")
.withRequiredArg() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> republishMailboxEntriesOpt = ArgumentAcceptingOptionSpec<Boolean> republishMailboxEntriesOpt =
parser.accepts(REPUBLISH_MAILBOX_ENTRIES, parser.accepts(REPUBLISH_MAILBOX_ENTRIES,
"Republish mailbox messages at startup") "Republish mailbox messages at startup")
.withRequiredArg() .withRequiredArg()
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> bypassMempoolValidationOpt = ArgumentAcceptingOptionSpec<Boolean> bypassMempoolValidationOpt =
parser.accepts(BYPASS_MEMPOOL_VALIDATION, parser.accepts(BYPASS_MEMPOOL_VALIDATION,
"Prevents mempool check of trade parameters") "Prevents mempool check of trade parameters")
.withRequiredArg() .withRequiredArg()
.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() {
// Do nothing
}
daoNodeService = new DaoNodeService(daoStateService); @Override
daoNodeService.start(restServerPort); 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() { private void setupConnectionLossCheck() {
getInventoryRequestHandler.shutDown(); // For dev testing (usually on BTC_REGTEST) we don't want to get the seed shut
daoNodeService.shutDown(); // 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

@ -14,12 +14,16 @@ Adjust the following command with tha arguments above and execute it twice:
The first execution should automatically update: The first execution should automatically update:
- `bisq/gradle/wrapper/gradle-wrapper.properties` - `bisq/gradle/wrapper/gradle-wrapper.properties`
The second execution should then update: The second execution should then update:
- `bisq/gradle/wrapper/gradle-wrapper.jar` - `bisq/gradle/wrapper/gradle-wrapper.jar`
- `bisq/gradlew` - `bisq/gradlew`
- `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`