diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index b8114e891..45aa32457 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -8,10 +8,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@v2
         with:
-          java-version: '17'
+          java-version: '21'
           distribution: 'adopt'
       - name: Validate Gradle wrapper
         uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index bb0bb5658..1443ab4f7 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -11,10 +11,10 @@ jobs:
         with:
           # Shallow clones should be disabled for a better relevancy of analysis
           fetch-depth: 0
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@v1
         with:
-          java-version: 17
+          java-version: 21
       - name: Cache Maven packages
         uses: actions/cache@v1
         with:
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
index bf82ff01c..cb28b0e37 100644
Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 9527f33d8..346d645fd 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -6,7 +6,7 @@
 # "License"); you may not use this file except in compliance
 # with the License.  You may obtain a copy of the License at
 #
-#   https://www.apache.org/licenses/LICENSE-2.0
+#   http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing,
 # software distributed under the License is distributed on an
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/3rd-party-licenses.md b/3rd-party-licenses.md
index a82a990be..05da90e60 100644
--- a/3rd-party-licenses.md
+++ b/3rd-party-licenses.md
@@ -4,11 +4,11 @@
 
 ## Product
 
-* GSON                     - https://github.com/google/gson/blob/master/LICENSE
-* Guava                    - https://github.com/google/guava/blob/master/COPYING
+* GSON                     - https://github.com/google/gson/blob/main/LICENSE
+* Guava                    - https://github.com/google/guava/blob/main/COPYING
 * log4j                    - https://logging.apache.org/log4j/2.0/license.html
 * JavaMail                 - https://glassfish.java.net/public/CDDL+GPL_1_1.html
-* Spring Boot              - https://github.com/spring-projects/spring-boot/blob/master/LICENSE.txt
+* Spring Boot              - https://github.com/spring-projects/spring-boot/blob/main/LICENSE.txt
 * Snake YAML               - https://bitbucket.org/asomov/snakeyaml/src/default/LICENSE.txt
 * H2                       - https://www.h2database.com/html/license.html
 * JJWT                     - https://github.com/jwtk/jjwt/blob/master/LICENSE
@@ -20,14 +20,14 @@
 
 ## Build & Test
 
-* JUnit                     - http://opensource.org/licenses/eclipse-1.0.html
+* JUnit                     - https://opensource.org/licenses/eclipse-1.0.html
 * Powermock                 - https://github.com/jayway/powermock/blob/master/LICENSE.txt
-* EasyMock                  - http://easymock.org/License.html
-* Maven                     - http://www.apache.org/licenses/
+* EasyMock                  - https://easymock.org/License.html
+* Maven                     - https://www.apache.org/licenses/
 * Gradle                    - https://gradle.org/license/
 * JsonPath                  - https://github.com/jayway/JsonPath/blob/master/LICENSE
 * Awaitility                - https://github.com/awaitility/awaitility/blob/master/LICENSE
-* spring-boot-starter-test  - http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html
+* spring-boot-starter-test  - https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html
 * SpotBugs                  - https://spotbugs.github.io/index.html
 * JaCoCo                    - https://www.jacoco.org/license.html
 * Checkstyle                - https://checkstyle.org/licenses.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0d7821075..d826e7278 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ To work on something, whether a new feature or a bug fix:
 
   
 After your PR has been reviewed and signed off, a maintainer will merge it into 
-the master branch.
+the main branch.
 
 ## Code conventions and housekeeping
 
diff --git a/Dockerfile b/Dockerfile
index 5861d964d..3f13c4d4e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM eclipse-temurin:17.0.7_7-jdk
+FROM eclipse-temurin:21.0.2_13-jdk
 
 RUN apt-get update
 RUN apt-get install -y maven
diff --git a/README.md b/README.md
index 625e18adf..544012538 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # BX-bot
 
-[![Gradle CI](https://github.com/gazbert/bxbot/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/gazbert/bxbot/actions/workflows/gradle.yml)
-[![Maven CI](https://github.com/gazbert/bxbot/actions/workflows/maven.yml/badge.svg?branch=master)](https://github.com/gazbert/bxbot/actions/workflows/maven.yml)
+[![Gradle CI](https://github.com/gazbert/bxbot/actions/workflows/gradle.yml/badge.svg?branch=main)](https://github.com/gazbert/bxbot/actions/workflows/gradle.yml)
+[![Maven CI](https://github.com/gazbert/bxbot/actions/workflows/maven.yml/badge.svg?branch=main)](https://github.com/gazbert/bxbot/actions/workflows/maven.yml)
 [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=gazbert_bxbot&metric=alert_status)](https://sonarcloud.io/dashboard?id=gazbert_bxbot)
 [![Join the chat at https://gitter.im/BX-bot/Lobby](https://badges.gitter.im/BX-bot/Lobby.svg)](https://gitter.im/BX-bot/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)		 	 
  
@@ -12,13 +12,13 @@
 BX-bot (_Bex_) is a simple Bitcoin trading bot written in Java for trading on cryptocurrency 
 [exchanges](https://bitcoin.org/en/exchanges).
 
-The project contains the basic infrastructure to trade on a [cryptocurrency](http://coinmarketcap.com/) exchange...
+The project contains the basic infrastructure to trade on a [cryptocurrency](https://coinmarketcap.com/) exchange...
 except for the trading strategies - you'll need to write those yourself! A simple 
 [`ExampleScalpingStrategy`](./bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleScalpingStrategy.java) 
 is included to get you started with the Trading API - see [Ta4j](https://github.com/ta4j/ta4j) for more ideas.
 
 Exchange Adapters for using [Bitstamp](https://www.bitstamp.net), [Bitfinex](https://www.bitfinex.com), 
-[Kraken](https://www.kraken.com), [Gemini](https://gemini.com/), and [Coinbase Pro](https://pro.coinbase.com/) are included. 
+[Kraken](https://www.kraken.com), and [Gemini](https://gemini.com/) are included. 
 Feel free to improve these or contribute new adapters to the project; that would be 
 [shiny!](https://en.wikipedia.org/wiki/Firefly_(TV_series))
 
@@ -27,13 +27,13 @@ configured by default to delegate public API calls to Bitstamp, but it simulates
 API (order management) calls; it's good for testing your initial setup and 
 [paper trading](https://www.investopedia.com/terms/p/papertrade.asp) without actually sending orders to the exchange.
 
-The Trading API provides support for [limit orders](http://www.investopedia.com/terms/l/limitorder.asp)
-traded at the [spot price](http://www.investopedia.com/terms/s/spotprice.asp).
+The Trading API provides support for [limit orders](https://www.investopedia.com/terms/l/limitorder.asp)
+traded at the [spot price](https://www.investopedia.com/terms/s/spotprice.asp).
 If you're looking for something more sophisticated with a much richer Trading API, take a look at
 [XChange](https://github.com/knowm/XChange).
  
 **Warning:** Trading cryptocurrency carries significant financial risk; you could lose money. This software is provided 'as is'
-and released under the [MIT license](http://opensource.org/licenses/MIT).
+and released under the [MIT license](https://opensource.org/licenses/MIT).
 
 ## Architecture
 
@@ -58,8 +58,8 @@ it will log the error, send an email alert (if configured), and then shut down.
   
 The bot runs on Linux, macOS, and Windows.
 
-BX-bot is supported on the current JDK 17 LTS. You'll need the JDK (e.g. [OpenJDK 17](http://openjdk.java.net/projects/jdk/17) or 
-[Oracle JDK 17](https://www.oracle.com/uk/java/technologies/downloads/#java17))
+BX-bot is supported on the current JDK 21 LTS. You'll need the JDK (e.g. [OpenJDK 21](https://openjdk.java.net/projects/jdk/21) or 
+[Oracle JDK 21](https://www.oracle.com/uk/java/technologies/downloads/#java21))
 installed on the machine you are going to use to build and run the bot.
 Be mindful of Oracle's recent [licensing changes](https://www.oracle.com/technetwork/java/javase/overview/oracle-jdk-faqs.html)
 and how you intend to use the bot.
@@ -112,13 +112,13 @@ and evaluate the bot, Docker is the way to go.
    Then run: `docker container attach <CONTAINER ID>`   
    
 ## Build Guide
-If you plan on developing the bot, you'll need JDK 17 installed on your dev box.
+If you plan on developing the bot, you'll need JDK 21 installed on your dev box.
 
 You can use Gradle or Maven to build the bot and pull down the dependencies.
 
 The instructions below are for Linux/macOS, but equivalent Windows scripts are included.
 
-Clone the repo locally (master branch).
+Clone the repo locally (main branch).
 
 ### Maven
 1. From the project root, run `./mvnw clean install`.
@@ -149,7 +149,7 @@ The bot has undergone basic unit testing on a _best-effort_ basis.
 There is a CI build running on [GitHub Actions](https://github.com/gazbert/bxbot/actions).
 
 The latest stable build can always be found on the [Releases](https://github.com/gazbert/bxbot/releases) page. 
-The SNAPSHOT builds on master are active development builds, but the tests should always pass and the bot should always 
+The SNAPSHOT builds on main are active development builds, but the tests should always pass and the bot should always 
 be deployable.
 
 ## User Guide
@@ -533,7 +533,7 @@ your own jar for your adapters, e.g. `my-adapters.jar`, and include it on BX-bot
 see the _[Installation Guide](#the-manual-way)_ for how to do this.
 
 ### Logging
-Logging for the bot is provided by [log4j](http://logging.apache.org/log4j). The log file is written to `logs/bxbot.log` 
+Logging for the bot is provided by [log4j](https://logging.apache.org/log4j). The log file is written to `logs/bxbot.log` 
 using a rolling policy. When a log file size reaches 100 MB or a new day is started, it is archived and a new log file 
 is created. BX-bot will create up to 7 archives on the same day; these are stored in a directory based on the current 
 year and month. Only the last 90 archives are kept. Each archive is compressed using gzip. The logging level is set 
@@ -612,7 +612,7 @@ JWT before it expires in order to get a new one. Alternatively, you can re-authe
 The REST API must be configured to use TLS before accessing it over a public network.
 
 You will need to 
-[create a keystore](https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html) - the command to
+[create a keystore](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html) - the command to
 create a [PKCS12](https://en.wikipedia.org/wiki/PKCS_12) self-signed certificate is shown below:
 
 ```bash
diff --git a/build.gradle b/build.gradle
index f0f1f54ec..a6675f2cd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,15 +6,15 @@ buildscript {
   }
 
   dependencies {
-    classpath('io.spring.gradle:dependency-management-plugin:1.1.3')
-    classpath('org.springframework.boot:spring-boot-gradle-plugin:3.1.3')
+    classpath('io.spring.gradle:dependency-management-plugin:1.1.4')
+    classpath('org.springframework.boot:spring-boot-gradle-plugin:3.1.11')
   }
 }
 
 plugins {
-  id 'org.sonarqube' version '4.3.1.3277'  // https://plugins.gradle.org/plugin/org.sonarqube
+  id 'org.sonarqube' version '5.0.0.4638'  // https://plugins.gradle.org/plugin/org.sonarqube
   id 'jacoco'
-  id 'com.github.spotbugs' version '5.1.3' // https://plugins.gradle.org/plugin/com.github.spotbugs
+  id 'com.github.spotbugs' version '5.2.5' // https://plugins.gradle.org/plugin/com.github.spotbugs
 }
 
 buildScan {
@@ -23,11 +23,11 @@ buildScan {
 }
 
 ext.versions = [
-    springBootVersion        : '3.1.3',
-    springCloudVersion       : '4.0.4',
+    springBootVersion        : '3.1.11',
+    springCloudVersion       : '4.0.5',
 
     // Should be same as dependency used by springBootVersion
-    springCoreVersion        : '6.0.11',
+    springCoreVersion        : '6.0.19',
 
     hibernateVaildatorVersion: '8.0.1.Final',
     jjwtVersion              : '0.11.5'
@@ -62,20 +62,20 @@ ext.libraries = [
     jjwt_impl                               : dependencies.create("io.jsonwebtoken:jjwt-impl:" + ext.versions.jjwtVersion),
     jjwt_jackson                            : dependencies.create("io.jsonwebtoken:jjwt-jackson:" + ext.versions.jjwtVersion),
 
-    google_guava                            : dependencies.create("com.google.guava:guava:32.1.2-jre"),
+    google_guava                            : dependencies.create("com.google.guava:guava:33.1.0-jre"),
     google_gson                             : dependencies.create("com.google.code.gson:gson:2.10.1"),
-    h2                                      : dependencies.create("com.h2database:h2:2.2.222"),
+    h2                                      : dependencies.create("com.h2database:h2:2.2.224"),
 
-    jakarta_mail_api                        : dependencies.create("jakarta.mail:jakarta.mail-api:2.1.2"),
-    jakarta_mail_sun                        : dependencies.create("org.eclipse.angus:angus-mail:2.0.2"),
+    jakarta_mail_api                        : dependencies.create("jakarta.mail:jakarta.mail-api:2.1.3"),
+    jakarta_mail_sun                        : dependencies.create("org.eclipse.angus:angus-mail:2.0.3"),
 
-    jakarta_xml_api                         : dependencies.create("javax.xml.bind:jaxb-api:2.4.0-b180830.0359"),
+    jakarta_xml_api                         : dependencies.create("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"),
 
     snake_yaml                              : dependencies.create("org.yaml:snakeyaml:1.33"),
 
     springdoc_openapi_ui                    : dependencies.create("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"),
 
-    lombok                                  : dependencies.create("org.projectlombok:lombok:1.18.20"),
+    lombok                                  : dependencies.create("org.projectlombok:lombok:1.18.32"),
 
     validation_api                          : dependencies.create("jakarta.validation:jakarta.validation-api:3.0.2"),
     hibernate_validator                     : dependencies.create("org.hibernate.validator:hibernate-validator:" + ext.versions.hibernateVaildatorVersion),
@@ -86,13 +86,13 @@ ext.libraries = [
     easymock                                : dependencies.create("org.easymock:easymock:5.2.0"),
 
     // JUnit 4 still as Powermock does not play with Junit 5: https://github.com/powermock/powermock/issues/929
-    junit_vintage_engine                    : dependencies.create("org.junit.vintage:junit-vintage-engine:5.10.0"),
+    junit_vintage_engine                    : dependencies.create("org.junit.vintage:junit-vintage-engine:5.10.2"),
 
     spring_boot_starter_test                : dependencies.create("org.springframework.boot:spring-boot-starter-test:" + ext.versions.springBootVersion) {
       exclude module: "spring-boot-starter-logging"
     },
-    spring_security_test                    : dependencies.create("org.springframework.security:spring-security-test:6.1.3"),
-    awaitility                              : dependencies.create("org.awaitility:awaitility:4.2.0")
+    spring_security_test                    : dependencies.create("org.springframework.security:spring-security-test:6.1.8"),
+    awaitility                              : dependencies.create("org.awaitility:awaitility:4.2.1")
 ]
 
 allprojects {
@@ -117,8 +117,8 @@ subprojects {
 
   apply plugin: 'java'
 
-  sourceCompatibility = 1.17
-  targetCompatibility = 1.17
+  sourceCompatibility = 1.21
+  targetCompatibility = 1.21
 
   repositories {
     mavenCentral()
@@ -151,7 +151,7 @@ subprojects {
   }
 
   jacoco {
-    toolVersion = "0.8.9" // https://docs.gradle.org/current/userguide/jacoco_plugin.html
+    toolVersion = "0.8.11" // https://docs.gradle.org/current/userguide/jacoco_plugin.html
   }
   jacocoTestReport {
     reports {
diff --git a/bxbot-app/pom.xml b/bxbot-app/pom.xml
index 45911a268..6249360de 100644
--- a/bxbot-app/pom.xml
+++ b/bxbot-app/pom.xml
@@ -117,6 +117,10 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-core/pom.xml b/bxbot-core/pom.xml
index 96ea5e343..ca8d42b07 100644
--- a/bxbot-core/pom.xml
+++ b/bxbot-core/pom.xml
@@ -166,6 +166,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/exchange/ExchangeConfigImpl.java b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/exchange/ExchangeConfigImpl.java
index f92472ddd..03fe93f9f 100644
--- a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/exchange/ExchangeConfigImpl.java
+++ b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/exchange/ExchangeConfigImpl.java
@@ -42,6 +42,11 @@ public class ExchangeConfigImpl implements ExchangeConfig {
   private NetworkConfig networkConfig;
   private OtherConfig otherConfig;
 
+  /** Creates the Exchange Config impl. */
+  public ExchangeConfigImpl() {
+    // No extra init needed.
+  }
+
   @Override
   public String getExchangeName() {
     return exchangeName;
diff --git a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/StrategyConfigItems.java b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/StrategyConfigItems.java
index 868a22081..325cae190 100644
--- a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/StrategyConfigItems.java
+++ b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/StrategyConfigItems.java
@@ -39,6 +39,11 @@ public final class StrategyConfigItems implements StrategyConfig {
 
   private Map<String, String> items = new HashMap<>();
 
+  /** Creates the Strategy Config Items. */
+  public StrategyConfigItems() {
+    // No extra init needed.
+  }
+
   @Override
   public String getConfigItem(String key) {
     return items.get(key);
diff --git a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/TradingStrategiesBuilder.java b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/TradingStrategiesBuilder.java
index 17b8c5aba..09f124941 100644
--- a/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/TradingStrategiesBuilder.java
+++ b/bxbot-core/src/main/java/com/gazbert/bxbot/core/config/strategy/TradingStrategiesBuilder.java
@@ -50,6 +50,11 @@ public class TradingStrategiesBuilder {
 
   private TradingStrategyFactory tradingStrategyFactory;
 
+  /** Creates the Trading Strategies Builder. */
+  public TradingStrategiesBuilder() {
+    // No extra init needed.
+  }
+
   /**
    * Sets the trading strategy factory.
    *
diff --git a/bxbot-core/src/main/java/com/gazbert/bxbot/core/util/ConfigurableComponentFactory.java b/bxbot-core/src/main/java/com/gazbert/bxbot/core/util/ConfigurableComponentFactory.java
index 5504ecfea..e23102977 100644
--- a/bxbot-core/src/main/java/com/gazbert/bxbot/core/util/ConfigurableComponentFactory.java
+++ b/bxbot-core/src/main/java/com/gazbert/bxbot/core/util/ConfigurableComponentFactory.java
@@ -37,6 +37,11 @@
 @Log4j2
 public class ConfigurableComponentFactory {
 
+  /** Creates the Configurable Component Factory. */
+  public ConfigurableComponentFactory() {
+    // No extra init needed.
+  }
+
   /**
    * Loads and instantiates a given class and returns it.
    *
diff --git a/bxbot-domain-objects/pom.xml b/bxbot-domain-objects/pom.xml
index f6e30eeb5..e7b447ee2 100644
--- a/bxbot-domain-objects/pom.xml
+++ b/bxbot-domain-objects/pom.xml
@@ -69,6 +69,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-domain-objects/src/main/java/com/gazbert/bxbot/domain/exchange/ExchangeConfig.java b/bxbot-domain-objects/src/main/java/com/gazbert/bxbot/domain/exchange/ExchangeConfig.java
index a66593758..55b937009 100644
--- a/bxbot-domain-objects/src/main/java/com/gazbert/bxbot/domain/exchange/ExchangeConfig.java
+++ b/bxbot-domain-objects/src/main/java/com/gazbert/bxbot/domain/exchange/ExchangeConfig.java
@@ -36,7 +36,7 @@
 public class ExchangeConfig {
 
   @Schema(
-          requiredMode = Schema.RequiredMode.REQUIRED,
+      requiredMode = Schema.RequiredMode.REQUIRED,
       description =
           "The Exchange name. It is used in log statements to display the Exchange's name."
               + " Value must be an alphanumeric string. Spaces are allowed.")
@@ -69,6 +69,11 @@ public class ExchangeConfig {
               + "any additional config, e.g. buy/sell fees.")
   private Map<String, String> otherConfig;
 
+  /** Creates the Exchange config. */
+  public ExchangeConfig() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the name.
    *
diff --git a/bxbot-exchange-api/pom.xml b/bxbot-exchange-api/pom.xml
index 857a36c7a..f600d10a9 100644
--- a/bxbot-exchange-api/pom.xml
+++ b/bxbot-exchange-api/pom.xml
@@ -69,6 +69,10 @@
         <groupId>com.github.spotbugs</groupId>
         <artifactId>spotbugs-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-exchanges/build.gradle b/bxbot-exchanges/build.gradle
index 4cb475abc..485261b94 100644
--- a/bxbot-exchanges/build.gradle
+++ b/bxbot-exchanges/build.gradle
@@ -53,7 +53,7 @@ configurations {
   integrationTestsRuntimeOnly.extendsFrom(testRuntimeOnly)
 }
 
-// For Powermock tests messing with JDK 17 bytecode.
+// For Powermock tests messing with JDK 17+ bytecode.
 // See: https://stackoverflow.com/questions/69896191/powermock-compatibility-with-jdk-17
 test.jvmArgs "--add-opens", "java.base/java.lang=ALL-UNNAMED"
 test.jvmArgs "--add-opens", "java.base/java.util=ALL-UNNAMED"
diff --git a/bxbot-exchanges/pom.xml b/bxbot-exchanges/pom.xml
index 395c0b9ef..68e3b725f 100644
--- a/bxbot-exchanges/pom.xml
+++ b/bxbot-exchanges/pom.xml
@@ -85,8 +85,8 @@
       <artifactId>guava</artifactId>
     </dependency>
     <dependency>
-      <groupId>javax.xml.bind</groupId>
-      <artifactId>jaxb-api</artifactId>
+      <groupId>jakarta.xml.bind</groupId>
+      <artifactId>jakarta.xml.bind-api</artifactId>
     </dependency>
     <dependency>
       <groupId>org.projectlombok</groupId>
@@ -142,7 +142,7 @@
             <include>**/Test*.java</include>
           </includes>
           <!--
-            For Powermock tests messing with JDK 17 bytecode.
+            For Powermock tests messing with JDK 17+ bytecode.
             See: https://stackoverflow.com/questions/69896191/powermock-compatibility-with-jdk-17
           -->
           <argLine>
@@ -249,6 +249,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java b/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java
deleted file mode 100644
index deca79c71..000000000
--- a/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2021 Gareth Jon Lynch
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- * the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-package com.gazbert.bxbot.exchanges;
-
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.gazbert.bxbot.exchange.api.AuthenticationConfig;
-import com.gazbert.bxbot.exchange.api.ExchangeAdapter;
-import com.gazbert.bxbot.exchange.api.ExchangeConfig;
-import com.gazbert.bxbot.exchange.api.NetworkConfig;
-import com.gazbert.bxbot.exchange.api.OtherConfig;
-import com.gazbert.bxbot.trading.api.BalanceInfo;
-import com.gazbert.bxbot.trading.api.MarketOrderBook;
-import com.gazbert.bxbot.trading.api.Ticker;
-import java.math.BigDecimal;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-
-/**
- * Basic integration testing with Coinbase Pro exchange.
- *
- * @author gazbert
- */
-public class CoinbaseProIT {
-
-  private static final String MARKET_ID = "BTC-GBP";
-  private static final BigDecimal BUY_ORDER_PRICE = new BigDecimal("450.176");
-  private static final BigDecimal BUY_ORDER_QUANTITY = new BigDecimal("0.01");
-
-  private static final String PASSPHRASE = "lePassPhrase";
-  private static final String KEY = "key123";
-  private static final String SECRET = "notGonnaTellYa";
-  private static final List<Integer> nonFatalNetworkErrorCodes = Arrays.asList(502, 503, 504);
-  private static final List<String> nonFatalNetworkErrorMessages =
-      Arrays.asList(
-          "Connection refused",
-          "Connection reset",
-          "Remote host closed connection during handshake");
-
-  private ExchangeConfig exchangeConfig;
-  private AuthenticationConfig authenticationConfig;
-  private NetworkConfig networkConfig;
-  private OtherConfig otherConfig;
-
-  /** Create some exchange config - the TradingEngine would normally do this. */
-  @Before
-  public void setupForEachTest() {
-    authenticationConfig = createMock(AuthenticationConfig.class);
-    expect(authenticationConfig.getItem("passphrase")).andReturn(PASSPHRASE);
-    expect(authenticationConfig.getItem("key")).andReturn(KEY);
-    expect(authenticationConfig.getItem("secret")).andReturn(SECRET);
-
-    networkConfig = createMock(NetworkConfig.class);
-    expect(networkConfig.getConnectionTimeout()).andReturn(30);
-    expect(networkConfig.getNonFatalErrorCodes()).andReturn(nonFatalNetworkErrorCodes);
-    expect(networkConfig.getNonFatalErrorMessages()).andReturn(nonFatalNetworkErrorMessages);
-
-    otherConfig = createMock(OtherConfig.class);
-    expect(otherConfig.getItem("buy-fee")).andReturn("0.25");
-    expect(otherConfig.getItem("sell-fee")).andReturn("0.25");
-    expect(otherConfig.getItem("time-server-bias")).andReturn("1");
-
-    exchangeConfig = createMock(ExchangeConfig.class);
-    expect(exchangeConfig.getAuthenticationConfig()).andReturn(authenticationConfig);
-    expect(exchangeConfig.getNetworkConfig()).andReturn(networkConfig);
-    expect(exchangeConfig.getOtherConfig()).andReturn(otherConfig);
-  }
-
-  @Test
-  public void testPublicApiCalls() throws Exception {
-    replay(authenticationConfig, networkConfig, otherConfig, exchangeConfig);
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    assertNotNull(exchangeAdapter.getLatestMarketPrice(MARKET_ID));
-
-    final MarketOrderBook orderBook = exchangeAdapter.getMarketOrders(MARKET_ID);
-    assertFalse(orderBook.getBuyOrders().isEmpty());
-    assertFalse(orderBook.getSellOrders().isEmpty());
-
-    final Ticker ticker = exchangeAdapter.getTicker(MARKET_ID);
-    assertNotNull(ticker.getLast());
-    assertNotNull(ticker.getAsk());
-    assertNotNull(ticker.getBid());
-    assertNotNull(ticker.getHigh());
-    assertNotNull(ticker.getLow());
-    assertNotNull(ticker.getOpen());
-    assertNotNull(ticker.getVolume());
-    assertNull(ticker.getVwap()); // not provided by Coinbase Pro
-    assertNotNull(ticker.getTimestamp());
-
-    verify(authenticationConfig, networkConfig, otherConfig, exchangeConfig);
-  }
-
-  /*
-   * You'll need to change the PASSPHRASE, KEY, SECRET, constants to real-world values.
-   */
-  @Ignore("Disabled. Integration testing authenticated API calls requires your secret credentials!")
-  @Test
-  public void testAuthenticatedApiCalls() throws Exception {
-    replay(authenticationConfig, networkConfig, otherConfig, exchangeConfig);
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BalanceInfo balanceInfo = exchangeAdapter.getBalanceInfo();
-    assertNotNull(balanceInfo.getBalancesAvailable().get("BTC"));
-
-    // Careful here: make sure the SELL_ORDER_PRICE is sensible!
-    // final String orderId = exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY,
-    // BUY_ORDER_QUANTITY, BUY_ORDER_PRICE);
-    // final List<OpenOrder> openOrders = exchangeAdapter.getYourOpenOrders(MARKET_ID);
-    // assertTrue(openOrders.stream().anyMatch(o -> o.getId().equals(orderId)));
-    // assertTrue(exchangeAdapter.cancelOrder(orderId, MARKET_ID));
-
-    verify(authenticationConfig, networkConfig, otherConfig, exchangeConfig);
-  }
-}
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitfinexExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitfinexExchangeAdapter.java
index a9e1e3cd3..b1518754b 100644
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitfinexExchangeAdapter.java
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitfinexExchangeAdapter.java
@@ -44,10 +44,13 @@
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.annotations.SerializedName;
+import jakarta.xml.bind.DatatypeConverter;
 import java.io.Serial;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
@@ -62,7 +65,6 @@
 import java.util.Map;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-import javax.xml.bind.DatatypeConverter;
 import lombok.extern.log4j.Log4j2;
 
 /**
@@ -135,6 +137,11 @@ public final class BitfinexExchangeAdapter extends AbstractExchangeAdapter
 
   private Gson gson;
 
+  /** Constructs the Exchange Adapter. */
+  public BitfinexExchangeAdapter() {
+    // No extra init.
+  }
+
   @Override
   public void init(ExchangeConfig config) {
     log.info("About to initialise Bitfinex ExchangeConfig: " + config);
@@ -895,10 +902,10 @@ public String toString() {
   private ExchangeHttpResponse sendPublicRequestToExchange(String apiMethod)
       throws ExchangeNetworkException, TradingApiException {
     try {
-      final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod);
+      final URL url = new URI(PUBLIC_API_BASE_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "GET", null, createHeaderParamMap());
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
@@ -981,10 +988,10 @@ private ExchangeHttpResponse sendAuthenticatedRequestToExchange(
       // payload is JSON for this exchange
       requestHeaders.put("Content-Type", "application/json");
 
-      final URL url = new URL(AUTHENTICATED_API_URL + apiMethod);
+      final URL url = new URI(AUTHENTICATED_API_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "POST", paramsInJson, requestHeaders);
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitstampExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitstampExchangeAdapter.java
index d9ea33a15..bb5b97e77 100644
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitstampExchangeAdapter.java
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/BitstampExchangeAdapter.java
@@ -53,6 +53,8 @@
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLEncoder;
@@ -129,6 +131,11 @@ public class BitstampExchangeAdapter extends AbstractExchangeAdapter implements
 
   private Gson gson;
 
+  /** Constructs the Exchange Adapter. */
+  public BitstampExchangeAdapter() {
+    // No extra init.
+  }
+
   @Override
   public void init(ExchangeConfig config) {
     log.info("About to initialise Bitstamp ExchangeConfig: " + config);
@@ -749,10 +756,10 @@ public Date deserialize(JsonElement json, Type type, JsonDeserializationContext
   private ExchangeHttpResponse sendPublicRequestToExchange(String apiMethod)
       throws ExchangeNetworkException, TradingApiException {
     try {
-      final URL url = new URL(API_BASE_URL + apiMethod);
+      final URL url = new URI(API_BASE_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "GET", null, createHeaderParamMap());
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
@@ -815,10 +822,10 @@ private ExchangeHttpResponse sendAuthenticatedRequestToExchange(
       requestHeaders.put("Content-Type", "application/x-www-form-urlencoded");
 
       // MUST have the trailing slash else exchange barfs...
-      final URL url = new URL(API_BASE_URL + apiMethod + "/");
+      final URL url = new URI(API_BASE_URL + apiMethod + "/").toURL();
       return makeNetworkRequest(url, "POST", postData.toString(), requestHeaders);
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java
deleted file mode 100644
index 7603af9ca..000000000
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java
+++ /dev/null
@@ -1,960 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2015 Gareth Jon Lynch
- * Copyright (c) 2019 David Huertas
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- * the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-package com.gazbert.bxbot.exchanges;
-
-import com.gazbert.bxbot.exchange.api.AuthenticationConfig;
-import com.gazbert.bxbot.exchange.api.ExchangeAdapter;
-import com.gazbert.bxbot.exchange.api.ExchangeConfig;
-import com.gazbert.bxbot.exchange.api.OtherConfig;
-import com.gazbert.bxbot.exchanges.trading.api.impl.BalanceInfoImpl;
-import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderBookImpl;
-import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderImpl;
-import com.gazbert.bxbot.exchanges.trading.api.impl.OpenOrderImpl;
-import com.gazbert.bxbot.exchanges.trading.api.impl.TickerImpl;
-import com.gazbert.bxbot.trading.api.BalanceInfo;
-import com.gazbert.bxbot.trading.api.ExchangeNetworkException;
-import com.gazbert.bxbot.trading.api.MarketOrder;
-import com.gazbert.bxbot.trading.api.MarketOrderBook;
-import com.gazbert.bxbot.trading.api.OpenOrder;
-import com.gazbert.bxbot.trading.api.OrderType;
-import com.gazbert.bxbot.trading.api.Ticker;
-import com.gazbert.bxbot.trading.api.TradingApi;
-import com.gazbert.bxbot.trading.api.TradingApiException;
-import com.google.common.base.MoreObjects;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.annotations.SerializedName;
-import java.io.Serial;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.DecimalFormat;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import javax.xml.bind.DatatypeConverter;
-import lombok.extern.log4j.Log4j2;
-
-/**
- * Exchange Adapter for integrating with the CoinbasePro exchange. The CoinbasePro API is documented
- * <a href="https://docs.pro.coinbase.com">here</a>.
- *
- * <p><strong> DISCLAIMER: This Exchange Adapter is provided as-is; it might have bugs in it and you
- * could lose money. Despite running live on COINBASE PRO, it has only been unit tested up until the
- * point of calling the {@link #sendPublicRequestToExchange(String, Map)} and {@link
- * #sendAuthenticatedRequestToExchange(String, String, Map)} methods. Use it at our own risk!
- * </strong>
- *
- * <p>This adapter only supports the CoinbasePro <a href="https://docs.pro.coinbase.com/#api">REST
- * API</a>. The design of the API and documentation is excellent.
- *
- * <p>The adapter currently only supports <a
- * href="https://docs.pro.coinbase.com/#place-a-new-order">Limit Orders</a>. It was originally
- * developed and tested for BTC-GBP market, but it should work for BTC-USD or BTC-EUR.
- *
- * <p>Exchange fees are loaded from the exchange.yaml file on startup; they are not fetched from the
- * exchange at runtime as the CoinbasePro REST API does not support this. The fees are used across
- * all markets. Make sure you keep an eye on the <a
- * href="https://docs.pro.coinbase.com/#fees">exchange fees</a> and update the config accordingly.
- *
- * <p>NOTE: CoinbasePro requires all price values to be limited to 2 decimal places when creating
- * orders. This adapter truncates any prices with more than 2 decimal places and rounds using {@link
- * java.math.RoundingMode#HALF_EVEN}, E.g. 250.176 would be sent to the exchange as 250.18.
- *
- * <p>The Exchange Adapter is <em>not</em> thread safe. It expects to be called using a single
- * thread in order to preserve trade execution order. The {@link URLConnection} achieves this by
- * blocking/waiting on the input stream (response) for each API call.
- *
- * <p>The {@link TradingApi} calls will throw a {@link ExchangeNetworkException} if a network error
- * occurs trying to connect to the exchange. A {@link TradingApiException} is thrown for
- * <em>all</em> other failures.
- *
- * @author davidhuertas
- * @since 1.0
- */
-@Log4j2
-public final class CoinbaseProExchangeAdapter extends AbstractExchangeAdapter
-    implements ExchangeAdapter {
-  private static final String PUBLIC_API_BASE_URL = "https://api.pro.coinbase.com/";
-  private static final String AUTHENTICATED_API_URL = PUBLIC_API_BASE_URL;
-
-  private static final String UNEXPECTED_ERROR_MSG =
-      "Unexpected error has occurred in COINBASE PRO Exchange Adapter. ";
-  private static final String UNEXPECTED_IO_ERROR_MSG =
-      "Failed to connect to Exchange due to unexpected IO error.";
-
-  private static final String PRODUCTS = "products/";
-  private static final String PRICE = "price";
-
-  private static final String PASSPHRASE_PROPERTY_NAME = "passphrase";
-  private static final String KEY_PROPERTY_NAME = "key";
-  private static final String SECRET_PROPERTY_NAME = "secret";
-
-  private static final String BUY_FEE_PROPERTY_NAME = "buy-fee";
-  private static final String SELL_FEE_PROPERTY_NAME = "sell-fee";
-  private static final String SERVER_TIME_BIAS_PROPERTY_NAME = "time-server-bias";
-
-  private BigDecimal buyFeePercentage;
-  private BigDecimal sellFeePercentage;
-  private Long timeServerBias;
-
-  private String passphrase = "";
-  private String key = "";
-  private String secret = "";
-
-  private Mac mac;
-  private boolean initializedMacAuthentication = false;
-
-  private Gson gson;
-
-  @Override
-  public void init(ExchangeConfig config) {
-    log.info("About to initialise COINBASE PRO ExchangeConfig: " + config);
-    setAuthenticationConfig(config);
-    setNetworkConfig(config);
-    setOtherConfig(config);
-
-    initSecureMessageLayer();
-    initGson();
-  }
-
-  // --------------------------------------------------------------------------
-  // COINBASE PRO API Calls adapted to the Trading API.
-  // See https://docs.pro.coinbase.com/#api
-  // --------------------------------------------------------------------------
-
-  @Override
-  public String createOrder(
-      String marketId, OrderType orderType, BigDecimal quantity, BigDecimal price)
-      throws TradingApiException, ExchangeNetworkException {
-    try {
-      /*
-       * Build Limit Order: https://docs.pro.coinbase.com/#place-a-new-order
-       *
-       * stp param optional           - (Self-trade prevention flag) defaults to 'dc' Decrease &
-       *                                Cancel
-       * post_only param optional     - defaults to 'false'
-       * time_in_force param optional - defaults to 'GTC' Good til Cancel
-       * client_oid param is optional - thia adapter does not use it.
-       */
-      final Map<String, String> params = createRequestParamMap();
-
-      if (orderType == OrderType.BUY) {
-        params.put("side", "buy");
-      } else if (orderType == OrderType.SELL) {
-        params.put("side", "sell");
-      } else {
-        final String errorMsg =
-            "Invalid order type: "
-                + orderType
-                + " - Can only be "
-                + OrderType.BUY.getStringValue()
-                + " or "
-                + OrderType.SELL.getStringValue();
-        log.error(errorMsg);
-        throw new IllegalArgumentException(errorMsg);
-      }
-
-      params.put("product_id", marketId);
-
-      // note we need to limit price to 2 decimal places else exchange will barf
-      params.put(PRICE, new DecimalFormat("#.##", getDecimalFormatSymbols()).format(price));
-
-      // note we need to limit size to 8 decimal places else exchange will barf
-      params.put(
-          "size", new DecimalFormat("#.########", getDecimalFormatSymbols()).format(quantity));
-
-      final ExchangeHttpResponse response =
-          sendAuthenticatedRequestToExchange("POST", "orders", params);
-      log.debug("Create Order response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProOrder createOrderResponse =
-            gson.fromJson(response.getPayload(), CoinbaseProOrder.class);
-        if (createOrderResponse != null
-            && (createOrderResponse.id != null && !createOrderResponse.id.isEmpty())) {
-          return createOrderResponse.id;
-        } else {
-          final String errorMsg = "Failed to place order on exchange. Error response: " + response;
-          log.error(errorMsg);
-          throw new TradingApiException(errorMsg);
-        }
-      } else {
-        final String errorMsg = "Failed to create order on exchange. Details: " + response;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  /*
-   * marketId is not needed for cancelling orders on this exchange.
-   */
-  @Override
-  public boolean cancelOrder(String orderId, String marketIdNotNeeded)
-      throws TradingApiException, ExchangeNetworkException {
-    try {
-      final ExchangeHttpResponse response =
-          sendAuthenticatedRequestToExchange("DELETE", "orders/" + orderId, null);
-
-      log.debug("Cancel Order response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        // 1 Nov 2017 - COINBASE PRO API no longer returns cancelled orderId in array payload;
-        // it returns [null]...
-        return true;
-      } else {
-        final String errorMsg = "Failed to cancel order on exchange. Details: " + response;
-        log.error(errorMsg);
-        return false;
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  @Override
-  public List<OpenOrder> getYourOpenOrders(String marketId)
-      throws TradingApiException, ExchangeNetworkException {
-    try {
-      // we use default request no-param call - only open or un-settled orders are returned.
-      // As soon as an order is no longer open and settled, it will no longer appear in the default
-      // request.
-      final ExchangeHttpResponse response =
-          sendAuthenticatedRequestToExchange("GET", "orders", null);
-
-      log.debug("Open Orders response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProOrder[] coinbaseProOpenOrders =
-            gson.fromJson(response.getPayload(), CoinbaseProOrder[].class);
-        final List<OpenOrder> ordersToReturn = new ArrayList<>();
-        for (final CoinbaseProOrder openOrder : coinbaseProOpenOrders) {
-
-          if (!marketId.equalsIgnoreCase(openOrder.productId)) {
-            continue;
-          }
-
-          OrderType orderType;
-          switch (openOrder.side) {
-            case "buy":
-              orderType = OrderType.BUY;
-              break;
-            case "sell":
-              orderType = OrderType.SELL;
-              break;
-            default:
-              throw new TradingApiException(
-                  "Unrecognised order type received in getYourOpenOrders(). Value: "
-                      + openOrder.side);
-          }
-
-          final OpenOrder order =
-              new OpenOrderImpl(
-                  openOrder.id,
-                  Date.from(Instant.parse(openOrder.createdAt)),
-                  marketId,
-                  orderType,
-                  openOrder.price,
-                  openOrder.size.subtract(
-                      openOrder.filledSize), // quantity remaining - not provided by COINBASE PRO
-                  openOrder.size, // orig quantity
-                  openOrder.price.multiply(openOrder.size) // total - not provided by COINBASE PRO
-                  );
-
-          ordersToReturn.add(order);
-        }
-        return ordersToReturn;
-      } else {
-        final String errorMsg =
-            "Failed to get your open orders from exchange. Details: " + response;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  @Override
-  public MarketOrderBook getMarketOrders(String marketId)
-      throws TradingApiException, ExchangeNetworkException {
-    try {
-      final Map<String, String> params = createRequestParamMap();
-      params.put("level", "2"); //  "2" = Top 50 bids and asks (aggregated)
-
-      final ExchangeHttpResponse response =
-          sendPublicRequestToExchange(PRODUCTS + marketId + "/book", params);
-
-      log.debug("Market Orders response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProBookWrapper orderBook =
-            gson.fromJson(response.getPayload(), CoinbaseProBookWrapper.class);
-
-        final List<MarketOrder> buyOrders = new ArrayList<>();
-        for (CoinbaseProMarketOrder coinbaseProBuyOrder : orderBook.bids) {
-          final MarketOrder buyOrder =
-              new MarketOrderImpl(
-                  OrderType.BUY,
-                  coinbaseProBuyOrder.get(0),
-                  coinbaseProBuyOrder.get(1),
-                  coinbaseProBuyOrder.get(0).multiply(coinbaseProBuyOrder.get(1)));
-          buyOrders.add(buyOrder);
-        }
-
-        final List<MarketOrder> sellOrders = new ArrayList<>();
-        for (CoinbaseProMarketOrder coinbaseProSellOrder : orderBook.asks) {
-          final MarketOrder sellOrder =
-              new MarketOrderImpl(
-                  OrderType.SELL,
-                  coinbaseProSellOrder.get(0),
-                  coinbaseProSellOrder.get(1),
-                  coinbaseProSellOrder.get(0).multiply(coinbaseProSellOrder.get(1)));
-          sellOrders.add(sellOrder);
-        }
-        return new MarketOrderBookImpl(marketId, sellOrders, buyOrders);
-
-      } else {
-        final String errorMsg =
-            "Failed to get market order book from exchange. Details: " + response;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  @Override
-  public BalanceInfo getBalanceInfo() throws TradingApiException, ExchangeNetworkException {
-    try {
-      final ExchangeHttpResponse response =
-          sendAuthenticatedRequestToExchange("GET", "accounts", null);
-
-      log.debug("Balance Info response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProAccount[] coinbaseProAccounts =
-            gson.fromJson(response.getPayload(), CoinbaseProAccount[].class);
-
-        final HashMap<String, BigDecimal> balancesAvailable = new HashMap<>();
-        final HashMap<String, BigDecimal> balancesOnHold = new HashMap<>();
-
-        for (final CoinbaseProAccount coinbaseProAccount : coinbaseProAccounts) {
-          balancesAvailable.put(coinbaseProAccount.currency, coinbaseProAccount.available);
-          balancesOnHold.put(coinbaseProAccount.currency, coinbaseProAccount.hold);
-        }
-        return new BalanceInfoImpl(balancesAvailable, balancesOnHold);
-      } else {
-        final String errorMsg =
-            "Failed to get your wallet balance info from exchange. Details: " + response;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  @Override
-  public BigDecimal getLatestMarketPrice(String marketId)
-      throws ExchangeNetworkException, TradingApiException {
-    try {
-      final ExchangeHttpResponse response =
-          sendPublicRequestToExchange(PRODUCTS + marketId + "/ticker", null);
-
-      log.debug("Latest Market Price response: " + response);
-
-      if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProTicker coinbaseProTicker =
-            gson.fromJson(response.getPayload(), CoinbaseProTicker.class);
-        return coinbaseProTicker.price;
-      } else {
-        final String errorMsg = "Failed to get market ticker from exchange. Details: " + response;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  /*
-   * COINBASE PRO does not provide API call for fetching % buy fee; it only provides the fee
-   * monetary value for a given order via e.g. /orders/<order-id> API call. We load the % fee
-   * statically from exchange.yaml file.
-   */
-  @Override
-  public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId) {
-    return buyFeePercentage;
-  }
-
-  /*
-   * COINBASE PRO does not provide API call for fetching % sell fee; it only provides the fee
-   * monetary value for a given order via e.g. /orders/<order-id> API call. We load the % fee
-   * statically from exchange.yaml file.
-   */
-  @Override
-  public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) {
-    return sellFeePercentage;
-  }
-
-  @Override
-  public String getImplName() {
-    return "COINBASE PRO REST API v1";
-  }
-
-  @Override
-  public Ticker getTicker(String marketId) throws ExchangeNetworkException, TradingApiException {
-    try {
-      final ExchangeHttpResponse tickerResponse =
-          sendPublicRequestToExchange(PRODUCTS + marketId + "/ticker", null);
-
-      log.debug("Ticker response: " + tickerResponse);
-
-      if (tickerResponse.getStatusCode() == HttpURLConnection.HTTP_OK) {
-        final CoinbaseProTicker coinbaseProTicker =
-            gson.fromJson(tickerResponse.getPayload(), CoinbaseProTicker.class);
-
-        final TickerImpl ticker =
-            new TickerImpl(
-                coinbaseProTicker.price,
-                coinbaseProTicker.bid,
-                coinbaseProTicker.ask,
-                null, // low,
-                null, // high,
-                null, // open,
-                coinbaseProTicker.volume,
-                null, // vwap - not supplied by COINBASE PRO
-                Date.from(Instant.parse(coinbaseProTicker.time)).getTime());
-
-        // Now we need to call the stats operation to get the 24hr indicators
-        final ExchangeHttpResponse statsResponse =
-            sendPublicRequestToExchange(PRODUCTS + marketId + "/stats", null);
-
-        log.debug("Stats response: " + statsResponse);
-
-        if (statsResponse.getStatusCode() == HttpURLConnection.HTTP_OK) {
-          final CoinbaseProStats coinbaseProStats =
-              gson.fromJson(statsResponse.getPayload(), CoinbaseProStats.class);
-          ticker.setLow(coinbaseProStats.low);
-          ticker.setHigh(coinbaseProStats.high);
-          ticker.setOpen(coinbaseProStats.open);
-        } else {
-          final String errorMsg = "Failed to get stats from exchange. Details: " + statsResponse;
-          log.error(errorMsg);
-          throw new TradingApiException(errorMsg);
-        }
-
-        return ticker;
-
-      } else {
-        final String errorMsg =
-            "Failed to get market ticker from exchange. Details: " + tickerResponse;
-        log.error(errorMsg);
-        throw new TradingApiException(errorMsg);
-      }
-
-    } catch (ExchangeNetworkException | TradingApiException e) {
-      throw e;
-
-    } catch (Exception e) {
-      log.error(UNEXPECTED_ERROR_MSG, e);
-      throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
-    }
-  }
-
-  // --------------------------------------------------------------------------
-  //  GSON classes for JSON responses.
-  //  See https://docs.pro.coinbase.com/#api
-  // --------------------------------------------------------------------------
-
-  /**
-   * GSON class for COINBASE PRO '/orders' API call response.
-   *
-   * <p>There are other critters in here different to <a
-   * href="https://docs.pro.coinbase.com/#list-orders">what is spec'd</a>.
-   */
-  private static class CoinbaseProOrder {
-
-    String id;
-    BigDecimal price;
-    BigDecimal size;
-
-    @SerializedName("product_id")
-    String productId; // e.g. "BTC-GBP", "BTC-USD"
-
-    String side; // "buy" or "sell"
-    String stp; // Self-Trade Prevention flag, e.g. "dc"
-    String type; // order type, e.g. "limit"
-
-    @SerializedName("time_in_force")
-    String timeInForce; // e.g. "GTC" (Good Til Cancelled)
-
-    @SerializedName("post_only")
-    boolean postOnly; // shows in book + provides exchange liquidity, but will not execute
-
-    @SerializedName("created_at")
-    String createdAt; // e.g. "2014-11-14 06:39:55.189376+00"
-
-    @SerializedName("fill_fees")
-    BigDecimal fillFees;
-
-    @SerializedName("filled_size")
-    BigDecimal filledSize;
-
-    String status; // e.g. "open"
-    boolean settled;
-
-    @Override
-    public String toString() {
-      return MoreObjects.toStringHelper(this)
-          .add("id", id)
-          .add(PRICE, price)
-          .add("size", size)
-          .add("productId", productId)
-          .add("side", side)
-          .add("stp", stp)
-          .add("type", type)
-          .add("timeInForce", timeInForce)
-          .add("postOnly", postOnly)
-          .add("createdAt", createdAt)
-          .add("fillFees", fillFees)
-          .add("filledSize", filledSize)
-          .add("status", status)
-          .add("settled", settled)
-          .toString();
-    }
-  }
-
-  /** GSON class for COINBASE PRO '/products/{marketId}/book' API call response. */
-  private static class CoinbaseProBookWrapper {
-
-    long sequence;
-    List<CoinbaseProMarketOrder> bids;
-    List<CoinbaseProMarketOrder> asks;
-
-    @Override
-    public String toString() {
-      return MoreObjects.toStringHelper(this)
-          .add("sequence", sequence)
-          .add("bids", bids)
-          .add("asks", asks)
-          .toString();
-    }
-  }
-
-  /**
-   * GSON class for holding Market Orders. First element in array is price, second element is
-   * amount, third is number of orders.
-   */
-  private static class CoinbaseProMarketOrder extends ArrayList<BigDecimal> {
-
-    @Serial private static final long serialVersionUID = -4919711220797077759L;
-  }
-
-  /** GSON class for COINBASE PRO '/products/{marketId}/ticker' API call response. */
-  private static class CoinbaseProTicker {
-
-    @SerializedName("trade_id")
-    long tradeId;
-
-    BigDecimal price;
-    BigDecimal size;
-    BigDecimal bid;
-    BigDecimal ask;
-    BigDecimal volume;
-    String time; // e.g. "2015-10-14T19:19:36.604735Z"
-
-    @Override
-    public String toString() {
-      return MoreObjects.toStringHelper(this)
-          .add("tradeId", tradeId)
-          .add(PRICE, price)
-          .add("size", size)
-          .add("bid", bid)
-          .add("ask", ask)
-          .add("volume", volume)
-          .add("time", time)
-          .toString();
-    }
-  }
-
-  /** GSON class for COINBASE PRO '/products/&ltproduct-id&gt/stats' API call response. */
-  private static class CoinbaseProStats {
-
-    BigDecimal open;
-    BigDecimal high;
-    BigDecimal low;
-    BigDecimal volume;
-    BigDecimal last;
-
-    @SerializedName("volume_30day")
-    String volume30Day;
-
-    @Override
-    public String toString() {
-      return MoreObjects.toStringHelper(this)
-          .add("open", open)
-          .add("high", high)
-          .add("low", low)
-          .add("volume", volume)
-          .add("last", last)
-          .add("volume30Day", volume30Day)
-          .toString();
-    }
-  }
-
-  /** GSON class for COINBASE PRO '/accounts' API call response. */
-  private static class CoinbaseProAccount {
-
-    String id;
-    String currency;
-    BigDecimal balance; // e.g. "0.0000000000000000"
-    BigDecimal hold;
-    BigDecimal available;
-
-    @SerializedName("profile_id") // no idea what this is?!
-    String profileId;
-
-    @Override
-    public String toString() {
-      return MoreObjects.toStringHelper(this)
-          .add("id", id)
-          .add("currency", currency)
-          .add("balance", balance)
-          .add("hold", hold)
-          .add("available", available)
-          .add("profileId", profileId)
-          .toString();
-    }
-  }
-
-  // --------------------------------------------------------------------------
-  //  Transport layer methods
-  // --------------------------------------------------------------------------
-
-  private ExchangeHttpResponse sendPublicRequestToExchange(
-      String apiMethod, Map<String, String> params)
-      throws ExchangeNetworkException, TradingApiException {
-    if (params == null) {
-      params = createRequestParamMap(); // no params, so empty query string
-    }
-
-    // Request headers required by Exchange
-    final Map<String, String> requestHeaders = new HashMap<>();
-
-    try {
-
-      final StringBuilder queryString = new StringBuilder();
-      if (params.size() > 0) {
-
-        queryString.append("?");
-
-        for (final Map.Entry<String, String> param : params.entrySet()) {
-          if (queryString.length() > 1) {
-            queryString.append("&");
-          }
-          queryString.append(param.getKey());
-          queryString.append("=");
-          queryString.append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8));
-        }
-
-        requestHeaders.put("Content-Type", "application/x-www-form-urlencoded");
-      }
-
-      final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod + queryString);
-      return makeNetworkRequest(url, "GET", null, requestHeaders);
-
-    } catch (MalformedURLException e) {
-      final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
-      log.error(errorMsg, e);
-      throw new TradingApiException(errorMsg, e);
-    }
-  }
-
-  /*
-   * Makes an authenticated API call to the COINBASE PRO exchange.
-   *
-   * The COINBASE PRO authentication process is complex, but well documented:
-   * https://docs.pro.coinbase.com/#creating-a-request
-   *
-   * All REST requests must contain the following headers:
-   *
-   * CB-ACCESS-KEY          The api key as a string.
-   * CB-ACCESS-SIGN         The base64-encoded signature (see Signing a Message).
-   * CB-ACCESS-TIMESTAMP    A timestamp for your request.
-   * CB-ACCESS-PASSPHRASE   The passphrase you specified when creating the API key.
-   *
-   * The CB-ACCESS-TIMESTAMP header MUST be number of seconds since Unix Epoch in UTC.
-   * Decimal values are allowed.
-   *
-   * Your timestamp must be within 30 seconds of the api service time or your request will be
-   * considered expired and rejected. We recommend using the time endpoint to query for the API
-   * server time if you believe there many be time skew between your server and the API servers.
-   *
-   * All request bodies should have content type application/json and be valid JSON.
-   *
-   * The CB-ACCESS-SIGN header is generated by creating a sha256 HMAC using the base64-decoded
-   * secret key on the prehash string:
-   *
-   * timestamp + method + requestPath + body (where + represents string concatenation)
-   *
-   * and base64-encode the output.
-   * The timestamp value is the same as the CB-ACCESS-TIMESTAMP header.
-   *
-   * The body is the request body string or omitted if there is no request body
-   * (typically for GET requests).
-   *
-   * The method should be UPPER CASE.
-   *
-   * Remember to first base64-decode the alphanumeric secret string (resulting in 64 bytes) before
-   * using it as the key for HMAC. Also, base64-encode the digest output before sending in the
-   * header.
-   */
-  private ExchangeHttpResponse sendAuthenticatedRequestToExchange(
-      String httpMethod, String apiMethod, Map<String, String> params)
-      throws ExchangeNetworkException, TradingApiException {
-
-    if (!initializedMacAuthentication) {
-      final String errorMsg = "MAC Message security layer has not been initialized.";
-      log.error(errorMsg);
-      throw new IllegalStateException(errorMsg);
-    }
-
-    try {
-      if (params == null) {
-        // create empty map for non-param API calls
-        params = createRequestParamMap();
-      }
-
-      // Build the request
-      final String invocationUrl;
-      String requestBody = "";
-
-      switch (httpMethod) {
-        case "GET":
-          log.debug("Building secure GET request...");
-          // Build (optional) query param string
-          final StringBuilder queryParamBuilder = new StringBuilder();
-          for (final Map.Entry<String, String> param : params.entrySet()) {
-            if (queryParamBuilder.length() > 0) {
-              queryParamBuilder.append("&");
-            }
-            queryParamBuilder.append(param.getKey());
-            queryParamBuilder.append("=");
-            queryParamBuilder.append(param.getValue());
-          }
-
-          final String queryParams = queryParamBuilder.toString();
-          log.debug("Query param string: " + queryParams);
-
-          if (params.isEmpty()) {
-            invocationUrl = AUTHENTICATED_API_URL + apiMethod;
-          } else {
-            invocationUrl = AUTHENTICATED_API_URL + apiMethod + "?" + queryParams;
-          }
-          break;
-
-        case "POST":
-          log.debug("Building secure POST request...");
-          invocationUrl = AUTHENTICATED_API_URL + apiMethod;
-          requestBody = gson.toJson(params);
-          break;
-
-        case "DELETE":
-          log.debug("Building secure DELETE request...");
-          invocationUrl = AUTHENTICATED_API_URL + apiMethod;
-          break;
-
-        default:
-          throw new IllegalArgumentException(
-              "Don't know how to build secure [" + httpMethod + "] request!");
-      }
-
-      // Get UNIX EPOCH in secs and add the time-server bias
-      final long timeServer = Instant.now().getEpochSecond() + timeServerBias;
-      final String timestamp = Long.toString(timeServer);
-      log.debug("Server UNIX EPOCH in seconds: " + timestamp);
-
-      // Build the signature string: timestamp + method + requestPath + body
-      final String signatureBuilder =
-          timestamp + httpMethod.toUpperCase() + "/" + apiMethod + requestBody;
-
-      // Sign the signature string and Base64 encode it
-      mac.reset();
-      mac.update(signatureBuilder.getBytes(StandardCharsets.UTF_8));
-      final String signature = DatatypeConverter.printBase64Binary(mac.doFinal());
-
-      // Request headers required by Exchange
-      final Map<String, String> requestHeaders = createHeaderParamMap();
-      requestHeaders.put("Content-Type", "application/json");
-      requestHeaders.put("CB-ACCESS-KEY", key);
-      requestHeaders.put("CB-ACCESS-SIGN", signature);
-      requestHeaders.put("CB-ACCESS-TIMESTAMP", timestamp);
-      requestHeaders.put("CB-ACCESS-PASSPHRASE", passphrase);
-
-      final URL url = new URL(invocationUrl);
-      return makeNetworkRequest(url, httpMethod, requestBody, requestHeaders);
-
-    } catch (MalformedURLException e) {
-      final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
-      log.error(errorMsg, e);
-      throw new TradingApiException(errorMsg, e);
-    }
-  }
-
-  /*
-   * Initialises the secure messaging layer.
-   * Sets up the MAC to safeguard the data we send to the exchange.
-   * Used to encrypt the hash of the entire message with the private key to ensure message
-   * integrity. We fail hard n fast if any of this stuff blows.
-   */
-  private void initSecureMessageLayer() {
-    try {
-      // COINBASE PRO secret is in Base64, so we must decode it first.
-      final byte[] decodedBase64Secret = DatatypeConverter.parseBase64Binary(secret);
-
-      final SecretKeySpec keyspec = new SecretKeySpec(decodedBase64Secret, "HmacSHA256");
-      mac = Mac.getInstance("HmacSHA256");
-      mac.init(keyspec);
-      initializedMacAuthentication = true;
-    } catch (NoSuchAlgorithmException e) {
-      final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA256 installed?";
-      log.error(errorMsg, e);
-      throw new IllegalStateException(errorMsg, e);
-    } catch (InvalidKeyException e) {
-      final String errorMsg = "Failed to setup MAC security. Secret key seems invalid!";
-      log.error(errorMsg, e);
-      throw new IllegalArgumentException(errorMsg, e);
-    }
-  }
-
-  // --------------------------------------------------------------------------
-  //  Config methods
-  // --------------------------------------------------------------------------
-
-  private void setAuthenticationConfig(ExchangeConfig exchangeConfig) {
-    final AuthenticationConfig authenticationConfig = getAuthenticationConfig(exchangeConfig);
-    passphrase = getAuthenticationConfigItem(authenticationConfig, PASSPHRASE_PROPERTY_NAME);
-    key = getAuthenticationConfigItem(authenticationConfig, KEY_PROPERTY_NAME);
-    secret = getAuthenticationConfigItem(authenticationConfig, SECRET_PROPERTY_NAME);
-  }
-
-  private void setOtherConfig(ExchangeConfig exchangeConfig) {
-    final OtherConfig otherConfig = getOtherConfig(exchangeConfig);
-
-    final String buyFeeInConfig = getOtherConfigItem(otherConfig, BUY_FEE_PROPERTY_NAME);
-    buyFeePercentage =
-        new BigDecimal(buyFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP);
-    log.info("Buy fee % in BigDecimal format: " + buyFeePercentage);
-
-    final String sellFeeInConfig = getOtherConfigItem(otherConfig, SELL_FEE_PROPERTY_NAME);
-    sellFeePercentage =
-        new BigDecimal(sellFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP);
-    log.info("Sell fee % in BigDecimal format: " + sellFeePercentage);
-
-    final String serverTimeBiasInConfig =
-        getOtherConfigItem(otherConfig, SERVER_TIME_BIAS_PROPERTY_NAME);
-    timeServerBias = Long.parseLong(serverTimeBiasInConfig);
-    log.info("Time server bias in long format: " + timeServerBias);
-  }
-
-  // --------------------------------------------------------------------------
-  //  Util methods
-  // --------------------------------------------------------------------------
-
-  private void initGson() {
-    final GsonBuilder gsonBuilder = new GsonBuilder();
-    gson = gsonBuilder.create();
-  }
-
-  /*
-   * Hack for unit-testing request params passed to transport layer.
-   */
-  private Map<String, String> createRequestParamMap() {
-    return new HashMap<>();
-  }
-
-  /*
-   * Hack for unit-testing header params passed to transport layer.
-   */
-  private Map<String, String> createHeaderParamMap() {
-    return new HashMap<>();
-  }
-
-  /*
-   * Hack for unit-testing transport layer.
-   */
-  private ExchangeHttpResponse makeNetworkRequest(
-      URL url, String httpMethod, String postData, Map<String, String> requestHeaders)
-      throws TradingApiException, ExchangeNetworkException {
-    return super.sendNetworkRequest(url, httpMethod, postData, requestHeaders);
-  }
-}
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/GeminiExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/GeminiExchangeAdapter.java
index 39cbe823d..156859823 100644
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/GeminiExchangeAdapter.java
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/GeminiExchangeAdapter.java
@@ -43,10 +43,13 @@
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.annotations.SerializedName;
+import jakarta.xml.bind.DatatypeConverter;
 import java.io.Serial;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
@@ -61,7 +64,6 @@
 import java.util.Map;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-import javax.xml.bind.DatatypeConverter;
 import lombok.extern.log4j.Log4j2;
 
 /**
@@ -160,6 +162,11 @@ public String getStringValue() {
 
   private Gson gson;
 
+  /** Constructs the Exchange Adapter. */
+  public GeminiExchangeAdapter() {
+    // No extra init.
+  }
+
   @Override
   public void init(ExchangeConfig config) {
     log.info("About to initialise Gemini ExchangeConfig: " + config);
@@ -639,10 +646,10 @@ public String toString() {
   private ExchangeHttpResponse sendPublicRequestToExchange(String apiMethod)
       throws ExchangeNetworkException, TradingApiException {
     try {
-      final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod);
+      final URL url = new URI(PUBLIC_API_BASE_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "GET", null, createRequestParamMap());
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
@@ -734,10 +741,10 @@ private ExchangeHttpResponse sendAuthenticatedRequestToExchange(
       // payload is JSON for this exchange
       requestHeaders.put("Content-Type", "application/json");
 
-      final URL url = new URL(AUTHENTICATED_API_URL + apiMethod);
+      final URL url = new URI(AUTHENTICATED_API_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "POST", paramsInJson, requestHeaders);
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/KrakenExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/KrakenExchangeAdapter.java
index f3c9874a8..03b239eff 100644
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/KrakenExchangeAdapter.java
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/KrakenExchangeAdapter.java
@@ -61,6 +61,8 @@
 import java.math.RoundingMode;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLEncoder;
@@ -188,9 +190,13 @@ public final class KrakenExchangeAdapter extends AbstractExchangeAdapter
 
   private Mac mac;
   private boolean initializedMacAuthentication = false;
-
   private Gson gson;
 
+  /** Constructs the Exchange Adapter. */
+  public KrakenExchangeAdapter() {
+    // No extra init.
+  }
+
   @Override
   public void init(ExchangeConfig config) {
     log.info("About to initialise Kraken ExchangeConfig: " + config);
@@ -939,10 +945,10 @@ private ExchangeHttpResponse sendPublicRequestToExchange(
         requestHeaders.put("Content-Type", "application/x-www-form-urlencoded");
       }
 
-      final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod + queryString);
+      final URL url = new URI(PUBLIC_API_BASE_URL + apiMethod + queryString).toURL();
       return makeNetworkRequest(url, "GET", null, requestHeaders);
 
-    } catch (MalformedURLException e) {
+    } catch (MalformedURLException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
@@ -1024,10 +1030,10 @@ private ExchangeHttpResponse sendAuthenticatedRequestToExchange(
       requestHeaders.put("API-Key", key);
       requestHeaders.put("API-Sign", signature);
 
-      final URL url = new URL(AUTHENTICATED_API_URL + apiMethod);
+      final URL url = new URI(AUTHENTICATED_API_URL + apiMethod).toURL();
       return makeNetworkRequest(url, "POST", postData.toString(), requestHeaders);
 
-    } catch (MalformedURLException | NoSuchAlgorithmException e) {
+    } catch (MalformedURLException | NoSuchAlgorithmException | URISyntaxException e) {
       final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
       log.error(errorMsg, e);
       throw new TradingApiException(errorMsg, e);
diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TryModeExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TryModeExchangeAdapter.java
index d041a10ad..77092d832 100644
--- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TryModeExchangeAdapter.java
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TryModeExchangeAdapter.java
@@ -94,6 +94,11 @@ public class TryModeExchangeAdapter extends AbstractExchangeAdapter implements E
   private OpenOrder currentOpenOrder;
   private boolean isOpenOrderCheckReentering;
 
+  /** Constructs the Exchange Adapter. */
+  public TryModeExchangeAdapter() {
+    // No extra init.
+  }
+
   @Override
   public void init(ExchangeConfig config) {
     log.info("About to initialise try-mode adapter with the following exchange config: " + config);
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json
deleted file mode 100644
index ed4942c77..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
-  {
-    "id": "7262ae65-fbc3-4d11-b959-25f1befc7a21",
-    "currency": "BTC",
-    "balance": "200.000000000000009",
-    "hold": "100.0000000000000005",
-    "available": "100.0000000000000004",
-    "profile_id": "5624aa4a-85f6-462f-a4d9-bac80ea184c7"
-  },
-  {
-    "id": "de2b5848-533e-4374-9004-cd8ea985f8cc",
-    "currency": "GBP",
-    "balance": "1000.0000000000000003",
-    "hold": "499.9900000000000002",
-    "available": "501.0100000000000001",
-    "profile_id": "5624aa4a-85f6-462f-a4d9-bac80ea184c2"
-  },
-  {
-    "id": "864881f5-bd18-4970-b254-e7cd2fef6b4d",
-    "currency": "EUR",
-    "balance": "0.0000000000000000",
-    "hold": "0.0000000000000000",
-    "available": "0.0000000000000000",
-    "profile_id": "5622aa4a-87f6-462f-c4d9-bac80ef184d0"
-  }
-]
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json
deleted file mode 100644
index 72ff024c1..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json
+++ /dev/null
@@ -1,507 +0,0 @@
-{
-  "sequence": 95643108,
-  "bids": [
-    [
-      "165.87",
-      "16.2373",
-      10
-    ],
-    [
-      "165.86",
-      "21.645",
-      3
-    ],
-    [
-      "165.85",
-      "0.357",
-      3
-    ],
-    [
-      "165.84",
-      "15.4652",
-      1
-    ],
-    [
-      "165.83",
-      "0.08",
-      2
-    ],
-    [
-      "165.82",
-      "0.207",
-      4
-    ],
-    [
-      "165.8",
-      "3.46334",
-      1
-    ],
-    [
-      "165.78",
-      "43.355346",
-      6
-    ],
-    [
-      "165.77",
-      "3.157",
-      2
-    ],
-    [
-      "165.76",
-      "32.6",
-      4
-    ],
-    [
-      "165.75",
-      "0.02042",
-      1
-    ],
-    [
-      "165.74",
-      "0.3264",
-      3
-    ],
-    [
-      "165.73",
-      "0.902",
-      4
-    ],
-    [
-      "165.72",
-      "0.07",
-      1
-    ],
-    [
-      "165.71",
-      "0.38466",
-      6
-    ],
-    [
-      "165.7",
-      "23.32458",
-      12
-    ],
-    [
-      "165.69",
-      "13.6215",
-      1
-    ],
-    [
-      "165.68",
-      "0.08",
-      2
-    ],
-    [
-      "165.66",
-      "0.03",
-      1
-    ],
-    [
-      "165.65",
-      "0.02",
-      2
-    ],
-    [
-      "165.63",
-      "0.051",
-      1
-    ],
-    [
-      "165.52",
-      "18.39",
-      1
-    ],
-    [
-      "165.31",
-      "0.016",
-      1
-    ],
-    [
-      "165.3",
-      "0.0183",
-      1
-    ],
-    [
-      "165.28",
-      "0.088",
-      1
-    ],
-    [
-      "165.26",
-      "0.053",
-      2
-    ],
-    [
-      "165.24",
-      "0.02",
-      1
-    ],
-    [
-      "165.23",
-      "3.65",
-      3
-    ],
-    [
-      "165.22",
-      "0.01",
-      1
-    ],
-    [
-      "165.18",
-      "3.3559",
-      2
-    ],
-    [
-      "165.16",
-      "0.031",
-      1
-    ],
-    [
-      "165.12",
-      "2.01",
-      1
-    ],
-    [
-      "165.07",
-      "3.15",
-      1
-    ],
-    [
-      "165",
-      "2.5",
-      3
-    ],
-    [
-      "164.91",
-      "3.5",
-      1
-    ],
-    [
-      "164.62",
-      "3.64",
-      1
-    ],
-    [
-      "164.56",
-      "1.676",
-      1
-    ],
-    [
-      "164.5",
-      "1.66",
-      1
-    ],
-    [
-      "164.49",
-      "7.82071",
-      1
-    ],
-    [
-      "164.44",
-      "3",
-      1
-    ],
-    [
-      "164.38",
-      "2.06",
-      1
-    ],
-    [
-      "164.2",
-      "1.21",
-      1
-    ],
-    [
-      "164.13",
-      "0.02",
-      1
-    ],
-    [
-      "164.05",
-      "1.7",
-      1
-    ],
-    [
-      "164.03",
-      "3.906",
-      1
-    ],
-    [
-      "163.97",
-      "0.6281",
-      1
-    ],
-    [
-      "163.91",
-      "2.6",
-      1
-    ],
-    [
-      "163.84",
-      "4.7602",
-      1
-    ],
-    [
-      "163.75",
-      "2.314",
-      1
-    ],
-    [
-      "163.73",
-      "0.3",
-      1
-    ]
-  ],
-  "asks": [
-    [
-      "165.96",
-      "24.31",
-      1
-    ],
-    [
-      "166.05",
-      "0.01",
-      1
-    ],
-    [
-      "166.08",
-      "0.027",
-      1
-    ],
-    [
-      "166.1",
-      "3.98718",
-      1
-    ],
-    [
-      "166.12",
-      "0.0815",
-      2
-    ],
-    [
-      "166.13",
-      "0.1404",
-      2
-    ],
-    [
-      "166.14",
-      "0.108",
-      3
-    ],
-    [
-      "166.16",
-      "0.784",
-      3
-    ],
-    [
-      "166.17",
-      "1.41472",
-      3
-    ],
-    [
-      "166.18",
-      "1.3774",
-      5
-    ],
-    [
-      "166.19",
-      "0.3284",
-      6
-    ],
-    [
-      "166.2",
-      "0.02",
-      1
-    ],
-    [
-      "166.21",
-      "0.1",
-      1
-    ],
-    [
-      "166.22",
-      "0.203",
-      6
-    ],
-    [
-      "166.23",
-      "0.152",
-      2
-    ],
-    [
-      "166.24",
-      "3.0575",
-      11
-    ],
-    [
-      "166.26",
-      "0.0656",
-      2
-    ],
-    [
-      "166.27",
-      "0.01",
-      1
-    ],
-    [
-      "166.29",
-      "17.43",
-      2
-    ],
-    [
-      "166.3",
-      "0.86",
-      1
-    ],
-    [
-      "166.33",
-      "0.6327395",
-      10
-    ],
-    [
-      "166.46",
-      "132.536",
-      5
-    ],
-    [
-      "166.91",
-      "0.19",
-      1
-    ],
-    [
-      "167.37",
-      "0.13",
-      1
-    ],
-    [
-      "167.6",
-      "2.214",
-      1
-    ],
-    [
-      "167.63",
-      "0.2142",
-      1
-    ],
-    [
-      "167.66",
-      "2.64",
-      1
-    ],
-    [
-      "167.72",
-      "3.12",
-      1
-    ],
-    [
-      "167.76",
-      "0.01",
-      1
-    ],
-    [
-      "167.78",
-      "4.2",
-      2
-    ],
-    [
-      "167.88",
-      "4.387",
-      1
-    ],
-    [
-      "167.98",
-      "4.1482",
-      1
-    ],
-    [
-      "168.08",
-      "3.234",
-      1
-    ],
-    [
-      "168.1",
-      "1.8",
-      1
-    ],
-    [
-      "168.54",
-      "0.013",
-      1
-    ],
-    [
-      "170.3",
-      "0.026",
-      1
-    ],
-    [
-      "171.16",
-      "0.8",
-      1
-    ],
-    [
-      "171.25",
-      "0.11",
-      1
-    ],
-    [
-      "171.38",
-      "0.051",
-      1
-    ],
-    [
-      "171.55",
-      "0.0138",
-      1
-    ],
-    [
-      "171.85",
-      "0.1",
-      1
-    ],
-    [
-      "174.24",
-      "3",
-      1
-    ],
-    [
-      "175.11",
-      "0.017",
-      1
-    ],
-    [
-      "179.29",
-      "0.57",
-      1
-    ],
-    [
-      "182.58",
-      "0.09",
-      1
-    ],
-    [
-      "183.73",
-      "0.2",
-      1
-    ],
-    [
-      "186.28",
-      "0.3",
-      1
-    ],
-    [
-      "186.39",
-      "0.39",
-      1
-    ],
-    [
-      "186.65",
-      "8",
-      1
-    ],
-    [
-      "186.83",
-      "1.25",
-      1
-    ]
-  ]
-}
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json
deleted file mode 100644
index d390bd800..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json
+++ /dev/null
@@ -1 +0,0 @@
-["3ecf7a12-fc89-4d3d-baef-f158f80b3bd3"]
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json
deleted file mode 100644
index 1c47f17cb..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "id": "193d2ad9-e671-4d66-9211-7f75f6380231",
-  "price": "280.18000000",
-  "size": "0.01000000",
-  "product_id": "BTC-GBP",
-  "side": "buy",
-  "stp": "dc",
-  "type": "limit",
-  "time_in_force": "GTC",
-  "post_only": false,
-  "created_at": "2015-10-17T14:48:18.873Z",
-  "fill_fees": "0.0000000000000000",
-  "filled_size": "0.00000000",
-  "status": "pending",
-  "settled": false
-}
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json
deleted file mode 100644
index 2f244ec18..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "id": "693d7ad9-e671-4d66-9911-7f75f6380134",
-  "price": "290.18000000",
-  "size": "0.01000000",
-  "product_id": "BTC-GBP",
-  "side": "sell",
-  "stp": "dc",
-  "type": "limit",
-  "time_in_force": "GTC",
-  "post_only": false,
-  "created_at": "2015-10-17T14:43:18.873Z",
-  "fill_fees": "0.0000000000000000",
-  "filled_size": "0.00000000",
-  "status": "pending",
-  "settled": false
-}
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json
deleted file mode 100644
index 14e56d03f..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json
+++ /dev/null
@@ -1,66 +0,0 @@
-[
-  {
-    "id": "cdad7602-f290-41e5-a64d-42a1a20fd02",
-    "price": "275.00000000",
-    "size": "0.01000000",
-    "product_id": "BTC-GBP",
-    "side": "sell",
-    "stp": "dc",
-    "type": "limit",
-    "time_in_force": "GTC",
-    "post_only": false,
-    "created_at": "2015-10-15T21:10:38.193Z",
-    "fill_fees": "0.0000000000000000",
-    "filled_size": "0.00500000",
-    "status": "open",
-    "settled": false
-  },
-  {
-    "id": "09cac657-df6c-40ef-97b9-4e64b181dec1",
-    "price": "270.00000000",
-    "size": "0.01000000",
-    "product_id": "BTC-GBP",
-    "side": "sell",
-    "stp": "dc",
-    "type": "limit",
-    "time_in_force": "GTC",
-    "post_only": false,
-    "created_at": "2015-10-15T21:10:10.569Z",
-    "fill_fees": "0.0000000000000000",
-    "filled_size": "0.00000000",
-    "status": "open",
-    "settled": false
-  },
-  {
-    "id": "09cac657-df6c-10ef-97b9-4e64b181dec1",
-    "price": "2001.02",
-    "size": "0.01000000",
-    "product_id": "BTC-USD",
-    "side": "sell",
-    "stp": "dc",
-    "type": "limit",
-    "time_in_force": "GTC",
-    "post_only": false,
-    "created_at": "2015-10-15T21:10:10.569Z",
-    "fill_fees": "0.0000000000000000",
-    "filled_size": "0.00000000",
-    "status": "open",
-    "settled": false
-  },
-  {
-    "id": "09ca1657-df6c-10ef-97b9-4e64b181dec1",
-    "price": "341.42000000",
-    "size": "2.01000000",
-    "product_id": "ETH-USD",
-    "side": "sell",
-    "stp": "dc",
-    "type": "limit",
-    "time_in_force": "GTC",
-    "post_only": false,
-    "created_at": "2015-10-15T21:10:10.569Z",
-    "fill_fees": "0.0000000000000000",
-    "filled_size": "0.00000000",
-    "status": "open",
-    "settled": false
-  }
-]
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json
deleted file mode 100644
index 755d7b916..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "open": "13609.53000000",
-  "high": "14899.00000000",
-  "low": "13409.97000000",
-  "volume": "607.54445656",
-  "last": "14744.81000000",
-  "volume_30day": "22412.37849136"
-}
\ No newline at end of file
diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json
deleted file mode 100644
index 265129f79..000000000
--- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "trade_id": 29582,
-  "price": "14744.9",
-  "size": "2.6108",
-  "bid":"14744.8",
-  "ask":"14744.81",
-  "volume": "607.54445656",
-  "time": "2017-10-14T19:19:36.604735Z"
-}
diff --git a/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java b/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java
deleted file mode 100644
index 3f6c1875c..000000000
--- a/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java
+++ /dev/null
@@ -1,1224 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2015 Gareth Jon Lynch
- * Copyright (c) 2019 David Huertas
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- * the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-package com.gazbert.bxbot.exchanges;
-
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.anyString;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import com.gazbert.bxbot.exchange.api.AuthenticationConfig;
-import com.gazbert.bxbot.exchange.api.ExchangeAdapter;
-import com.gazbert.bxbot.exchange.api.ExchangeConfig;
-import com.gazbert.bxbot.exchange.api.NetworkConfig;
-import com.gazbert.bxbot.exchange.api.OtherConfig;
-import com.gazbert.bxbot.trading.api.BalanceInfo;
-import com.gazbert.bxbot.trading.api.ExchangeNetworkException;
-import com.gazbert.bxbot.trading.api.MarketOrderBook;
-import com.gazbert.bxbot.trading.api.OpenOrder;
-import com.gazbert.bxbot.trading.api.OrderType;
-import com.gazbert.bxbot.trading.api.Ticker;
-import com.gazbert.bxbot.trading.api.TradingApiException;
-import com.google.gson.GsonBuilder;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.text.DecimalFormat;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.powermock.api.easymock.PowerMock;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-
-/**
- * Tests the behaviour of the COINBASE PRO Exchange Adapter.
- *
- * @author davidhuertas
- */
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({
-    "javax.crypto.*",
-    "javax.management.*",
-    "com.sun.org.apache.xerces.*",
-    "javax.xml.parsers.*",
-    "org.xml.sax.*",
-    "org.w3c.dom.*",
-    "javax.xml.datatype.*"
-})
-@PrepareForTest(CoinbaseProExchangeAdapter.class)
-public class TestCoinbaseProExchangeAdapter extends AbstractExchangeAdapterTest {
-
-  private static final String BOOK_JSON_RESPONSE = "./src/test/exchange-data/coinbasepro/book.json";
-  private static final String ORDERS_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/orders.json";
-  private static final String ACCOUNTS_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/accounts.json";
-  private static final String TICKER_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/ticker.json";
-  private static final String NEW_BUY_ORDER_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/new_buy_order.json";
-  private static final String NEW_SELL_ORDER_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/new_sell_order.json";
-  private static final String CANCEL_ORDER_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/cancel.json";
-  private static final String STATS_JSON_RESPONSE =
-      "./src/test/exchange-data/coinbasepro/stats.json";
-
-  private static final String MARKET_ID = "BTC-GBP";
-  private static final String ORDER_BOOK_DEPTH_LEVEL =
-      "2"; //  "2" = Top 50 bids and asks (aggregated)
-  private static final BigDecimal BUY_ORDER_PRICE = new BigDecimal("200.18");
-  private static final BigDecimal BUY_ORDER_QUANTITY = new BigDecimal("0.01");
-  private static final BigDecimal SELL_ORDER_PRICE = new BigDecimal("300.176");
-  private static final BigDecimal SELL_ORDER_QUANTITY = new BigDecimal("0.01");
-  private static final String ORDER_ID_TO_CANCEL = "3ecf7a12-fc89-4d3d-baef-f158f80b3bd3";
-
-  private static final String BOOK = "products/" + MARKET_ID + "/book";
-  private static final String ORDERS = "orders";
-  private static final String ACCOUNTS = "accounts";
-  private static final String TICKER = "products/" + MARKET_ID + "/ticker";
-  private static final String NEW_ORDER = "orders";
-  private static final String CANCEL_ORDER = "orders/" + ORDER_ID_TO_CANCEL;
-  private static final String STATS = "products/" + MARKET_ID + "/stats";
-
-  private static final String MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD = "createRequestParamMap";
-  private static final String MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD =
-      "sendAuthenticatedRequestToExchange";
-  private static final String MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD =
-      "sendPublicRequestToExchange";
-  private static final String MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD = "createHeaderParamMap";
-  private static final String MOCKED_MAKE_NETWORK_REQUEST_METHOD = "makeNetworkRequest";
-
-  private static final String PASSPHRASE = "lePassPhrase";
-  private static final String KEY = "key123";
-  private static final String SECRET = "notGonnaTellYa";
-  private static final List<Integer> nonFatalNetworkErrorCodes = Arrays.asList(502, 503, 504);
-  private static final List<String> nonFatalNetworkErrorMessages =
-      Arrays.asList(
-          "Connection refused",
-          "Connection reset",
-          "Remote host closed connection during handshake");
-
-  private static final String PUBLIC_API_BASE_URL = "https://api.pro.coinbase.com/";
-  private static final String AUTHENTICATED_API_URL = PUBLIC_API_BASE_URL;
-
-  private ExchangeConfig exchangeConfig;
-  private AuthenticationConfig authenticationConfig;
-  private NetworkConfig networkConfig;
-  private OtherConfig otherConfig;
-
-  /** Create some exchange config - the TradingEngine would normally do this. */
-  @Before
-  public void setupForEachTest() {
-    authenticationConfig = PowerMock.createMock(AuthenticationConfig.class);
-    expect(authenticationConfig.getItem("passphrase")).andReturn(PASSPHRASE);
-    expect(authenticationConfig.getItem("key")).andReturn(KEY);
-    expect(authenticationConfig.getItem("secret")).andReturn(SECRET);
-
-    networkConfig = PowerMock.createMock(NetworkConfig.class);
-    expect(networkConfig.getConnectionTimeout()).andReturn(30);
-    expect(networkConfig.getNonFatalErrorCodes()).andReturn(nonFatalNetworkErrorCodes);
-    expect(networkConfig.getNonFatalErrorMessages()).andReturn(nonFatalNetworkErrorMessages);
-
-    otherConfig = PowerMock.createMock(OtherConfig.class);
-    expect(otherConfig.getItem("buy-fee")).andReturn("0.25");
-    expect(otherConfig.getItem("sell-fee")).andReturn("0.25");
-    expect(otherConfig.getItem("time-server-bias")).andReturn("82");
-
-    exchangeConfig = PowerMock.createMock(ExchangeConfig.class);
-    expect(exchangeConfig.getAuthenticationConfig()).andReturn(authenticationConfig);
-    expect(exchangeConfig.getNetworkConfig()).andReturn(networkConfig);
-    expect(exchangeConfig.getOtherConfig()).andReturn(otherConfig);
-  }
-
-  // --------------------------------------------------------------------------
-  //  Create Orders tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  @SuppressWarnings("unchecked")
-  public void testCreateOrderToBuyIsSuccessful() throws Exception {
-    // Load the canned response from the exchange
-    final byte[] encoded = Files.readAllBytes(Paths.get(NEW_BUY_ORDER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    // Mock out param map, so we can assert the contents passed to the transport
-    // layer are what we expect.
-    final Map<String, String> requestParamMap = PowerMock.createMock(Map.class);
-    expect(
-            requestParamMap.put(
-                "size",
-                new DecimalFormat("#.########", getDecimalFormatSymbols())
-                    .format(BUY_ORDER_QUANTITY)))
-        .andStubReturn(null);
-    expect(
-            requestParamMap.put(
-                "price",
-                new DecimalFormat("#.##", getDecimalFormatSymbols()).format(BUY_ORDER_PRICE)))
-        .andStubReturn(null);
-    expect(requestParamMap.put("side", "buy")).andStubReturn(null);
-    expect(requestParamMap.put("product_id", MARKET_ID)).andStubReturn(null);
-
-    // Partial mock so we do not send stuff down the wire
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD);
-
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD)
-        .andReturn(requestParamMap);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("POST"),
-            eq(NEW_ORDER),
-            eq(requestParamMap))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final String orderId =
-        exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY, BUY_ORDER_QUANTITY, BUY_ORDER_PRICE);
-    assertEquals("193d2ad9-e671-4d66-9211-7f75f6380231", orderId);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test
-  @SuppressWarnings("unchecked")
-  public void testCreateOrderToSellIsSuccessful() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(NEW_SELL_ORDER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final Map<String, String> requestParamMap = PowerMock.createMock(Map.class);
-    expect(
-            requestParamMap.put(
-                "size",
-                new DecimalFormat("#.########", getDecimalFormatSymbols())
-                    .format(SELL_ORDER_QUANTITY)))
-        .andStubReturn(null);
-    expect(
-            requestParamMap.put(
-                "price",
-                new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE)))
-        .andStubReturn(null);
-    expect(requestParamMap.put("side", "sell")).andStubReturn(null);
-    expect(requestParamMap.put("product_id", MARKET_ID)).andStubReturn(null);
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD);
-
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD)
-        .andReturn(requestParamMap);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("POST"),
-            eq(NEW_ORDER),
-            eq(requestParamMap))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final String orderId =
-        exchangeAdapter.createOrder(
-            MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE);
-    assertEquals("693d7ad9-e671-4d66-9911-7f75f6380134", orderId);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testCreateOrderHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("POST"),
-            eq(NEW_ORDER),
-            anyObject(Map.class))
-        .andThrow(
-            new ExchangeNetworkException(
-                " When it comes to the safety of these people, there's me and "
-                    + "then there's God, understand?"));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testCreateOrderHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("POST"),
-            eq(NEW_ORDER),
-            anyObject(Map.class))
-        .andThrow(
-            new IllegalArgumentException(
-                " We all see what we want to see. Coffey looks and he sees Russians. He sees hate "
-                    + "and fear. You have to look with better eyes than that"));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY, BUY_ORDER_QUANTITY, BUY_ORDER_PRICE);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Cancel Order tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testCancelOrderIsSuccessful() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(CANCEL_ORDER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("DELETE"),
-            eq(CANCEL_ORDER),
-            eq(null))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    // marketId arg not needed for cancelling orders on this exchange.
-    final boolean success = exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null);
-    assertTrue(success);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testCancelOrderHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("DELETE"),
-            eq(CANCEL_ORDER),
-            eq(null))
-        .andThrow(
-            new ExchangeNetworkException(
-                "We don't need them. We can't trust them. We may have to take steps."
-                    + " We're gonna have to take steps."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    // marketId arg not needed for cancelling orders on this exchange.
-    exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testCancelOrderHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("DELETE"),
-            eq(CANCEL_ORDER),
-            eq(null))
-        .andThrow(
-            new IllegalStateException(
-                "Fluid breathing system, we just got it. You use it when you go really deep."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    // marketId arg not needed for cancelling orders on this exchange.
-    exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Get Your Open Orders tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testGettingYourOpenOrdersSuccessfully() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(ORDERS_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ORDERS),
-            eq(null))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final List<OpenOrder> openOrders = exchangeAdapter.getYourOpenOrders(MARKET_ID);
-
-    // assert some key stuff; we're not testing GSON here.
-    assertEquals(2, openOrders.size());
-    assertEquals(MARKET_ID, openOrders.get(0).getMarketId());
-    assertEquals("cdad7602-f290-41e5-a64d-42a1a20fd02", openOrders.get(0).getId());
-    assertSame(OrderType.SELL, openOrders.get(0).getType());
-    assertEquals(
-        openOrders.get(0).getCreationDate(), Date.from(Instant.parse("2015-10-15T21:10:38.193Z")));
-    assertEquals(0, openOrders.get(0).getPrice().compareTo(new BigDecimal("275.00000000")));
-    assertEquals(
-        0, openOrders.get(0).getOriginalQuantity().compareTo(new BigDecimal("0.01000000")));
-    assertEquals(0, openOrders.get(0).getQuantity().compareTo(new BigDecimal("0.00500000")));
-    assertEquals(
-        0,
-        openOrders
-            .get(0)
-            .getTotal()
-            .compareTo(
-                openOrders.get(0).getPrice().multiply(openOrders.get(0).getOriginalQuantity())));
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testGettingYourOpenOrdersHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ORDERS),
-            eq(null))
-        .andThrow(new ExchangeNetworkException("Bond. James Bond."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getYourOpenOrders(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testGettingYourOpenOrdersHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ORDERS),
-            eq(null))
-        .andThrow(
-            new IllegalStateException(
-                "All those moments will be lost in time... like tears in rain."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getYourOpenOrders(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Get Market Orders tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  @SuppressWarnings("unchecked")
-  public void testGettingMarketOrders() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(BOOK_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final Map<String, String> requestParamMap = PowerMock.createMock(Map.class);
-    expect(requestParamMap.put("level", ORDER_BOOK_DEPTH_LEVEL)).andStubReturn(null);
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD,
-            MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD);
-
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD)
-        .andReturn(requestParamMap);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD,
-            eq(BOOK),
-            eq(requestParamMap))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final MarketOrderBook marketOrderBook = exchangeAdapter.getMarketOrders(MARKET_ID);
-
-    // assert some key stuff; we're not testing GSON here.
-    assertEquals(MARKET_ID, marketOrderBook.getMarketId());
-
-    final BigDecimal buyPrice = new BigDecimal("165.87");
-    final BigDecimal buyQuantity = new BigDecimal("16.2373");
-    final BigDecimal buyTotal = buyPrice.multiply(buyQuantity);
-
-    assertEquals(50, marketOrderBook.getBuyOrders().size());
-    assertSame(OrderType.BUY, marketOrderBook.getBuyOrders().get(0).getType());
-    assertEquals(0, marketOrderBook.getBuyOrders().get(0).getPrice().compareTo(buyPrice));
-    assertEquals(0, marketOrderBook.getBuyOrders().get(0).getQuantity().compareTo(buyQuantity));
-    assertEquals(0, marketOrderBook.getBuyOrders().get(0).getTotal().compareTo(buyTotal));
-
-    final BigDecimal sellPrice = new BigDecimal("165.96");
-    final BigDecimal sellQuantity = new BigDecimal("24.31");
-    final BigDecimal sellTotal = sellPrice.multiply(sellQuantity);
-
-    assertEquals(50, marketOrderBook.getSellOrders().size());
-    assertSame(OrderType.SELL, marketOrderBook.getSellOrders().get(0).getType());
-    assertEquals(0, marketOrderBook.getSellOrders().get(0).getPrice().compareTo(sellPrice));
-    assertEquals(0, marketOrderBook.getSellOrders().get(0).getQuantity().compareTo(sellQuantity));
-    assertEquals(0, marketOrderBook.getSellOrders().get(0).getTotal().compareTo(sellTotal));
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testGettingMarketOrdersHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD,
-            eq(BOOK),
-            anyObject(Map.class))
-        .andThrow(new ExchangeNetworkException("Re-verify our range to target... one ping only."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getMarketOrders(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testGettingMarketOrdersHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD,
-            eq(BOOK),
-            anyObject(Map.class))
-        .andThrow(
-            new IllegalArgumentException(
-                "Mr. Ambassador, you have nearly a hundred naval vessels operating in the "
-                    + "North Atlantic right now. Your aircraft has dropped enough sonar buoys "
-                    + "so that a man could walk from Greenland to Iceland to Scotland without "
-                    + "getting his feet wet. Now, shall we dispense with the bull?"));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getMarketOrders(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Get Latest Market Price tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testGettingLatestMarketPriceSuccessfully() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BigDecimal latestMarketPrice =
-        exchangeAdapter.getLatestMarketPrice(MARKET_ID).setScale(8, RoundingMode.HALF_UP);
-    assertEquals(0, latestMarketPrice.compareTo(new BigDecimal("14744.9")));
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testGettingLatestMarketPriceHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andThrow(
-            new ExchangeNetworkException("I need your clothes, your boots and your motorcycle."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getLatestMarketPrice(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testGettingLatestMarketPriceHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andThrow(new IllegalArgumentException("Come with me if you want to live."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getLatestMarketPrice(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Get Balance Info tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testGettingBalanceInfoSuccessfully() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(ACCOUNTS_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ACCOUNTS),
-            eq(null))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BalanceInfo balanceInfo = exchangeAdapter.getBalanceInfo();
-
-    // assert some key stuff; we're not testing GSON here.
-    assertEquals(
-        0,
-        balanceInfo
-            .getBalancesAvailable()
-            .get("BTC")
-            .compareTo(new BigDecimal("100.0000000000000004")));
-    assertEquals(
-        0,
-        balanceInfo
-            .getBalancesAvailable()
-            .get("GBP")
-            .compareTo(new BigDecimal("501.0100000000000001")));
-    assertEquals(0, balanceInfo.getBalancesAvailable().get("EUR").compareTo(new BigDecimal("0")));
-
-    assertEquals(
-        0,
-        balanceInfo
-            .getBalancesOnHold()
-            .get("BTC")
-            .compareTo(new BigDecimal("100.0000000000000005")));
-    assertEquals(
-        0,
-        balanceInfo
-            .getBalancesOnHold()
-            .get("GBP")
-            .compareTo(new BigDecimal("499.9900000000000002")));
-    assertEquals(0, balanceInfo.getBalancesOnHold().get("EUR").compareTo(new BigDecimal("0")));
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testGettingBalanceInfoHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ACCOUNTS),
-            eq(null))
-        .andThrow(
-            new ExchangeNetworkException(
-                "Three o'clock is always too late or too early for anything you want to do."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getBalanceInfo();
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testGettingBalanceInfoHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD,
-            eq("GET"),
-            eq(ACCOUNTS),
-            eq(null))
-        .andThrow(
-            new IllegalStateException(
-                "There is a time for many words, and there is also a time for sleep."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getBalanceInfo();
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Get Ticker tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testGettingTickerSuccessfully() throws Exception {
-    final byte[] encodedTicker = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse tickerExchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encodedTicker, StandardCharsets.UTF_8));
-
-    final byte[] encodedStats = Files.readAllBytes(Paths.get(STATS_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse statsExchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encodedStats, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andReturn(tickerExchangeResponse);
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(STATS), eq(null))
-        .andReturn(statsExchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final Ticker ticker = exchangeAdapter.getTicker(MARKET_ID);
-
-    assertEquals(0, ticker.getLast().compareTo(new BigDecimal("14744.9")));
-    assertEquals(0, ticker.getAsk().compareTo(new BigDecimal("14744.81")));
-    assertEquals(0, ticker.getBid().compareTo(new BigDecimal("14744.8")));
-    assertEquals(0, ticker.getHigh().compareTo(new BigDecimal("14899.00000000")));
-    assertEquals(0, ticker.getLow().compareTo(new BigDecimal("13409.97000000")));
-    assertEquals(0, ticker.getOpen().compareTo(new BigDecimal("13609.53000000")));
-    assertEquals(0, ticker.getVolume().compareTo(new BigDecimal("607.54445656")));
-    assertNull(ticker.getVwap()); // not provided by COINBASE PRO
-    assertEquals(1508008776604L, (long) ticker.getTimestamp());
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testGettingTickerHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andThrow(
-            new ExchangeNetworkException(
-                "Listen, Herr Mac, I don't know what kind of people you're used to dealing with, "
-                    + "but nobody tells me what to do in my place."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getTicker(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testGettingTickerHandlesUnexpectedException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD);
-    PowerMock.expectPrivate(
-            exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null))
-        .andThrow(
-            new IllegalArgumentException(
-                "Indiana Jones. I always knew some day you'd come "
-                    + "walking back through my door. I never doubted that. Something made it "
-                    + "inevitable. So, what are you doing here in Nepal?"));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getTicker(MARKET_ID);
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Non Exchange visiting tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testGettingExchangeSellingFeeIsAsExpected() {
-    PowerMock.replayAll();
-    final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BigDecimal sellPercentageFee =
-        exchangeAdapter.getPercentageOfSellOrderTakenForExchangeFee(MARKET_ID);
-    assertEquals(0, sellPercentageFee.compareTo(new BigDecimal("0.0025")));
-    PowerMock.verifyAll();
-  }
-
-  @Test
-  public void testGettingExchangeBuyingFeeIsAsExpected() {
-    PowerMock.replayAll();
-    final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BigDecimal buyPercentageFee =
-        exchangeAdapter.getPercentageOfBuyOrderTakenForExchangeFee(MARKET_ID);
-    assertEquals(0, buyPercentageFee.compareTo(new BigDecimal("0.0025")));
-    PowerMock.verifyAll();
-  }
-
-  @Test
-  public void testGettingImplNameIsAsExpected() {
-    PowerMock.replayAll();
-    final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    assertEquals("COINBASE PRO REST API v1", exchangeAdapter.getImplName());
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Initialisation tests
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testExchangeAdapterInitialisesSuccessfully() {
-    PowerMock.replayAll();
-
-    final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-    assertNotNull(exchangeAdapter);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfPassphraseConfigIsMissing() {
-    PowerMock.reset(authenticationConfig);
-    expect(authenticationConfig.getItem("passphrase")).andReturn(null);
-    expect(authenticationConfig.getItem("key")).andReturn("your_client_key");
-    expect(authenticationConfig.getItem("secret")).andReturn("your_client_secret");
-    PowerMock.replayAll();
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfPublicKeyConfigIsMissing() {
-    PowerMock.reset(authenticationConfig);
-    expect(authenticationConfig.getItem("passphrase")).andReturn("your_passphrase");
-    expect(authenticationConfig.getItem("key")).andReturn(null);
-    expect(authenticationConfig.getItem("secret")).andReturn("your_client_secret");
-    PowerMock.replayAll();
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfSecretConfigIsMissing() {
-    PowerMock.reset(authenticationConfig);
-    expect(authenticationConfig.getItem("passphrase")).andReturn("your_passphrase");
-    expect(authenticationConfig.getItem("key")).andReturn("your_client_key");
-    expect(authenticationConfig.getItem("secret")).andReturn(null);
-    PowerMock.replayAll();
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfBuyFeeIsMissing() {
-    PowerMock.reset(otherConfig);
-    expect(otherConfig.getItem("buy-fee")).andReturn("");
-    expect(otherConfig.getItem("sell-fee")).andReturn("0.25");
-    PowerMock.replayAll();
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfSellFeeIsMissing() {
-    PowerMock.reset(otherConfig);
-    expect(otherConfig.getItem("buy-fee")).andReturn("0.25");
-    expect(otherConfig.getItem("sell-fee")).andReturn("");
-
-    PowerMock.replayAll();
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testExchangeAdapterThrowsExceptionIfTimeoutConfigIsMissing() {
-    PowerMock.reset(networkConfig);
-    expect(networkConfig.getConnectionTimeout()).andReturn(0);
-    PowerMock.replayAll();
-
-    final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter();
-    exchangeAdapter.init(exchangeConfig);
-
-    PowerMock.verifyAll();
-  }
-
-  // --------------------------------------------------------------------------
-  //  Request sending tests
-  //
-  //  "The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down,
-  //   so suddenly that Alice had not a moment to think about stopping herself before she found
-  //   herself falling down what seemed to be a very deep well..."
-  // --------------------------------------------------------------------------
-
-  @Test
-  public void testSendingPublicRequestToExchangeSuccessfully() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD);
-
-    final URL url = new URL(PUBLIC_API_BASE_URL + TICKER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("GET"),
-            eq(null),
-            eq(new HashMap<>()))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final BigDecimal lastMarketPrice = exchangeAdapter.getLatestMarketPrice(MARKET_ID);
-    assertEquals(0, lastMarketPrice.compareTo(new BigDecimal("14744.9")));
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  public void testSendingPublicRequestToExchangeHandlesExchangeNetworkException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD);
-
-    final URL url = new URL(PUBLIC_API_BASE_URL + TICKER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("GET"),
-            eq(null),
-            eq(new HashMap<>()))
-        .andThrow(
-            new ExchangeNetworkException("One wrong note eventually ruins the entire symphony."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getLatestMarketPrice(MARKET_ID);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  public void testSendingPublicRequestToExchangeHandlesTradingApiException() throws Exception {
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD);
-
-    final URL url = new URL(PUBLIC_API_BASE_URL + TICKER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("GET"),
-            eq(null),
-            eq(new HashMap<>()))
-        .andThrow(new TradingApiException("Look on my works, ye Mighty, and despair."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.getLatestMarketPrice(MARKET_ID);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test
-  @SuppressWarnings("unchecked")
-  public void testSendingAuthenticatedRequestToExchangeSuccessfully() throws Exception {
-    final byte[] encoded = Files.readAllBytes(Paths.get(NEW_SELL_ORDER_JSON_RESPONSE));
-    final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse =
-        new AbstractExchangeAdapter.ExchangeHttpResponse(
-            200, "OK", new String(encoded, StandardCharsets.UTF_8));
-
-    final Map<String, String> requestParamMap = new HashMap<>();
-    requestParamMap.put(
-        "size",
-        new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY));
-    requestParamMap.put(
-        "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE));
-    requestParamMap.put("side", "sell");
-    requestParamMap.put("product_id", MARKET_ID);
-
-    final Map<String, String> requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put");
-    expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null);
-    PowerMock.replay(requestHeaderMap); // map needs to be in play early
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD);
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD)
-        .andReturn(requestHeaderMap);
-
-    final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("POST"),
-            eq(new GsonBuilder().create().toJson(requestParamMap)),
-            eq(requestHeaderMap))
-        .andReturn(exchangeResponse);
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    final String orderId =
-        exchangeAdapter.createOrder(
-            MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE);
-    assertEquals("693d7ad9-e671-4d66-9911-7f75f6380134", orderId);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = ExchangeNetworkException.class)
-  @SuppressWarnings("unchecked")
-  public void testSendingAuthenticatedRequestToExchangeHandlesExchangeNetworkException()
-      throws Exception {
-    final Map<String, String> requestParamMap = new HashMap<>();
-    requestParamMap.put(
-        "size",
-        new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY));
-    requestParamMap.put(
-        "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE));
-    requestParamMap.put("side", "sell");
-    requestParamMap.put("product_id", MARKET_ID);
-
-    final Map<String, String> requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put");
-    expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null);
-    PowerMock.replay(requestHeaderMap); // map needs to be in play early
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD);
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD)
-        .andReturn(requestHeaderMap);
-
-    final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("POST"),
-            eq(new GsonBuilder().create().toJson(requestParamMap)),
-            eq(requestHeaderMap))
-        .andThrow(
-            new ExchangeNetworkException(
-                "Allow me then a moment to consider. You seek your creator. "
-                    + "I am looking at mine. I will serve you, yet you're human. "
-                    + "You will die, I will not."));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE);
-
-    PowerMock.verifyAll();
-  }
-
-  @Test(expected = TradingApiException.class)
-  @SuppressWarnings("unchecked")
-  public void testSendingAuthenticatedRequestToExchangeHandlesTradingApiException()
-      throws Exception {
-    final Map<String, String> requestParamMap = new HashMap<>();
-    requestParamMap.put(
-        "size",
-        new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY));
-    requestParamMap.put(
-        "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE));
-    requestParamMap.put("side", "sell");
-    requestParamMap.put("product_id", MARKET_ID);
-
-    final Map<String, String> requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put");
-    expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null);
-    expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null);
-    PowerMock.replay(requestHeaderMap); // map needs to be in play early
-
-    final CoinbaseProExchangeAdapter exchangeAdapter =
-        PowerMock.createPartialMockAndInvokeDefaultConstructor(
-            CoinbaseProExchangeAdapter.class,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD);
-    PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD)
-        .andReturn(requestHeaderMap);
-
-    final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER);
-    PowerMock.expectPrivate(
-            exchangeAdapter,
-            MOCKED_MAKE_NETWORK_REQUEST_METHOD,
-            eq(url),
-            eq("POST"),
-            eq(new GsonBuilder().create().toJson(requestParamMap)),
-            eq(requestHeaderMap))
-        .andThrow(new TradingApiException("When you close your eyes do you dream of me?"));
-
-    PowerMock.replayAll();
-    exchangeAdapter.init(exchangeConfig);
-
-    exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE);
-
-    PowerMock.verifyAll();
-  }
-}
diff --git a/bxbot-repository/pom.xml b/bxbot-repository/pom.xml
index 168a05ee9..7e71f6f87 100644
--- a/bxbot-repository/pom.xml
+++ b/bxbot-repository/pom.xml
@@ -117,6 +117,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-rest-api/pom.xml b/bxbot-rest-api/pom.xml
index 282352686..72f17ce7f 100644
--- a/bxbot-rest-api/pom.xml
+++ b/bxbot-rest-api/pom.xml
@@ -176,6 +176,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-dependency-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/RestApiConfig.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/RestApiConfig.java
index 8e5e9168c..84224c439 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/RestApiConfig.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/RestApiConfig.java
@@ -54,6 +54,11 @@ public class RestApiConfig {
   @Min(1)
   private int maxLogfileDownloadSize;
 
+  /** Creates the REST API config. */
+  public RestApiConfig() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the max logfile size (in bytes) to be returned by the REST API.
    *
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationEntryPoint.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationEntryPoint.java
index 7c2f3f560..f7c821b86 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationEntryPoint.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationEntryPoint.java
@@ -45,6 +45,11 @@
 @Component
 public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
 
+  /** Creates the JWT Authentication Entry Point. */
+  public JwtAuthenticationEntryPoint() {
+    // No extra init needed.
+  }
+
   /**
    * This is invoked when a user tries to access a secured REST resource without supplying any
    * credentials in the HTTP Authorization header.
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationFilter.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationFilter.java
index 78d1c089a..b7957e4af 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationFilter.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/authentication/JwtAuthenticationFilter.java
@@ -58,6 +58,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
   private JwtUtils jwtUtils;
 
+  /** Creates the JWT Authentication Filter. */
+  public JwtAuthenticationFilter() {
+    // No extra init needed.
+  }
+
   @Override
   protected void doFilterInternal(
       HttpServletRequest request, HttpServletResponse response, FilterChain chain)
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/config/RestCorsConfig.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/config/RestCorsConfig.java
index 35516133e..beb73a1d7 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/config/RestCorsConfig.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/config/RestCorsConfig.java
@@ -50,6 +50,11 @@ public class RestCorsConfig {
   @Value("${restapi.cors.allowed_origin}")
   private String allowedOrigin;
 
+  /** Creates the REST CORS config. */
+  public RestCorsConfig() {
+    // No extra init needed.
+  }
+
   /**
    * Creates the CORS configuration for the CorsFilter to use.
    *
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/jwt/JwtUtils.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/jwt/JwtUtils.java
index ccd2d5852..81dd513bf 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/jwt/JwtUtils.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/jwt/JwtUtils.java
@@ -92,6 +92,11 @@ public class JwtUtils {
   @Value("${bxbot.restapi.jwt.audience}")
   private String audience;
 
+  /** Creates the JWT Utils. */
+  public JwtUtils() {
+    // No extra init needed.
+  }
+
   /**
    * For simple validation, it is sufficient to check the token integrity by just decrypting it with
    * the signing key and making sure it has not expired. We don't have to call the database for an
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/Role.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/Role.java
index 301bbaf48..a8520c05f 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/Role.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/Role.java
@@ -61,6 +61,11 @@ public class Role {
   @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
   private List<User> users;
 
+  /** Creates the Role. */
+  public Role() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the id.
    *
diff --git a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/User.java b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/User.java
index d7d6c0426..e7c8d5819 100644
--- a/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/User.java
+++ b/bxbot-rest-api/src/main/java/com/gazbert/bxbot/rest/api/security/model/User.java
@@ -99,6 +99,11 @@ public class User {
       inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID")})
   private List<Role> roles;
 
+  /** Creates the User. */
+  public User() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the id.
    *
diff --git a/bxbot-services/pom.xml b/bxbot-services/pom.xml
index f4e508b73..0b55d9bf8 100644
--- a/bxbot-services/pom.xml
+++ b/bxbot-services/pom.xml
@@ -92,6 +92,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-strategies/pom.xml b/bxbot-strategies/pom.xml
index 8e15a30ef..27f627ec9 100644
--- a/bxbot-strategies/pom.xml
+++ b/bxbot-strategies/pom.xml
@@ -103,6 +103,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleScalpingStrategy.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleScalpingStrategy.java
index 2d7e44c21..6cd149206 100644
--- a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleScalpingStrategy.java
+++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleScalpingStrategy.java
@@ -155,6 +155,11 @@ public class ExampleScalpingStrategy implements TradingStrategy {
    */
   private BigDecimal minimumPercentageGain;
 
+  /** Constructs the Example Scalping Strategy. */
+  public ExampleScalpingStrategy() {
+    // No extra init.
+  }
+
   /**
    * Initialises the Trading Strategy. Called once by the Trading Engine when the bot starts up;
    * it's a bit like a servlet init() method.
diff --git a/bxbot-strategy-api/pom.xml b/bxbot-strategy-api/pom.xml
index 4aacd993c..2d7ed2641 100644
--- a/bxbot-strategy-api/pom.xml
+++ b/bxbot-strategy-api/pom.xml
@@ -73,6 +73,10 @@
         <groupId>com.github.spotbugs</groupId>
         <artifactId>spotbugs-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-trading-api/pom.xml b/bxbot-trading-api/pom.xml
index 3ae7c5d6f..d79399af3 100644
--- a/bxbot-trading-api/pom.xml
+++ b/bxbot-trading-api/pom.xml
@@ -65,6 +65,10 @@
         <groupId>com.github.spotbugs</groupId>
         <artifactId>spotbugs-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-yaml-datastore/pom.xml b/bxbot-yaml-datastore/pom.xml
index 618899335..35c7e7814 100644
--- a/bxbot-yaml-datastore/pom.xml
+++ b/bxbot-yaml-datastore/pom.xml
@@ -74,6 +74,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/ConfigurationManager.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/ConfigurationManager.java
index d34210635..6bcf9573e 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/ConfigurationManager.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/ConfigurationManager.java
@@ -54,6 +54,11 @@ public class ConfigurationManager {
 
   private static final String YAML_HEADER = "---" + System.getProperty("line.separator");
 
+  /** Creates the Configuration Manager. */
+  public ConfigurationManager() {
+    // No extra init needed.
+  }
+
   /**
    * Loads the config from the YAML file.
    *
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/emailalerts/EmailAlertsType.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/emailalerts/EmailAlertsType.java
index 75c345617..6575171d9 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/emailalerts/EmailAlertsType.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/emailalerts/EmailAlertsType.java
@@ -34,6 +34,11 @@ public class EmailAlertsType {
 
   private EmailAlertsConfig emailAlerts;
 
+  /** Creates the Email Alerts type. */
+  public EmailAlertsType() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the email alerts config.
    *
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/engine/EngineType.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/engine/EngineType.java
index ffc62f9ca..fc89ad030 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/engine/EngineType.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/engine/EngineType.java
@@ -34,6 +34,11 @@ public class EngineType {
 
   private EngineConfig engine;
 
+  /** Creates the Engine type. */
+  public EngineType() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the engine config.
    *
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/exchange/ExchangeType.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/exchange/ExchangeType.java
index 5edc750ac..3aa3cf020 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/exchange/ExchangeType.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/exchange/ExchangeType.java
@@ -34,6 +34,11 @@ public class ExchangeType {
 
   private ExchangeConfig exchange;
 
+  /** Creates the Exchange type. */
+  public ExchangeType() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the exchange config.
    *
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/market/MarketsType.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/market/MarketsType.java
index e67975cd3..8f752f5db 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/market/MarketsType.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/market/MarketsType.java
@@ -36,6 +36,11 @@ public class MarketsType {
 
   private List<MarketConfig> markets;
 
+  /** Creates the Market type. */
+  public MarketsType() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the market configs.
    *
diff --git a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/strategy/StrategiesType.java b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/strategy/StrategiesType.java
index 6a266f6a3..dc33cc911 100644
--- a/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/strategy/StrategiesType.java
+++ b/bxbot-yaml-datastore/src/main/java/com/gazbert/bxbot/datastore/yaml/strategy/StrategiesType.java
@@ -36,6 +36,11 @@ public class StrategiesType {
 
   private List<StrategyConfig> strategies;
 
+  /** Creates the Strategies type. */
+  public StrategiesType() {
+    // No extra init needed.
+  }
+
   /**
    * Returns the Strategy configs.
    *
diff --git a/bxbot.bat b/bxbot.bat
index e2b6fd325..9a9a8e8e2 100644
--- a/bxbot.bat
+++ b/bxbot.bat
@@ -5,7 +5,7 @@ REM Bare bones script for starting BX-bot on Windows systems.
 REM
 REM Could be made better, but will do for now...
 REM
-REM You need a Java 17 JDK/JRE installed.
+REM You need a Java 21 JDK/JRE installed.
 REM
 REM This script expects all the jar files to live in the lib_dir.
 REM
diff --git a/bxbot.sh b/bxbot.sh
index 47f4d9981..4bc2cd6e4 100755
--- a/bxbot.sh
+++ b/bxbot.sh
@@ -7,7 +7,7 @@
 #
 # Could be made better, but will do for now...
 #
-# You need a Java 17 JDK/JRE installed.
+# You need a Java 21 JDK/JRE installed.
 #
 # This script expects all the jar files to live in the lib_dir.
 #
diff --git a/config/samples/coinbase-pro/email-alerts.yaml b/config/samples/coinbase-pro/email-alerts.yaml
deleted file mode 100644
index 55b896c69..000000000
--- a/config/samples/coinbase-pro/email-alerts.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-############################################################################################
-# Email Alerts YAML config.
-#
-# - All fields are mandatory unless stated otherwise.
-# - Only 1 emailAlerts block can be specified.
-# - The email is sent using TLS.
-# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML
-#
-# Sample config for using a Gmail account to send the email is shown below.
-############################################################################################
----
-emailAlerts:
-
-  # If set to true, the bot will load the smtpConfig, and enable email alerts.
-  enabled: false
-
-  # Set your SMTP details here.
-  smtpConfig:
-    host: smtp.gmail.com
-    tlsPort: 587
-    accountUsername: your.account.username@gmail.com
-    accountPassword: your.account.password
-    fromAddress: from.addr@gmail.com
-    toAddress: to.addr@gmail.com
-
diff --git a/config/samples/coinbase-pro/engine.yaml b/config/samples/coinbase-pro/engine.yaml
deleted file mode 100644
index 94731495a..000000000
--- a/config/samples/coinbase-pro/engine.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-############################################################################################
-# Trading Engine YAML config.
-#
-# - All fields are mandatory unless stated otherwise.
-# - Only 1 engine block can be specified.
-# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML
-############################################################################################
----
-engine:
-
-  # A unique identifier for the bot. Value must be an alphanumeric string.
-  # Underscores and dashes are also permitted.
-  botId: my-coinbasepro-bot-1
-
-  # A friendly name for the bot. Value must be an alphanumeric string. Spaces are allowed.
-  botName: CoinbasePro Bot
-
-  # This must be set to prevent catastrophic loss on the exchange.
-  # This is normally the currency you intend to hold a long position in. It should be set to the currency short code for the
-  # wallet, e.g. BTC, LTC, USD. This value can be case sensitive for some exchanges - check the Exchange Adapter documentation.
-  emergencyStopCurrency: BTC
-
-  # This must be set to prevent a catastrophic loss on the exchange.
-  # The Trading Engine checks this value at the start of every trade cycle: if your emergencyStopCurrency balance on
-  # the trading drops below this value, the Trading Engine will stop trading on all markets and shutdown.
-  # Manual intervention is then required to restart the bot. You can set this value to 0 to override this check.
-  emergencyStopBalance: 0.7
-
-  # The is the interval in seconds that the Trading Engine will wait/sleep before executing
-  # the next trade cycle. The minimum value is 1 second. Some exchanges allow you to hit them harder than others. However,
-  # while their API documentation might say one thing, the reality is you might get socket timeouts and 5XX responses if you
-  # hit it too hard - you cannot perform ultra low latency trading over the public internet ;-)
-  # You'll need to experiment with the trade cycle interval for different exchanges.
-  tradeCycleInterval: 60
diff --git a/config/samples/coinbase-pro/exchange.yaml b/config/samples/coinbase-pro/exchange.yaml
deleted file mode 100644
index 7ab94c1b0..000000000
--- a/config/samples/coinbase-pro/exchange.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-############################################################################################
-# Exchange Adapter YAML config.
-#
-# - Sample config below currently set to run against Coinbase Pro
-# - All fields are mandatory unless stated otherwise.
-# - BX-bot only supports running 1 exchange per bot.
-# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML
-#
-# See the README "How do I write my own Exchange Adapter?" section for more details.
-############################################################################################
----
-exchange:
-
-  # A friendly name for the Exchange. Value must be an alphanumeric string. Spaces are allowed.
-  name: Coinbase Pro
-
-  # For the adapter value, you must specify the fully qualified name of your Exchange Adapter class so the Trading Engine
-  # can load and execute it. The class must be on the runtime classpath.
-  adapter: com.gazbert.bxbot.exchanges.CoinbaseProExchangeAdapter
-
-  authenticationConfig:
-    # See: https://docs.pro.coinbase.com/#authentication to get your Coinbase Pro Trading API credentials.
-    passphrase: your-passphrase
-    key: your-api-key
-    secret: your-secret-key
-
-  networkConfig:
-    # This value is in SECONDS. It is the timeout value that the exchange adapter will wait on socket connect/socket read
-    # when communicating with the exchange. Once this threshold has been breached, the exchange adapter will give up and
-    # throw a Trading API TimeoutException.
-    #
-    # The exchange adapter is single threaded: if one request gets blocked, it will block all subsequent requests from
-    # getting to the exchange. This timeout prevents an indefinite block.
-    #
-    # You'll need to experiment with values here.
-    connectionTimeout: 30
-
-    # Optional HTTP status codes that will trigger the adapter to throw a non-fatal ExchangeNetworkException
-    # if the exchange returns any of the below in an API call response:
-    nonFatalErrorCodes: [502, 503, 504, 520, 522, 525]
-
-    # Optional java.io exception messages that will trigger the adapter to throw a non-fatal ExchangeNetworkException
-    # if the exchange returns any of the below in an API call response:
-    nonFatalErrorMessages:
-      - Connection reset
-      - Connection refused
-      - Remote host closed connection during handshake
-      - Unexpected end of file from server
-
-  otherConfig:
-    # Exchange Taker Buy fee in %
-    # IMPORTANT - keep an eye on the fees:
-    # https://help.coinbase.com/en/pro/trading-and-funding/trading-rules-and-fees/fees.html
-    buy-fee: 0.5
-
-    # Exchange Taker Sell fee in %
-    # IMPORTANT - keep an eye on the fees:
-    # https://help.coinbase.com/en/pro/trading-and-funding/trading-rules-and-fees/fees.html
-    sell-fee: 0.5
-
-    # Amount of time in seconds to add to the locally calculated timestamp used to sign the message
-    # sent to the exchange. This allows for slight skew between the bot's local time and that
-    # of the exchange. See: https://docs.pro.coinbase.com/#selecting-a-timestamp
-    # Start with 0 and see how you get on...
-    time-server-bias: 0
-
diff --git a/config/samples/coinbase-pro/markets.yaml b/config/samples/coinbase-pro/markets.yaml
deleted file mode 100644
index 990f8c1eb..000000000
--- a/config/samples/coinbase-pro/markets.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-############################################################################################
-# Market YAML config.
-#
-# - All fields are mandatory unless stated otherwise.
-# - Multiple market blocks can be listed.
-# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML
-############################################################################################
----
-markets:
-
-  # The id value is the market id as defined on the exchange, e.g. 'BTC-GBP'.
-  - id: BTC-GBP
-
-    # A friendly name for the market.
-    # Value must be an alphanumeric string. Spaces are allowed. E.g. BTC/GBP
-    name: BTC/GBP
-
-    # The baseCurrency value is the currency short code for the base currency in the currency pair. When you buy or sell a
-    # currency pair, you are performing that action on the base currency. The base currency is the commodity you are buying or
-    # selling. E.g. in a BTC/GBP market, the first currency (BTC) is the base currency and the second currency (GBP) is the
-    # counter currency.
-    baseCurrency: BTC
-
-    # The counterCurrency value is the currency short code for the counter currency in the currency pair. This is also known
-    # as the quote currency.
-    counterCurrency: GBP
-
-    # The enabled value allows you toggle trading on the market - config changes are only applied on startup.
-    enabled: true
-
-    # The tradingStrategyId value must match a strategy id defined in your strategies.yaml config.
-    # Currently, BX-bot only supports 1 strategy per market.
-    tradingStrategyId: scalping-strategy
-
diff --git a/config/samples/coinbase-pro/strategies.yaml b/config/samples/coinbase-pro/strategies.yaml
deleted file mode 100644
index aa953259d..000000000
--- a/config/samples/coinbase-pro/strategies.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-############################################################################################
-# Trading Strategy YAML config.
-#
-# - You configure the loading of your strategy using either a className or a beanName field.
-# - All fields are mandatory unless stated otherwise.
-# - Multiple strategy blocks can be listed.
-# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML
-#
-# See the README "How do I write my own Trading Strategy?" section for full details.
-############################################################################################
----
-strategies:
-
-  # A unique identifier for the strategy. The markets.yaml tradingStrategyId entries reference this.
-  # Value must be an alphanumeric string. Underscores and dashes are also permitted. E.g. my-macd-strat-1
-  - id: scalping-strategy
-
-    # A friendly name for the strategy.
-    # Value must be an alphanumeric string. Spaces are allowed. E.g. My Super MACD Strat
-    name: Basic Scalping Strat
-
-    # The description value is optional.
-    description: >
-      A simple trend following scalper that buys at the current BID price, holds until current market price has reached
-      a configurable minimum percentage gain, and then sells at current ASK price, thereby taking profit from the spread.
-      Don't forget to factor in the exchange fees!
-
-    # For the className value, you must specify the fully qualified name of your Strategy class for the
-    # Trading Engine to load and execute. This class must be on the runtime classpath.
-    # If you set this value to load your strategy, you cannot set the beanName value.
-    className: com.gazbert.bxbot.strategies.ExampleScalpingStrategy
-
-    # For the beanName value, you must specify the Spring bean name of you Strategy component class
-    # for the Trading Engine to load and execute.
-    # You will also need to annotate your strategy class with `@Component("exampleScalpingStrategy")` -
-    # take a look at ExampleScalpingStrategy.java. This results in Spring injecting the bean.
-    # (see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html)
-    # If you set this value to load your strategy, you cannot set the className value.
-    #beanName: exampleScalpingStrategy
-
-    # The configItems section is optional and allows you to set custom key/value pair config items. This config
-    # is passed to your Trading Strategy when the bot starts up.
-    configItems:
-      counter-currency-buy-order-amount: 20
-      minimum-percentage-gain: 2
diff --git a/etc/spotbugs-exclude-filter.xml b/etc/spotbugs-exclude-filter.xml
index 9efbadcfa..79755c6b5 100644
--- a/etc/spotbugs-exclude-filter.xml
+++ b/etc/spotbugs-exclude-filter.xml
@@ -46,12 +46,6 @@
         <Bug pattern="REC_CATCH_EXCEPTION"/>
     </Match>
 
-    <!-- Ignore Exception not thrown for catch warnings in Coinbase Pro adapter -->
-    <Match>
-        <Class name="com.gazbert.bxbot.exchanges.CoinbaseProExchangeAdapter"/>
-        <Bug pattern="REC_CATCH_EXCEPTION"/>
-    </Match>
-
     <!-- Ignore toLowerCase/toUpperCase warnings without locale in Bitfinex adapter
          Exchange uses US English.
     -->
@@ -76,14 +70,6 @@
         <Bug pattern="DM_CONVERT_CASE"/>
     </Match>
 
-    <!-- Ignore toLowerCase/toUpperCase warnings without locale in Coinbase Pro adapter
-         Exchange uses US English.
-    -->
-    <Match>
-        <Class name="com.gazbert.bxbot.exchanges.CoinbaseProExchangeAdapter"/>
-        <Bug pattern="DM_CONVERT_CASE"/>
-    </Match>
-
     <!-- False positive generated in BotLogfileServiceImpl:
 
          [ERROR] Nullcheck of stream at line 108 of value previously dereferenced in
@@ -327,4 +313,10 @@
         <Bug pattern="EI_EXPOSE_REP2"/>
     </Match>
 
+    <!-- It does throw an IllegalArg exception if SMTP config is missing - the bot should fail to start. -->
+    <Match>
+        <Class name="com.gazbert.bxbot.core.mail.EmailAlerter"/>
+        <Bug pattern="CT_CONSTRUCTOR_THROW"/>
+    </Match>
+
 </FindBugsFilter>
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 033e24c4c..7f93135c4 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ac72c34e8..b82aa23a4 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index fcb6fca14..0adc8e1a5 100755
--- a/gradlew
+++ b/gradlew
@@ -83,7 +83,8 @@ done
 # This is normally unused
 # shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
diff --git a/mvnw b/mvnw
index b7f064624..8d937f4c1 100755
--- a/mvnw
+++ b/mvnw
@@ -19,7 +19,7 @@
 # ----------------------------------------------------------------------------
 
 # ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.1.1
+# Apache Maven Wrapper startup batch script, version 3.2.0
 #
 # Required ENV vars:
 # ------------------
@@ -53,7 +53,7 @@ fi
 cygwin=false;
 darwin=false;
 mingw=false
-case "`uname`" in
+case "$(uname)" in
   CYGWIN*) cygwin=true ;;
   MINGW*) mingw=true;;
   Darwin*) darwin=true
@@ -61,7 +61,7 @@ case "`uname`" in
     # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
     if [ -z "$JAVA_HOME" ]; then
       if [ -x "/usr/libexec/java_home" ]; then
-        JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME
+        JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
       else
         JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
       fi
@@ -71,38 +71,38 @@ esac
 
 if [ -z "$JAVA_HOME" ] ; then
   if [ -r /etc/gentoo-release ] ; then
-    JAVA_HOME=`java-config --jre-home`
+    JAVA_HOME=$(java-config --jre-home)
   fi
 fi
 
 # For Cygwin, ensure paths are in UNIX format before anything is touched
 if $cygwin ; then
   [ -n "$JAVA_HOME" ] &&
-    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+    JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
   [ -n "$CLASSPATH" ] &&
-    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+    CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
 fi
 
 # For Mingw, ensure paths are in UNIX format before anything is touched
 if $mingw ; then
-  [ -n "$JAVA_HOME" ] &&
-    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+    JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
 fi
 
 if [ -z "$JAVA_HOME" ]; then
-  javaExecutable="`which javac`"
-  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+  javaExecutable="$(which javac)"
+  if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
     # readlink(1) is not available as standard on Solaris 10.
-    readLink=`which readlink`
-    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+    readLink=$(which readlink)
+    if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
       if $darwin ; then
-        javaHome="`dirname \"$javaExecutable\"`"
-        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+        javaHome="$(dirname "\"$javaExecutable\"")"
+        javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
       else
-        javaExecutable="`readlink -f \"$javaExecutable\"`"
+        javaExecutable="$(readlink -f "\"$javaExecutable\"")"
       fi
-      javaHome="`dirname \"$javaExecutable\"`"
-      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      javaHome="$(dirname "\"$javaExecutable\"")"
+      javaHome=$(expr "$javaHome" : '\(.*\)/bin')
       JAVA_HOME="$javaHome"
       export JAVA_HOME
     fi
@@ -118,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then
       JAVACMD="$JAVA_HOME/bin/java"
     fi
   else
-    JAVACMD="`\\unset -f command; \\command -v java`"
+    JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
   fi
 fi
 
@@ -150,108 +150,99 @@ find_maven_basedir() {
     fi
     # workaround for JBEAP-8937 (on Solaris 10/Sparc)
     if [ -d "${wdir}" ]; then
-      wdir=`cd "$wdir/.."; pwd`
+      wdir=$(cd "$wdir/.." || exit 1; pwd)
     fi
     # end of workaround
   done
-  printf '%s' "$(cd "$basedir"; pwd)"
+  printf '%s' "$(cd "$basedir" || exit 1; pwd)"
 }
 
 # concatenates all lines of a file
 concat_lines() {
   if [ -f "$1" ]; then
-    echo "$(tr -s '\n' ' ' < "$1")"
+    # Remove \r in case we run on Windows within Git Bash
+    # and check out the repository with auto CRLF management
+    # enabled. Otherwise, we may read lines that are delimited with
+    # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+    # splitting rules.
+    tr -s '\r\n' ' ' < "$1"
   fi
 }
 
-BASE_DIR=$(find_maven_basedir "$(dirname $0)")
+log() {
+  if [ "$MVNW_VERBOSE" = true ]; then
+    printf '%s\n' "$1"
+  fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
 if [ -z "$BASE_DIR" ]; then
   exit 1;
 fi
 
 MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
-if [ "$MVNW_VERBOSE" = true ]; then
-  echo $MAVEN_PROJECTBASEDIR
-fi
+log "$MAVEN_PROJECTBASEDIR"
 
 ##########################################################################################
 # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
 # This allows using the maven wrapper in projects that prohibit checking in binary data.
 ##########################################################################################
-if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
-    if [ "$MVNW_VERBOSE" = true ]; then
-      echo "Found .mvn/wrapper/maven-wrapper.jar"
-    fi
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+    log "Found $wrapperJarPath"
 else
-    if [ "$MVNW_VERBOSE" = true ]; then
-      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
-    fi
+    log "Couldn't find $wrapperJarPath, downloading it ..."
+
     if [ -n "$MVNW_REPOURL" ]; then
-      wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+      wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
     else
-      wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+      wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
     fi
-    while IFS="=" read key value; do
-      case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;;
+    while IFS="=" read -r key value; do
+      # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+      safeValue=$(echo "$value" | tr -d '\r')
+      case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
       esac
-    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
-    if [ "$MVNW_VERBOSE" = true ]; then
-      echo "Downloading from: $wrapperUrl"
-    fi
-    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+    log "Downloading from: $wrapperUrl"
+
     if $cygwin; then
-      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+      wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
     fi
 
     if command -v wget > /dev/null; then
-        QUIET="--quiet"
-        if [ "$MVNW_VERBOSE" = true ]; then
-          echo "Found wget ... using wget"
-          QUIET=""
-        fi
+        log "Found wget ... using wget"
+        [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
         if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-            wget $QUIET "$wrapperUrl" -O "$wrapperJarPath"
+            wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
         else
-            wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath"
+            wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
         fi
-        [ $? -eq 0 ] || rm -f "$wrapperJarPath"
     elif command -v curl > /dev/null; then
-        QUIET="--silent"
-        if [ "$MVNW_VERBOSE" = true ]; then
-          echo "Found curl ... using curl"
-          QUIET=""
-        fi
+        log "Found curl ... using curl"
+        [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
         if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-            curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L
+            curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
         else
-            curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L
+            curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
         fi
-        [ $? -eq 0 ] || rm -f "$wrapperJarPath"
     else
-        if [ "$MVNW_VERBOSE" = true ]; then
-          echo "Falling back to using Java to download"
-        fi
-        javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
-        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class"
+        log "Falling back to using Java to download"
+        javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
         # For Cygwin, switch paths to Windows format before running javac
         if $cygwin; then
-          javaSource=`cygpath --path --windows "$javaSource"`
-          javaClass=`cygpath --path --windows "$javaClass"`
+          javaSource=$(cygpath --path --windows "$javaSource")
+          javaClass=$(cygpath --path --windows "$javaClass")
         fi
         if [ -e "$javaSource" ]; then
             if [ ! -e "$javaClass" ]; then
-                if [ "$MVNW_VERBOSE" = true ]; then
-                  echo " - Compiling MavenWrapperDownloader.java ..."
-                fi
-                # Compiling the Java class
+                log " - Compiling MavenWrapperDownloader.java ..."
                 ("$JAVA_HOME/bin/javac" "$javaSource")
             fi
             if [ -e "$javaClass" ]; then
-                # Running the downloader
-                if [ "$MVNW_VERBOSE" = true ]; then
-                  echo " - Running MavenWrapperDownloader.java ..."
-                fi
-                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+                log " - Running MavenWrapperDownloader.java ..."
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
             fi
         fi
     fi
@@ -260,25 +251,55 @@ fi
 # End of extension
 ##########################################################################################
 
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+  case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+  esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+  wrapperSha256Result=false
+  if command -v sha256sum > /dev/null; then
+    if echo "$wrapperSha256Sum  $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+      wrapperSha256Result=true
+    fi
+  elif command -v shasum > /dev/null; then
+    if echo "$wrapperSha256Sum  $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+      wrapperSha256Result=true
+    fi
+  else
+    echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+    echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+    exit 1
+  fi
+  if [ $wrapperSha256Result = false ]; then
+    echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+    echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+    echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+    exit 1
+  fi
+fi
+
 MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
 
 # For Cygwin, switch paths to Windows format before running java
 if $cygwin; then
   [ -n "$JAVA_HOME" ] &&
-    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+    JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
   [ -n "$CLASSPATH" ] &&
-    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+    CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
   [ -n "$MAVEN_PROJECTBASEDIR" ] &&
-    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+    MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
 fi
 
 # Provide a "standardized" way to retrieve the CLI args that will
 # work with both Windows and non-Windows executions.
-MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
 export MAVEN_CMD_LINE_ARGS
 
 WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
+# shellcheck disable=SC2086 # safe args
 exec "$JAVACMD" \
   $MAVEN_OPTS \
   $MAVEN_DEBUG_OPTS \
diff --git a/mvnw.cmd b/mvnw.cmd
index 474c9d6b7..c4586b564 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -18,7 +18,7 @@
 @REM ----------------------------------------------------------------------------
 
 @REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.1.1
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
 @REM
 @REM Required ENV vars:
 @REM JAVA_HOME - location of a JDK home dir
@@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
 set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
 set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
-set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
 
 FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
     IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
@@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% (
     )
 ) else (
     if not "%MVNW_REPOURL%" == "" (
-        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
     )
     if "%MVNW_VERBOSE%" == "true" (
         echo Couldn't find %WRAPPER_JAR%, downloading it ...
@@ -153,6 +153,24 @@ if exist %WRAPPER_JAR% (
 )
 @REM End of extension
 
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+    powershell -Command "&{"^
+       "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+       "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+       "  Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+       "  Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+       "  Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+       "  exit 1;"^
+       "}"^
+       "}"
+    if ERRORLEVEL 1 goto error
+)
+
 @REM Provide a "standardized" way to retrieve the CLI args that will
 @REM work with both Windows and non-Windows executions.
 set MAVEN_CMD_LINE_ARGS=%*
diff --git a/pom.xml b/pom.xml
index 1f2387649..173d022d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,13 +45,13 @@
     <revision>2.2.0-SNAPSHOT</revision>
 
     <!-- Should be same as dependency used by spring-boot-starter.version -->
-    <spring-core.version>6.0.11</spring-core.version>
+    <spring-core.version>6.0.19</spring-core.version>
 
     <jjwt.version>0.11.5</jjwt.version>
     <powermock.version>2.0.9</powermock.version>
-    <spring-boot-starter.version>3.1.3</spring-boot-starter.version>
-    <checkstlye.version>3.3.0</checkstlye.version>
-    <spotbugs.version>4.7.3.5</spotbugs.version>
+    <spring-boot-starter.version>3.1.11</spring-boot-starter.version>
+    <checkstlye.version>3.3.1</checkstlye.version>
+    <spotbugs.version>4.8.4.0</spotbugs.version>
     <hibernate-vaildator.version>8.0.1.Final</hibernate-vaildator.version>
     <sonar.coverage.jacoco.xmlReportPaths>target/jacoco-report/jacoco.xml
     </sonar.coverage.jacoco.xmlReportPaths>
@@ -61,7 +61,7 @@
   <parent>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-parent</artifactId>
-    <version>3.1.3</version>
+    <version>3.1.11</version>
   </parent>
   <profiles>
     <profile>
@@ -193,7 +193,7 @@
       <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter</artifactId>
-        <version>4.0.4</version>
+        <version>4.0.5</version>
       </dependency>
       <dependency>
         <groupId>io.jsonwebtoken</groupId>
@@ -218,27 +218,27 @@
       <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
-        <version>32.1.2-jre</version>
+        <version>33.1.0-jre</version>
       </dependency>
       <dependency>
         <groupId>com.h2database</groupId>
         <artifactId>h2</artifactId>
-        <version>2.2.222</version>
+        <version>2.2.224</version>
       </dependency>
       <dependency>
         <groupId>jakarta.mail</groupId>
         <artifactId>jakarta.mail-api</artifactId>
-        <version>2.1.2</version>
+        <version>2.1.3</version>
       </dependency>
       <dependency>
         <groupId>org.eclipse.angus</groupId>
         <artifactId>angus-mail</artifactId>
-        <version>2.0.2</version>
+        <version>2.0.3</version>
       </dependency>
       <dependency>
-        <groupId>javax.xml.bind</groupId>
-        <artifactId>jaxb-api</artifactId>
-        <version>2.4.0-b180830.0359</version>
+        <groupId>jakarta.xml.bind</groupId>
+        <artifactId>jakarta.xml.bind-api</artifactId>
+        <version>4.0.2</version>
       </dependency>
       <dependency>
         <groupId>org.yaml</groupId>
@@ -268,7 +268,7 @@
       <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
-        <version>1.18.20</version>
+        <version>1.18.32</version>
       </dependency>
 
       <!--
@@ -306,19 +306,19 @@
       <dependency>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
-        <version>5.10.0</version>
+        <version>5.10.2</version>
       </dependency>
 
       <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-test</artifactId>
-        <version>6.1.3</version>
+        <version>6.1.8</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>org.awaitility</groupId>
         <artifactId>awaitility</artifactId>
-        <version>4.2.0</version>
+        <version>4.2.1</version>
         <scope>test</scope>
       </dependency>
     </dependencies>
@@ -331,7 +331,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
-          <version>3.1.2</version>
+          <version>3.2.5</version>
           <configuration>
             <skipTests>${skip.unit.tests}</skipTests>
             <excludes>
@@ -345,7 +345,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-failsafe-plugin</artifactId>
-          <version>3.1.2</version>
+          <version>3.2.5</version>
           <configuration>
             <skipTests>${skip.integration.tests}</skipTests>
           </configuration>
@@ -360,10 +360,13 @@
         </plugin>
         <plugin>
           <artifactId>maven-compiler-plugin</artifactId>
-          <version>3.11.0</version>
+          <version>3.13.0</version>
           <configuration>
-            <source>17</source>
-            <target>17</target>
+            <source>21</source>
+            <target>21</target>
+            <compilerArgs>
+              <arg>-Xlint:-options</arg>
+            </compilerArgs>
           </configuration>
         </plugin>
 
@@ -390,12 +393,12 @@
         </plugin>
         <plugin>
           <artifactId>maven-assembly-plugin</artifactId>
-          <version>3.6.0</version>
+          <version>3.7.1</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-javadoc-plugin</artifactId>
-          <version>3.5.0</version>
+          <version>3.6.3</version>
           <executions>
             <execution>
               <id>attach-javadocs</id>
@@ -421,7 +424,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>flatten-maven-plugin</artifactId>
-          <version>1.5.0</version>
+          <version>1.6.0</version>
           <configuration>
             <updatePomFile>true</updatePomFile>
           </configuration>
@@ -445,7 +448,7 @@
         <plugin>
           <groupId>org.jacoco</groupId>
           <artifactId>jacoco-maven-plugin</artifactId>
-          <version>0.8.10</version>
+          <version>0.8.12</version>
           <executions>
             <execution>
               <goals>
@@ -488,7 +491,7 @@
         <plugin>
           <groupId>org.sonarsource.scanner.maven</groupId>
           <artifactId>sonar-maven-plugin</artifactId>
-          <version>3.9.1.2184</version>
+          <version>3.11.0.3922</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
@@ -498,7 +501,7 @@
             <dependency>
               <groupId>com.puppycrawl.tools</groupId>
               <artifactId>checkstyle</artifactId>
-              <version>10.12.3</version>
+              <version>10.15.0</version>
             </dependency>
           </dependencies>
           <configuration>
@@ -539,18 +542,23 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-enforcer-plugin</artifactId>
-          <version>3.4.0</version>
+          <version>3.4.1</version>
           <executions>
             <execution>
-              <id>enforce-maven</id>
+              <id>enforce-maven-and-java</id>
               <goals>
                 <goal>enforce</goal>
               </goals>
               <configuration>
                 <rules>
                   <requireMavenVersion>
-                    <version>3.5.0</version>
+                    <version>3.8.1</version>
+                    <level>ERROR</level>
                   </requireMavenVersion>
+                  <requireJavaVersion>
+                    <version>21</version>
+                    <level>ERROR</level>
+                  </requireJavaVersion>
                 </rules>
               </configuration>
             </execution>