From 08bbd5d35b112057bf81285c6ee3f6e67ca60a41 Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 19 Jun 2019 10:46:07 +0200 Subject: [PATCH 1/8] Minimal HTTP API --- .dockerignore | 25 +++++ .idea/codeStyles/Project.xml | 2 + .travis.yml | 33 +++++-- api/.dockerignore | 17 ++++ api/README.md | 92 +++++++++++++++++++ api/docker-compose-base.yml | 34 +++++++ api/docker-compose.yml | 51 ++++++++++ api/docker/dev/Dockerfile | 29 ++++++ api/docker/startApi.sh | 42 +++++++++ .../java/bisq/api/http/HttpApiModule.java | 49 ++++++++++ .../bisq/api/http/app/HttpApiHeadlessApp.java | 15 +++ .../api/http/app/HttpApiHeadlessModule.java | 42 +++++++++ .../java/bisq/api/http/app/HttpApiMain.java | 67 ++++++++++++++ .../bisq/api/http/model/VersionDetails.java | 9 ++ .../api/http/service/HttpApiInterfaceV1.java | 33 +++++++ .../bisq/api/http/service/HttpApiServer.java | 80 ++++++++++++++++ .../service/endpoint/VersionEndpoint.java | 31 +++++++ .../META-INF/custom-swagger-ui/index.html | 58 ++++++++++++ .../main/resources/openapi-configuration.json | 6 ++ .../java/bisq/api/http/ApiTestHelper.java | 13 +++ .../java/bisq/api/http/ContainerFactory.java | 84 +++++++++++++++++ .../java/bisq/api/http/RegexMatcher.java | 28 ++++++ .../java/bisq/api/http/SwaggerIT.java | 61 ++++++++++++ .../java/bisq/api/http/VersionEndpointIT.java | 56 +++++++++++ .../bisq/api/http/arquillian/CubeLogger.java | 30 ++++++ .../http/arquillian/CubeLoggerExtension.java | 11 +++ ...boss.arquillian.core.spi.LoadableExtension | 1 + .../testIntegration/resources/arquillian.xml | 13 +++ .../resources/logback-test.xml | 15 +++ build.gradle | 84 +++++++++++++++++ .../java/bisq/core/app/AppOptionKeys.java | 3 + .../java/bisq/core/app/BisqEnvironment.java | 14 +++ .../java/bisq/core/app/BisqExecutable.java | 9 ++ .../java/bisq/desktop/app/BisqAppMain.java | 10 +- gradle/witness/gradle-witness.gradle | 58 ++++++++++-- settings.gradle | 1 + 36 files changed, 1187 insertions(+), 19 deletions(-) create mode 100644 .dockerignore create mode 100644 api/.dockerignore create mode 100644 api/README.md create mode 100644 api/docker-compose-base.yml create mode 100644 api/docker-compose.yml create mode 100644 api/docker/dev/Dockerfile create mode 100755 api/docker/startApi.sh create mode 100644 api/src/main/java/bisq/api/http/HttpApiModule.java create mode 100644 api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java create mode 100644 api/src/main/java/bisq/api/http/app/HttpApiHeadlessModule.java create mode 100644 api/src/main/java/bisq/api/http/app/HttpApiMain.java create mode 100644 api/src/main/java/bisq/api/http/model/VersionDetails.java create mode 100644 api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java create mode 100644 api/src/main/java/bisq/api/http/service/HttpApiServer.java create mode 100644 api/src/main/java/bisq/api/http/service/endpoint/VersionEndpoint.java create mode 100644 api/src/main/resources/META-INF/custom-swagger-ui/index.html create mode 100644 api/src/main/resources/openapi-configuration.json create mode 100644 api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java create mode 100644 api/src/testIntegration/java/bisq/api/http/ContainerFactory.java create mode 100644 api/src/testIntegration/java/bisq/api/http/RegexMatcher.java create mode 100644 api/src/testIntegration/java/bisq/api/http/SwaggerIT.java create mode 100644 api/src/testIntegration/java/bisq/api/http/VersionEndpointIT.java create mode 100644 api/src/testIntegration/java/bisq/api/http/arquillian/CubeLogger.java create mode 100644 api/src/testIntegration/java/bisq/api/http/arquillian/CubeLoggerExtension.java create mode 100644 api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension create mode 100644 api/src/testIntegration/resources/arquillian.xml create mode 100644 api/src/testIntegration/resources/logback-test.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..e4086f4601d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitignore +.gitmodules +.dockerignore +.travis.yml +docker-compose.yml +docker-compose-base.yml +Dockerfile +.idea +*.iml +target +support +.gradle +build +out +doc + +api/docker/dev/Dockerfile +api/docker/prod/Dockerfile +api/.dockerignore +api/.editorconfig +api/.git +api/.gitignore +api/.travis.yml +api/support diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 59b70d9459c..d07287b5026 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -23,6 +23,8 @@ + + diff --git a/.travis.yml b/.travis.yml index 9a389a5e411..4dd235c2bd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,29 @@ language: java jdk: openjdk10 -before_install: - grep -v '^#' assets/src/main/resources/META-INF/services/bisq.asset.Asset | sort --check --dictionary-order --ignore-case +sudo: required +services: +- docker +jobs: + include: + - stage: asset ordering + install: echo No installation needed + script: grep -v '^#' assets/src/main/resources/META-INF/services/bisq.asset.Asset | sort --check --dictionary-order --ignore-case + - stage: build + - stage: integration + env: + - CUBE_LOGGER_ENABLE=true + install: cd api; docker-compose build; + script: ../gradlew testIntegration; +before_cache: +- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock +- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ notifications: - slack: - on_success: change - on_failure: always - rooms: - - secure: EzlqWTBuhb3FCfApjUXaShWgsWOVDwMXI7ISMiVBkcl2VFISYs/lc/7Qjr7rdy4iqQOQXMcUPHdwMUp0diX+GxiSjLARjUpKIvNOPswZWBL+3Z1h28jyOwtHRviZbM1EU0BZROrr+ODyTNz2lf+L1iXTkpSvk50o5JAnAkumsPw= + slack: + on_success: change + on_failure: always + rooms: + - secure: EzlqWTBuhb3FCfApjUXaShWgsWOVDwMXI7ISMiVBkcl2VFISYs/lc/7Qjr7rdy4iqQOQXMcUPHdwMUp0diX+GxiSjLARjUpKIvNOPswZWBL+3Z1h28jyOwtHRviZbM1EU0BZROrr+ODyTNz2lf+L1iXTkpSvk50o5JAnAkumsPw= diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 00000000000..686ea619974 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,17 @@ +.git +.gitignore +.dockerignore +docker-compose.yml +docker-compose-base.yml +Dockerfile +docker/dev/Dockerfile +docker/prod/Dockerfile + +.idea +*.iml + +target +support + +build +out diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000000..2916638bd25 --- /dev/null +++ b/api/README.md @@ -0,0 +1,92 @@ +# Bisq HTTP API + +**The API branch is under development! +Do not use it in production environment at the current state!** + +Enabling the API exposes some of Bisq's functionality for access over a http API. +You can run it either as the desktop application or as a headless application. + +On that branch we start to implement feature by feature starting with the most simple one - `version`. + + +_**Known issues**: Wallet password protection is not supported at the moment for the headless version. So if you have set + a wallet password when running the Desktop version and afterwards run the headless version it will get stuck at startup + expecting the wallet password. This feature will be implemented soon._ + +_**Note**: +If you have a Bisq application with BTC already set up it is recommended to use the optional `appName` argument to +provide a different name and data directory so that the default Bisq application is not exposed via the API. That way +your data and wallet from your default Bisq application are completely isolated from the API application. In the below +commands we use the argument `--appName=bisq-API` to ensure you are not mixing up your default Bisq setup when +experimenting with the API branch. You cannot run the desktop and the headless version in parallel as they would access +the same data. + +## Run the API as Desktop application + + cd desktop + ../gradlew run --args="--desktopWithHttpApi=true --appName=bisq-API" + +If the application has started up you should see following line in the logs: + + HTTP API started on localhost/127.0.0.1:8080 + +If you prefer another port or host use the arguments `--httpApiHost` and `--httpApiPort`. + +### API Documentation + +Documentation is available at http://localhost:8080/docs/ + +Sample call: + + curl http://localhost:8080/api/v1/version + +## Run the API as headless application + + cd api + ../gradlew run --args="--appName=bisq-API" + +## Docker integration + +First you need to build the docker image for the API: + + cd api + docker-compose build + +Start container with the API: + + docker-compose up alice + +It will automatically start `bisq-api` (alice), `bitcoind` and `seednode` in regtest mode. + + curl localhost:8080/api/v1/version + +## Host and port configuration + + ../gradlew run --args="--httpApiHost=127.0.0.1 --httpApiPort=8080" + +**CAUTION! Please do not expose the API over a public interface** + +## Experimental features + +Some features will be not sufficiently tested and will only be enabled if you add the +`enableHttpApiExperimentalFeatures` argument: + + ../gradlew run --args="--enableHttpApiExperimentalFeatures" + +## Regtest mode + + ../gradlew run --args="--appName=bisq-BTC_REGTEST-alice --nodePort=8003 --useLocalhostForP2P=true + --seedNodes=localhost:8000 --btcNodes=localhost:18445 --baseCurrencyNetwork=BTC_REGTEST --logLevel=info + --useDevPrivilegeKeys=true --bitcoinRegtestHost=NONE --myAddress=172.17.0.1:8003 + --enableHttpApiExperimentalFeatures" + +## Integration tests + +Integration tests leverage Docker and run in headless mode. First you need to build docker images for the api: + + cd api + docker-compose build + ../gradlew testIntegration + +IntelliJ Idea has awesome integration so just right click on `api/src/testIntegration` directory and select +`Debug All Tests`. diff --git a/api/docker-compose-base.yml b/api/docker-compose-base.yml new file mode 100644 index 00000000000..c34dbd45057 --- /dev/null +++ b/api/docker-compose-base.yml @@ -0,0 +1,34 @@ +version: '2.1' + +services: + api: + build: + context: .. + dockerfile: api/docker/dev/Dockerfile + image: bisq/api + environment: + - LOG_LEVEL=debug + - USE_LOCALHOST_FOR_P2P=true + - USE_DEV_PRIVILEGE_KEYS=true + - SEED_NODES=seed:8000 + - BTC_NODES=bitcoin:18444 + - BASE_CURRENCY_NETWORK=BTC_REGTEST + - BITCOIN_REGTEST_HOST=NONE + - HTTP_API_HOST=0.0.0.0 + - ENABLE_HTTP_API_EXPERIMENTAL_FEATURES=true + seed: + image: bisq/seednode + environment: + - MY_ADDRESS=seed:8000 + - SEED_NODES=seed:8000 + - BTC_NODES=bitcoin:18444 + - USE_LOCALHOST_FOR_P2P=true + - BASE_CURRENCY_NETWORK=BTC_REGTEST + - BITCOIN_REGTEST_HOST=NONE + bitcoin: + image: kylemanna/bitcoind:latest + command: -logips -rpcallowip=::/0 -regtest -printtoconsole + environment: + - DISABLEWALLET=0 + - RPCUSER=user + - RPCPASSWORD=password diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 00000000000..2305ff48f75 --- /dev/null +++ b/api/docker-compose.yml @@ -0,0 +1,51 @@ +version: '2.1' + +services: + alice: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8080:8080 + environment: + - NODE_PORT=8003 + links: + - seed + - bitcoin + bob: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8081:8080 + environment: + - NODE_PORT=8004 + links: + - seed + - bitcoin + arbitrator: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8082:8080 + environment: + - NODE_PORT=8005 + links: + - seed + - bitcoin + seed: + extends: + file: docker-compose-base.yml + service: seed + ports: + - 8000:8000 + links: + - bitcoin + bitcoin: + extends: + file: docker-compose-base.yml + service: bitcoin + ports: +# If it is default regtest port (18444) then bisq ignores btcNodes param and uses localhost + - 18445:18444 diff --git a/api/docker/dev/Dockerfile b/api/docker/dev/Dockerfile new file mode 100644 index 00000000000..5fffc0bfeae --- /dev/null +++ b/api/docker/dev/Dockerfile @@ -0,0 +1,29 @@ +FROM openjdk:10-jdk + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjfx && rm -rf /var/lib/apt/lists/* + +WORKDIR /bisq/api + +#ENV HTTP_API_HOST= +#ENV HTTP_API_PORT= +ENV LANG=en_US + +CMD ./docker/startApi.sh + +#Fetch gradle and dependencies +COPY gradle /bisq/gradle/ +COPY gradlew build.gradle gradle.properties settings.gradle /bisq/ +RUN ../gradlew --no-daemon -v + +COPY assets /bisq/assets/ +COPY common /bisq/common/ +COPY core /bisq/core/ +COPY p2p /bisq/p2p/ +COPY pricenode /bisq/pricenode/ +RUN ../gradlew --no-daemon build -x test + +COPY api /bisq/api + +#Compile sources to speed up startup +RUN ../gradlew --no-daemon --offline compileJava -x test diff --git a/api/docker/startApi.sh b/api/docker/startApi.sh new file mode 100755 index 00000000000..1fad1954b54 --- /dev/null +++ b/api/docker/startApi.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +ARGS="" +if [ ! -z "$APP_NAME" ]; then + ARGS="$ARGS --appName=$APP_NAME" +fi +if [ ! -z "$NODE_PORT" ]; then + ARGS="$ARGS --nodePort=$NODE_PORT" +fi +if [ ! -z "$USE_LOCALHOST_FOR_P2P" ]; then + ARGS="$ARGS --useLocalhostForP2P=$USE_LOCALHOST_FOR_P2P" +fi +if [ ! -z "$SEED_NODES" ]; then + ARGS="$ARGS --seedNodes=$SEED_NODES" +fi +if [ ! -z "$BTC_NODES" ]; then + ARGS="$ARGS --btcNodes=$BTC_NODES" +fi +if [ ! -z "$BITCOIN_REGTEST_HOST" ]; then + ARGS="$ARGS --bitcoinRegtestHost=$BITCOIN_REGTEST_HOST" +fi +if [ ! -z "$BASE_CURRENCY_NETWORK" ]; then + ARGS="$ARGS --baseCurrencyNetwork=$BASE_CURRENCY_NETWORK" +fi +if [ ! -z "$LOG_LEVEL" ]; then + ARGS="$ARGS --logLevel=$LOG_LEVEL" +fi +if [ ! -z "$USE_DEV_PRIVILEGE_KEYS" ]; then + ARGS="$ARGS --useDevPrivilegeKeys=$USE_DEV_PRIVILEGE_KEYS" +fi +if [ "$ENABLE_HTTP_API_EXPERIMENTAL_FEATURES" == "true" ]; then + ARGS="$ARGS --enableHttpApiExperimentalFeatures" +fi +if [ ! -z "$HTTP_API_PORT" ]; then + ARGS="$ARGS --httpApiPort=$HTTP_API_PORT" +fi +if [ ! -z "$HTTP_API_HOST" ]; then + ARGS="$ARGS --httpApiHost=$HTTP_API_HOST" +fi + +echo ../gradlew run --no-daemon --args "foo $ARGS" +../gradlew run --no-daemon --args "foo $ARGS" diff --git a/api/src/main/java/bisq/api/http/HttpApiModule.java b/api/src/main/java/bisq/api/http/HttpApiModule.java new file mode 100644 index 00000000000..af199a24c7d --- /dev/null +++ b/api/src/main/java/bisq/api/http/HttpApiModule.java @@ -0,0 +1,49 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package bisq.api.http; + +import bisq.api.http.service.HttpApiServer; +import bisq.api.http.service.endpoint.VersionEndpoint; + +import bisq.core.app.AppOptionKeys; + +import bisq.common.app.AppModule; + +import org.springframework.core.env.Environment; + +import com.google.inject.Singleton; +import com.google.inject.name.Names; + +public class HttpApiModule extends AppModule { + + public HttpApiModule(Environment environment) { + super(environment); + } + + @Override + protected void configure() { + bind(HttpApiServer.class).in(Singleton.class); + bind(VersionEndpoint.class).in(Singleton.class); + + String httpApiHost = environment.getProperty(AppOptionKeys.HTTP_API_HOST, String.class, "127.0.0.1"); + bind(String.class).annotatedWith(Names.named(AppOptionKeys.HTTP_API_HOST)).toInstance(httpApiHost); + + Integer httpApiPort = Integer.valueOf(environment.getProperty(AppOptionKeys.HTTP_API_PORT, String.class, "8080")); + bind(Integer.class).annotatedWith(Names.named(AppOptionKeys.HTTP_API_PORT)).toInstance(httpApiPort); + } +} diff --git a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java new file mode 100644 index 00000000000..877bc6be807 --- /dev/null +++ b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java @@ -0,0 +1,15 @@ +package bisq.api.http.app; + +import bisq.core.app.BisqHeadlessApp; + +import bisq.common.setup.UncaughtExceptionHandler; + +import lombok.extern.slf4j.Slf4j; + +/** + * BisqHeadlessApp implementation for HttpApi. + * This is only used in case of the headless version to startup Bisq. + */ +@Slf4j +class HttpApiHeadlessApp extends BisqHeadlessApp implements UncaughtExceptionHandler { +} diff --git a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessModule.java b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessModule.java new file mode 100644 index 00000000000..e739e28c135 --- /dev/null +++ b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessModule.java @@ -0,0 +1,42 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package bisq.api.http.app; + +import bisq.api.http.HttpApiModule; + +import bisq.core.CoreModule; + +import bisq.common.app.AppModule; + +import org.springframework.core.env.Environment; + +/** + * Used in case of the headless version. + */ +public class HttpApiHeadlessModule extends AppModule { + + public HttpApiHeadlessModule(Environment environment) { + super(environment); + } + + @Override + protected void configure() { + install(new CoreModule(environment)); + install(new HttpApiModule(environment)); + } +} diff --git a/api/src/main/java/bisq/api/http/app/HttpApiMain.java b/api/src/main/java/bisq/api/http/app/HttpApiMain.java new file mode 100644 index 00000000000..d8fea6f9248 --- /dev/null +++ b/api/src/main/java/bisq/api/http/app/HttpApiMain.java @@ -0,0 +1,67 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.api.http.app; + +import bisq.api.http.service.HttpApiServer; + +import bisq.core.app.BisqExecutable; +import bisq.core.app.BisqHeadlessAppMain; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.setup.CommonSetup; + +import lombok.extern.slf4j.Slf4j; + +/** + * Main class for headless version. + */ +@Slf4j +public class HttpApiMain extends BisqHeadlessAppMain { + + public static void main(String[] args) throws Exception { + if (BisqExecutable.setupInitialOptionParser(args)) { + // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. + // In order to work around a bug in JavaFX 8u25 and below, you must include the following code as the first line of your realMain method: + Thread.currentThread().setContextClassLoader(HttpApiMain.class.getClassLoader()); + + new HttpApiMain().execute(args); + } + } + + @Override + protected void launchApplication() { + headlessApp = new HttpApiHeadlessApp(); + CommonSetup.setup(HttpApiMain.this.headlessApp); + + UserThread.execute(this::onApplicationLaunched); + } + + @Override + protected AppModule getModule() { + return new HttpApiHeadlessModule(bisqEnvironment); + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + + HttpApiServer httpApiServer = injector.getInstance(HttpApiServer.class); + httpApiServer.startServer(); + } +} diff --git a/api/src/main/java/bisq/api/http/model/VersionDetails.java b/api/src/main/java/bisq/api/http/model/VersionDetails.java new file mode 100644 index 00000000000..4305aab4a18 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/VersionDetails.java @@ -0,0 +1,9 @@ +package bisq.api.http.model; + +public class VersionDetails { + public String application; + public int network; + public int p2PMessage; + public int localDB; + public int tradeProtocol; +} diff --git a/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java new file mode 100644 index 00000000000..20da4d5e46a --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java @@ -0,0 +1,33 @@ +package bisq.api.http.service; + +import bisq.api.http.service.endpoint.VersionEndpoint; + +import javax.inject.Inject; + + + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.Path; + +@OpenAPIDefinition( + info = @Info(version = "0.0.1", title = "Bisq HTTP API"), + tags = { + @Tag(name = "version") + } +) +@Path("/api/v1") +public class HttpApiInterfaceV1 { + private final VersionEndpoint versionEndpoint; + + @Inject + public HttpApiInterfaceV1(VersionEndpoint versionEndpoint) { + this.versionEndpoint = versionEndpoint; + } + + @Path("version") + public VersionEndpoint getVersionEndpoint() { + return versionEndpoint; + } +} diff --git a/api/src/main/java/bisq/api/http/service/HttpApiServer.java b/api/src/main/java/bisq/api/http/service/HttpApiServer.java new file mode 100644 index 00000000000..9822ab499ac --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/HttpApiServer.java @@ -0,0 +1,80 @@ +package bisq.api.http.service; + +import bisq.core.app.BisqEnvironment; + +import javax.inject.Inject; + +import java.net.InetSocketAddress; + +import lombok.extern.slf4j.Slf4j; + + + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Slf4jRequestLog; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +@SuppressWarnings("Duplicates") +@Slf4j +public class HttpApiServer { + private final HttpApiInterfaceV1 httpApiInterfaceV1; + private final BisqEnvironment bisqEnvironment; + + @Inject + public HttpApiServer(HttpApiInterfaceV1 httpApiInterfaceV1, + BisqEnvironment bisqEnvironment) { + this.httpApiInterfaceV1 = httpApiInterfaceV1; + this.bisqEnvironment = bisqEnvironment; + } + + public void startServer() { + try { + ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection(); + contextHandlerCollection.setHandlers(new Handler[]{buildAPIHandler(), buildSwaggerUIOverrideHandler(), buildSwaggerUIHandler()}); + // Start server + InetSocketAddress socketAddress = new InetSocketAddress(bisqEnvironment.getHttpApiHost(), bisqEnvironment.getHttpApiPort()); + Server server = new Server(socketAddress); + server.setHandler(contextHandlerCollection); + server.setRequestLog(new Slf4jRequestLog()); + server.start(); + log.info("HTTP API started on {}", socketAddress); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ContextHandler buildAPIHandler() { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(httpApiInterfaceV1); + resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources"); + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS | ServletContextHandler.NO_SECURITY); + servletContextHandler.setContextPath("/"); + servletContextHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*"); + return servletContextHandler; + } + + private ContextHandler buildSwaggerUIOverrideHandler() throws Exception { + ResourceHandler swaggerUIResourceHandler = new ResourceHandler(); + swaggerUIResourceHandler.setResourceBase(getClass().getClassLoader().getResource("META-INF/custom-swagger-ui").toURI().toString()); + ContextHandler swaggerUIContext = new ContextHandler(); + swaggerUIContext.setContextPath("/docs"); + swaggerUIContext.setHandler(swaggerUIResourceHandler); + return swaggerUIContext; + } + + private ContextHandler buildSwaggerUIHandler() throws Exception { + ResourceHandler swaggerUIResourceHandler = new ResourceHandler(); + swaggerUIResourceHandler.setResourceBase(getClass().getClassLoader().getResource("META-INF/resources/webjars/swagger-ui/3.20.1").toURI().toString()); + ContextHandler swaggerUIContext = new ContextHandler(); + swaggerUIContext.setContextPath("/docs"); + swaggerUIContext.setHandler(swaggerUIResourceHandler); + return swaggerUIContext; + } +} diff --git a/api/src/main/java/bisq/api/http/service/endpoint/VersionEndpoint.java b/api/src/main/java/bisq/api/http/service/endpoint/VersionEndpoint.java new file mode 100644 index 00000000000..b01c6e41a1c --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/endpoint/VersionEndpoint.java @@ -0,0 +1,31 @@ +package bisq.api.http.service.endpoint; + +import bisq.api.http.model.VersionDetails; + +import bisq.common.app.Version; + + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + + +@Tag(name = "version") +@Produces(MediaType.APPLICATION_JSON) +public class VersionEndpoint { + + @Operation(summary = "Get version details") + @GET + public VersionDetails getVersionDetails() { + VersionDetails versionDetails = new VersionDetails(); + versionDetails.application = Version.VERSION; + versionDetails.network = Version.P2P_NETWORK_VERSION; + versionDetails.p2PMessage = Version.getP2PMessageVersion(); + versionDetails.localDB = Version.LOCAL_DB_VERSION; + versionDetails.tradeProtocol = Version.TRADE_PROTOCOL_VERSION; + return versionDetails; + } +} diff --git a/api/src/main/resources/META-INF/custom-swagger-ui/index.html b/api/src/main/resources/META-INF/custom-swagger-ui/index.html new file mode 100644 index 00000000000..4847a0bbe4b --- /dev/null +++ b/api/src/main/resources/META-INF/custom-swagger-ui/index.html @@ -0,0 +1,58 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + diff --git a/api/src/main/resources/openapi-configuration.json b/api/src/main/resources/openapi-configuration.json new file mode 100644 index 00000000000..29387424917 --- /dev/null +++ b/api/src/main/resources/openapi-configuration.json @@ -0,0 +1,6 @@ +{ + "prettyPrint": true, + "cacheTTL": 0, + "openAPI": { + } +} diff --git a/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java b/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java new file mode 100644 index 00000000000..66530893655 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java @@ -0,0 +1,13 @@ +package bisq.api.http; + +@SuppressWarnings("WeakerAccess") +public final class ApiTestHelper { + + public static void waitForAllServicesToBeReady() throws InterruptedException { +// TODO it would be nice to expose endpoint that would respond with 200 + // PaymentMethod initializes it's static values after all services get initialized + int ALL_SERVICES_INITIALIZED_DELAY = 5000; + Thread.sleep(ALL_SERVICES_INITIALIZED_DELAY); + } + +} diff --git a/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java b/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java new file mode 100644 index 00000000000..56788120598 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java @@ -0,0 +1,84 @@ +package bisq.api.http; + +import org.arquillian.cube.docker.impl.client.config.Await; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.ContainerBuilder; + +@SuppressWarnings("WeakerAccess") +public final class ContainerFactory { + + public static final String BITCOIN_NODE_CONTAINER_NAME = "bisq-api-bitcoin-node"; + public static final String BITCOIN_NODE_HOST_NAME = "bitcoin"; + public static final String SEED_NODE_CONTAINER_NAME = "bisq-seednode"; + public static final String SEED_NODE_HOST_NAME = SEED_NODE_CONTAINER_NAME; + public static final String SEED_NODE_ADDRESS = SEED_NODE_HOST_NAME + ":8000"; + public static final String CONTAINER_NAME_PREFIX = "bisq-api-"; + public static final String API_IMAGE = "bisq/api"; + public static final String ENV_NODE_PORT_KEY = "NODE_PORT"; + public static final String ENV_ENABLE_HTTP_API_EXPERIMENTAL_FEATURES_KEY = "ENABLE_HTTP_API_EXPERIMENTAL_FEATURES"; + public static final String ENV_HTTP_API_HOST_KEY = "HTTP_API_HOST"; + public static final String ENV_HTTP_API_HOST_VALUE = "0.0.0.0"; + public static final String ENV_USE_DEV_PRIVILEGE_KEYS_KEY = "USE_DEV_PRIVILEGE_KEYS"; + public static final String ENV_USE_DEV_PRIVILEGE_KEYS_VALUE = "true"; + public static final String ENV_USE_LOCALHOST_FOR_P2P_KEY = "USE_LOCALHOST_FOR_P2P"; + public static final String ENV_USE_LOCALHOST_FOR_P2P_VALUE = "true"; + public static final String ENV_BASE_CURRENCY_NETWORK_KEY = "BASE_CURRENCY_NETWORK"; + public static final String ENV_BASE_CURRENCY_NETWORK_VALUE = "BTC_REGTEST"; + public static final String ENV_BITCOIN_REGTEST_HOST_KEY = "BITCOIN_REGTEST_HOST"; + public static final String ENV_BITCOIN_REGTEST_HOST_VALUE = "LOCALHOST"; + public static final String ENV_BTC_NODES_KEY = "BTC_NODES"; + public static final String ENV_BTC_NODES_VALUE = "bitcoin:18444"; + public static final String ENV_SEED_NODES_KEY = "SEED_NODES"; + public static final String ENV_SEED_NODES_VALUE = SEED_NODE_ADDRESS; + public static final String ENV_LOG_LEVEL_KEY = "LOG_LEVEL"; + public static final String ENV_LOG_LEVEL_VALUE = "debug"; + + @SuppressWarnings("WeakerAccess") + public static ContainerBuilder.ContainerOptionsBuilder createApiContainerBuilder(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin, boolean enableExperimentalFeatures) { + ContainerBuilder.ContainerOptionsBuilder containerOptionsBuilder = Container.withContainerName(CONTAINER_NAME_PREFIX + nameSuffix) + .fromImage(API_IMAGE) + .withPortBinding(portBinding) + .withEnvironment(ENV_NODE_PORT_KEY, nodePort) + .withEnvironment(ENV_HTTP_API_HOST_KEY, ENV_HTTP_API_HOST_VALUE) + .withEnvironment(ENV_ENABLE_HTTP_API_EXPERIMENTAL_FEATURES_KEY, enableExperimentalFeatures) + .withEnvironment(ENV_USE_DEV_PRIVILEGE_KEYS_KEY, ENV_USE_DEV_PRIVILEGE_KEYS_VALUE) + .withAwaitStrategy(getAwaitStrategy()); + if (linkToSeedNode) { + containerOptionsBuilder.withLink(SEED_NODE_CONTAINER_NAME); + } + if (linkToBitcoin) { + containerOptionsBuilder.withLink(BITCOIN_NODE_CONTAINER_NAME, BITCOIN_NODE_HOST_NAME); + } + return withRegtestEnv(containerOptionsBuilder); + } + + public static Await getAwaitStrategy() { + Await awaitStrategy = new Await(); + awaitStrategy.setStrategy("polling"); + int sleepPollingTime = 250; + awaitStrategy.setIterations(60000 / sleepPollingTime); + awaitStrategy.setSleepPollingTime(sleepPollingTime); + return awaitStrategy; + } + + public static Container createApiContainer(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin, boolean enableExperimentalFeatures) { + Container container = createApiContainerBuilder(nameSuffix, portBinding, nodePort, linkToSeedNode, linkToBitcoin, enableExperimentalFeatures).build(); + container.getCubeContainer().setKillContainer(true); + return container; + } + + public static Container createApiContainer(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin) { + return createApiContainer(nameSuffix, portBinding, nodePort, linkToSeedNode, linkToBitcoin, true); + } + + public static ContainerBuilder.ContainerOptionsBuilder withRegtestEnv(ContainerBuilder.ContainerOptionsBuilder builder) { + return builder + .withEnvironment(ENV_USE_LOCALHOST_FOR_P2P_KEY, ENV_USE_LOCALHOST_FOR_P2P_VALUE) + .withEnvironment(ENV_BASE_CURRENCY_NETWORK_KEY, ENV_BASE_CURRENCY_NETWORK_VALUE) + .withEnvironment(ENV_BITCOIN_REGTEST_HOST_KEY, ENV_BITCOIN_REGTEST_HOST_VALUE) + .withEnvironment(ENV_BTC_NODES_KEY, ENV_BTC_NODES_VALUE) + .withEnvironment(ENV_SEED_NODES_KEY, ENV_SEED_NODES_VALUE) + .withEnvironment(ENV_LOG_LEVEL_KEY, ENV_LOG_LEVEL_VALUE); + } + +} diff --git a/api/src/testIntegration/java/bisq/api/http/RegexMatcher.java b/api/src/testIntegration/java/bisq/api/http/RegexMatcher.java new file mode 100644 index 00000000000..36d37f02d95 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/RegexMatcher.java @@ -0,0 +1,28 @@ +package bisq.api.http; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +public class RegexMatcher extends TypeSafeMatcher { + + private final String regex; + + private RegexMatcher(String regex) { + this.regex = regex; + } + + @Override + public void describeTo(Description description) { + description.appendText("matches regex=`" + regex + "`"); + } + + @Override + public boolean matchesSafely(String string) { + return string.matches(regex); + } + + @SuppressWarnings("WeakerAccess") + public static RegexMatcher matchesRegex(String regex) { + return new RegexMatcher(regex); + } +} diff --git a/api/src/testIntegration/java/bisq/api/http/SwaggerIT.java b/api/src/testIntegration/java/bisq/api/http/SwaggerIT.java new file mode 100644 index 00000000000..349898b5024 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/SwaggerIT.java @@ -0,0 +1,61 @@ +package bisq.api.http; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + + + +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class SwaggerIT { + + @DockerContainer + private Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + @InSequence(1) + @Test + public void getDocs_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/docs"). +// + then(). + statusCode(200). + and().body(containsString("Swagger UI")) + ; + } + + @InSequence(1) + @Test + public void getOpenApiJson_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/openapi.json"). +// + then(). + statusCode(200). + and().body("info.title", equalTo("Bisq HTTP API")) + ; + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/api/src/testIntegration/java/bisq/api/http/VersionEndpointIT.java b/api/src/testIntegration/java/bisq/api/http/VersionEndpointIT.java new file mode 100644 index 00000000000..4a463de664d --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/VersionEndpointIT.java @@ -0,0 +1,56 @@ +package bisq.api.http; + +import bisq.common.app.Version; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isA; + + + +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class VersionEndpointIT { + + @DockerContainer + private Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + @InSequence + @Test + public void waitForAllServicesToBeReady() throws InterruptedException { + ApiTestHelper.waitForAllServicesToBeReady(); + } + + @InSequence(1) + @Test + public void getVersionDetails_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/api/v1/version"). +// + then(). + statusCode(200). + and().body("application", equalTo(Version.VERSION)). + and().body("network", equalTo(Version.P2P_NETWORK_VERSION)). + and().body("p2PMessage", isA(Integer.class)). + and().body("localDB", equalTo(Version.LOCAL_DB_VERSION)). + and().body("tradeProtocol", equalTo(Version.TRADE_PROTOCOL_VERSION)) + ; + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLogger.java b/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLogger.java new file mode 100644 index 00000000000..e08ad59e4e0 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLogger.java @@ -0,0 +1,30 @@ +package bisq.api.http.arquillian; + +import org.arquillian.cube.CubeController; +import org.arquillian.cube.spi.event.lifecycle.BeforeStop; +import org.jboss.arquillian.config.descriptor.api.ArquillianDescriptor; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.test.spi.TestClass; + +public class CubeLogger { + + @SuppressWarnings({"unused", "UnusedParameters"}) + public void beforeContainerStop(@Observes BeforeStop event, CubeController cubeController, ArquillianDescriptor arquillianDescriptor, TestClass testClass) { + if (isExtensionEnabled(arquillianDescriptor)) { + String cubeId = event.getCubeId(); + System.out.println("====================================================================================="); + System.out.println("Start of container logs: " + cubeId + " from " + testClass.getName()); + System.out.println("====================================================================================="); + cubeController.copyLog(cubeId, false, true, true, true, -1, System.out); + System.out.println("====================================================================================="); + System.out.println("End of container logs: " + cubeId + " from " + testClass.getName()); + System.out.println("====================================================================================="); + } + } + + private static boolean isExtensionEnabled(ArquillianDescriptor arquillianDescriptor) { + String dumpContainerLogs = arquillianDescriptor.extension("cubeLogger").getExtensionProperty("enable"); + return Boolean.parseBoolean(dumpContainerLogs); + } + +} diff --git a/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLoggerExtension.java b/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLoggerExtension.java new file mode 100644 index 00000000000..86233c9c7f5 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/arquillian/CubeLoggerExtension.java @@ -0,0 +1,11 @@ +package bisq.api.http.arquillian; + +import org.jboss.arquillian.core.spi.LoadableExtension; + +public class CubeLoggerExtension implements LoadableExtension { + + @Override + public void register(ExtensionBuilder extensionBuilder) { + extensionBuilder.observer(CubeLogger.class); + } +} diff --git a/api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 00000000000..2f2f5d33312 --- /dev/null +++ b/api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +bisq.api.http.arquillian.CubeLoggerExtension diff --git a/api/src/testIntegration/resources/arquillian.xml b/api/src/testIntegration/resources/arquillian.xml new file mode 100644 index 00000000000..45de142b060 --- /dev/null +++ b/api/src/testIntegration/resources/arquillian.xml @@ -0,0 +1,13 @@ + + + + + CUBE + unix:///var/run/docker.sock + false + + + true + + diff --git a/api/src/testIntegration/resources/logback-test.xml b/api/src/testIntegration/resources/logback-test.xml new file mode 100644 index 00000000000..5ebd64fe58e --- /dev/null +++ b/api/src/testIntegration/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 92316e488df..4e12f5d5ba4 100644 --- a/build.gradle +++ b/build.gradle @@ -267,6 +267,89 @@ configure(project(':core')) { } +configure(project(':api')) { + + apply plugin: 'application' + + mainClassName = 'bisq.api.http.app.HttpApiMain' + + dependencies { + compile project(':common') + compile project(':assets') + compile project(':core') + compile project(':p2p') + + compile 'org.eclipse.jetty:jetty-servlet:9.4.14.v20181114' + compile 'javax.activation:activation:1.1.1' + compile('org.glassfish.jersey.containers:jersey-container-servlet:2.27') { + exclude(module: 'jersey-client') + exclude(module: 'jersey-media-jaxb') + } + compile 'org.glassfish.jersey.inject:jersey-hk2:2.27' + compile 'org.glassfish.jersey.ext:jersey-bean-validation:2.27' + compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.27' + + // TODO Lets try to remove swagger for default config (add createDocs config/profile) + compile('io.swagger.core.v3:swagger-jaxrs2:2.0.6') { + exclude(module: 'javassist') + } + compile 'org.webjars:swagger-ui:3.20.1' + + compileOnly 'org.projectlombok:lombok:1.18.2' + annotationProcessor 'org.projectlombok:lombok:1.18.2' + + testCompile 'junit:junit:4.12' + testCompile('org.mockito:mockito-core:2.8.9') { + exclude(module: 'objenesis') + } + testCompileOnly 'org.projectlombok:lombok:1.18.2' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.2' + testCompile "junit:junit:4.12" + testCompile "org.mockito:mockito-core:2.7.5" + testCompile "com.github.javafaker:javafaker:0.14" + testCompile "org.arquillian.universe:arquillian-junit:1.2.0.1" + testCompile "org.arquillian.universe:arquillian-cube-docker:1.2.0.1" + testCompile "org.arquillian.cube:arquillian-cube-docker:1.15.3" + testCompile "io.rest-assured:rest-assured:3.0.2" + } + + sourceSets { + testIntegration { + java.srcDir 'src/testIntegration/java' + resources.srcDir 'src/testIntegration/resources' + compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath + runtimeClasspath += output + compileClasspath + } + } + + task testIntegration(type: Test) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = 'Runs the integration tests.' + + maxHeapSize = '1024m' + + testClassesDir = sourceSets.testIntegration.output.classesDir + classpath = sourceSets.testIntegration.runtimeClasspath + + binResultsDir = file("$buildDir/api/integration-test-results/binary/testIntegration") + + reports { + html.destination = "$buildDir/api/reports/integration-test" + junitXml.destination = "$buildDir/api/integration-test-results" + } + + systemProperties = [ + CUBE_LOGGER_ENABLE: System.getenv('CUBE_LOGGER_ENABLE') + ] + + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' + + mustRunAfter tasks.test + } +} + + configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'witness' @@ -287,6 +370,7 @@ configure(project(':desktop')) { compile project(':p2p') compile project(':core') compile project(':common') + compile project(':api') compile 'org.controlsfx:controlsfx:8.0.6_20' compile 'org.reactfx:reactfx:2.0-M3' compile 'net.glxn:qrgen:1.3' diff --git a/core/src/main/java/bisq/core/app/AppOptionKeys.java b/core/src/main/java/bisq/core/app/AppOptionKeys.java index 6c44f7d4f81..ead7aea8c18 100644 --- a/core/src/main/java/bisq/core/app/AppOptionKeys.java +++ b/core/src/main/java/bisq/core/app/AppOptionKeys.java @@ -29,4 +29,7 @@ public class AppOptionKeys { public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg"; public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys"; public static final String REFERRAL_ID = "referralId"; + public static final String HTTP_API_EXPERIMENTAL_FEATURES_ENABLED = "enableHttpApiExperimentalFeatures"; + public static final String HTTP_API_HOST = "httpApiHost"; + public static final String HTTP_API_PORT = "httpApiPort"; } diff --git a/core/src/main/java/bisq/core/app/BisqEnvironment.java b/core/src/main/java/bisq/core/app/BisqEnvironment.java index 4c7afa5d49d..ec6d7cf61b8 100644 --- a/core/src/main/java/bisq/core/app/BisqEnvironment.java +++ b/core/src/main/java/bisq/core/app/BisqEnvironment.java @@ -200,6 +200,13 @@ public static boolean isDaoActivated(Environment environment) { protected final boolean externalTorUseSafeCookieAuthentication, torStreamIsolation; + @Getter + protected final String httpApiHost; + @Getter + protected final Integer httpApiPort; + @Getter + protected boolean httpApiExperimentalFeaturesEnabled; + public BisqEnvironment(OptionSet options) { this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull( options))); @@ -241,6 +248,13 @@ public BisqEnvironment(PropertySource commandLineProperties) { referralId = commandLineProperties.containsProperty(AppOptionKeys.REFERRAL_ID) ? (String) commandLineProperties.getProperty(AppOptionKeys.REFERRAL_ID) : ""; + httpApiExperimentalFeaturesEnabled = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_EXPERIMENTAL_FEATURES_ENABLED); + httpApiHost = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_HOST) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_HOST) : + "127.0.0.1"; + httpApiPort = Integer.parseInt(commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_PORT) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_PORT) : + "8080"); useDevMode = commandLineProperties.containsProperty(CommonOptionKeys.USE_DEV_MODE) ? (String) commandLineProperties.getProperty(CommonOptionKeys.USE_DEV_MODE) : ""; diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index b2a8ee122e7..86340956333 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -473,6 +473,15 @@ protected void customizeOptionParsing(OptionParser parser) { "Optional Referral ID (e.g. for API users or pro market makers)") .withRequiredArg(); + parser.accepts(AppOptionKeys.HTTP_API_EXPERIMENTAL_FEATURES_ENABLED, "Enable experimental features of HTTP API (disabled by default)"); + parser.accepts(AppOptionKeys.HTTP_API_HOST, "Optional HTTP API host") + .withRequiredArg() + .defaultsTo("127.0.0.1"); + parser.accepts(AppOptionKeys.HTTP_API_PORT, "Optional HTTP API port") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(8080); + parser.accepts(CommonOptionKeys.USE_DEV_MODE, format("Enables dev mode which is used for convenience for developer testing (default: %s)", "false")) .withRequiredArg() diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java index a2494dfbdb1..a7f2d2292fd 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -21,6 +21,8 @@ import bisq.desktop.common.view.guice.InjectorViewFactory; import bisq.desktop.setup.DesktopPersistedDataHost; +import bisq.api.http.service.HttpApiServer; + import bisq.core.app.BisqExecutable; import bisq.common.UserThread; @@ -44,8 +46,6 @@ public BisqAppMain() { super("Bisq Desktop", "bisq-desktop", Version.VERSION); } - /* @Nullable - private BisqHttpApiServer bisqHttpApiServer;*/ /* @Nullable private BisqGrpcServer bisqGrpcServer; */ @@ -135,9 +135,9 @@ protected void startApplication() { protected void onApplicationStarted() { super.onApplicationStarted(); - /* if (runWithHttpApi()) { - bisqHttpApiServer = new BisqHttpApiServer(); - }*/ + if (runWithHttpApi()) { + injector.getInstance(HttpApiServer.class).startServer(); + } /* if (runWithGrpcApi()) { bisqGrpcServer = new BisqGrpcServer(); diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 29746adde8b..ef58927d175 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -11,7 +11,6 @@ // // See https://github.com/signalapp/gradle-witness#using-witness for further details. - dependencyVerification { verify = [ 'org.controlsfx:controlsfx:b98f1c9507c05600f80323674b33d15674926c71b0116f70085b62bdacf1e573', @@ -30,17 +29,27 @@ dependencyVerification { 'org.fxmisc.easybind:easybind:666af296dda6de68751668a62661571b5238ac6f1c07c8a204fc6f902b222aaf', 'network.bisq.btcd-cli4j:btcd-cli4j-daemon:c007116da1b0145ddee64bb3a54fef60d58ce5c3dcf27773f39471117be8f132', 'network.bisq.btcd-cli4j:btcd-cli4j-core:b1d0525f3629bad358ad4a40ea3be998220110331d4b9d24e76d7894e563a595', - 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', - 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', - 'com.fasterxml.jackson.core:jackson-annotations:2566b3a6662afa3c6af4f5b25006cb46be2efc68f1b5116291d6998a8cdf7ed3', + 'org.glassfish.jersey.media:jersey-media-json-jackson:815a783428d87e3f74591c6a9e4fd9c4bf37f5492e4c574b0a3e26a731dabc86', + 'io.swagger.core.v3:swagger-jaxrs2:f4de63695032f11df9664e92ca05284a5c2f484cf02cd53222513fd2ab4c8d32', + 'io.swagger.core.v3:swagger-integration:470dd943148b0ce90747af0583fb1a9c6db5979f845970353995c634bb124172', + 'io.swagger.core.v3:swagger-core:ef8bbaa4bc5643e17dafaaab137c43fb55c47e34287c909546f30f44ed332be0', + 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:26af65ee8a17c6751d51f90e241dd3974df0ba85bc6ba48709e58b509c3219e6', + 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations:eed08585b2a9b6d64f8dba5ab813813dde1a8df5e7f7e744a3a70d83925bcfc2', + 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:d81b4b71778bdca92f4c8b66db08d3e1e3b3fa5b0e4fe349d2f8b59a310cbdd2', + 'com.fasterxml.jackson.core:jackson-databind:0fb4e079c118e752cc94c15ad22e6782b0dfc5dc09145f4813fb39d82e686047', + 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:adc1c495e9c7286bfa1d861ca07e06c8d0980057ad981b48b04f68eb8dcade2c', + 'com.fasterxml.jackson.core:jackson-core:a2bebaa325ad25455b02149c67e6052367a7d7fc1ce77de000eed284a5214eac', + 'io.swagger.core.v3:swagger-models:01453452b0715f6ddd5ff52f82b7a2646d0753360f600fd14bb734853c3765a2', + 'com.fasterxml.jackson.core:jackson-annotations:38a0e450049f643570adac99888aa3480ec2de385790a7096908bf43bfc085d6', 'com.google.protobuf:protobuf-java:b5e2d91812d183c9f053ffeebcbcda034d4de6679521940a19064714966c2cd4', 'com.google.code.gson:gson:2d43eb5ea9e133d2ee2405cc14f5ee08951b8361302fdd93494a3a997b508d32', 'com.googlecode.json-simple:json-simple:4e69696892b88b41c55d49ab2fdcc21eead92bf54acc588c0050596c3b75199c', 'org.springframework:spring-core:c451e8417adb2ffb2445636da5e44a2f59307c4100037a1fe387c3fba4f29b52', 'ch.qos.logback:logback-classic:e66efc674e94837344bc5b748ff510c37a44eeff86cbfdbf9e714ef2eb374013', - 'org.slf4j:slf4j-api:3a4cd4969015f3beb4b5b4d81dbafc01765fb60b8a439955ca64d8476fef553e', + 'org.slf4j:slf4j-api:18c4a0095d5c1da6b817592e767bb23d29dd2f560ad74df75ff3961dbde25b79', 'ch.qos.logback:logback-core:4cd46fa17d77057b39160058df2f21ebbc2aded51d0edcc25d2c1cecc042a005', 'com.google.code.findbugs:jsr305:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7', + 'org.reflections:reflections:cca88428f8a8919df885105833d45ff07bd26f985f96ee55690551216b58b4a1', 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:9b9df27a5b8c7864112b4137fd92b36c3f1395bfe57be42fedf2f520ead1a93e', 'com.github.bisq-network.bitcoinj:bitcoinj-core:f979c2187e61ee3b08dd4cbfc49a149734cff64c045d29ed112f2e12f34068a3', @@ -52,8 +61,14 @@ dependencyVerification { 'org.jetbrains:annotations:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478', 'org.bouncycastle:bcpg-jdk15on:de3355b821fc81dd32e1f3f560d5b3eca1c678fd2400011d0bfc69fb91bcde85', 'commons-io:commons-io:cc6a41dc3eaacc9e440a6bd0d2890b20d36b4ee408fe2d67122f328bb6e01581', - 'org.apache.commons:commons-lang3:734c8356420cc8e30c795d64fd1fcd5d44ea9d90342a2cc3262c5158fbc6d98b', + 'org.apache.commons:commons-lang3:6e8dc31e046508d9953c96534edf0c2e0bfe6f468966b5b842b3f87e43b6a847', 'org.bouncycastle:bcprov-jdk15on:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349', + 'org.eclipse.jetty:jetty-servlet:fbcd7cc44b91a7f318930b237ad4fdeae79fe84ebeee6007baef01724a39e735', + 'javax.activation:activation:ae475120e9fcd99b4b00b38329bd61cdc5eb754eee03fe66c01f50e137724f99', + 'org.glassfish.jersey.containers:jersey-container-servlet:40349db8dabf6327a01ad59eaff172bd9a5f8927b2411bcdc59ceb05ce7731c1', + 'org.glassfish.jersey.inject:jersey-hk2:634a2790f08c2f33feb78586b22a23005a2f8aa483c316ae2435729be0943968', + 'org.glassfish.jersey.ext:jersey-bean-validation:9dc23bd60a6ff1b8ce3f436b1dec959f82a1a643b7a981f4462700aae432c16d', + 'org.webjars:swagger-ui:d6aa5d51493c016e95559452a911ffef739170edc063ff3a3e15ba76d0a40fa5', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf', 'com.github.JesusMcCloud.tor-binary:tor-binary-macos:18f7f1a567821dcc22c4b2146db8c4d00a5c6945a556f1a60085b06ad6d61054', @@ -64,6 +79,9 @@ dependencyVerification { 'org.apache.httpcomponents:httpcore:d7f853dee87680b07293d30855b39b9eb56c1297bd16ff1cd6f19ddb8fa745fb', 'commons-codec:commons-codec:ad19d2601c3abf0b946b5c3a4113e226a8c1e3305e395b90013b78dd94a723ce', 'commons-logging:commons-logging:daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636', + 'org.glassfish.hk2:hk2-locator:2a3766079d1cd21e715e4fd75e8189ebd58f9bf852885f1679253f0a27039b72', + 'org.glassfish.hk2:hk2-api:4d328e5b1cb5e8dcf3f97e1348d960f439e597009aa9d994dd5325bcef367908', + 'org.glassfish.hk2:hk2-utils:5edb176086ad0be1d4abbc0a1d26d519d088a01ee24f9ee064c657895a86e3ee', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', @@ -71,13 +89,39 @@ dependencyVerification { 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', + 'org.eclipse.jetty:jetty-security:c4102cdbcbf6becec08b9ab4afbd9979c3f120e8cf45fbdf6be6c3940fe55bf1', + 'org.glassfish.jersey.containers:jersey-container-servlet-core:39e9fee46f5c6b5d4e49dc03f54741671bd4261090c5f7b5c72541a232873946', + 'org.glassfish.jersey.core:jersey-server:45a2e1e87566cb9808953d1f5ce0b4d99ede51be4a0f22ed92a7ceda7ba9417e', + 'org.glassfish.jersey.core:jersey-client:aba407bda94df54f590041b4cde5f2fa31db45bd8b4cf7575af48c1f8f81bb04', + 'org.glassfish.jersey.media:jersey-media-jaxb:b295e0d7ca93dbb084abd22a01ae7f54d5ffa244dd4b3ce1a6792eda148e76b2', + 'org.glassfish.jersey.core:jersey-common:9a9578c6dac52b96195a614150f696d455db6b6d267a645c3120a4d0ee495789', + 'org.glassfish.jersey.ext:jersey-entity-filtering:529b7ee7830441cffe98851b1e6edc0edd30e8b066052999daa5de63c56302b2', + 'javax.ws.rs:javax.ws.rs-api:1a4295889416c6972addcd425dfeeee6e6ede110e8b2dc8b49044e9b400ad5db', + 'org.glassfish.hk2.external:javax.inject:3bcf096beb918c9586be829190903090a21ac40513c1401e1b986e6030addc98', + 'org.hibernate:hibernate-validator:7f9300345436349396944fc9347437d862f999abd563ebd212291a44ff66e41b', + 'javax.validation:validation-api:f39d7ba7253e35f5ac48081ec1bc28c5df9b32ac4b7db20853e5a8e76bf7b0ed', + 'org.glassfish.web:javax.el:787e7e247da8008c699bafd8e086ccae13e6f3cac3c07ca1c698e44f917b42de', + 'javax.el:javax.el-api:5fd94735743ed06252c83158a24c290fcbf94b3f599b1bcec3bdc8c80979bed7', + 'io.swagger.core.v3:swagger-annotations:36ee5908faa140104b4c19f9273b4813f70f7d8486074119a7b83360fd4a4efb', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.github.JesusMcCloud.tor-binary:tor-binary-geoip:766e4400e5651cf0b11788ea440cc72721be9b92e42f20809c22d0ff129df83c', 'com.github.JesusMcCloud:jtorctl:904f7c53332179a3479c64d63fb303afa6a02b6889aabdab5b235f3efc725ca7', 'org.apache.commons:commons-compress:5f2df1e467825e4cac5996d44890c4201c000b43c0b23cffc0782d28a0beb9b0', 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', + 'org.eclipse.jetty:jetty-server:a922f870f891f6ff4e3503b4e32ac80efb999e9d373ba1f397d055e1f34c8c4a', + 'javax.annotation:javax.annotation-api:5909b396ca3a2be10d0eea32c74ef78d816e1b4ead21de1d78de1f890d033e04', + 'org.glassfish.hk2:osgi-resource-locator:775003be577e8806f51b6e442be1033d83be2cb2207227b349be0bf16e6c0843', + 'org.glassfish.hk2.external:aopalliance-repackaged:669869a9d7e98fcea34580de250db54531550487d03571f26b9592e712897423', + 'org.javassist:javassist:230267ffd7bfe404c1b87faf215dd012f607ba3151bd7099562c305c09de6a7a', + 'org.jboss.logging:jboss-logging:6813931fe607469989f76a73a22515d2489dcd8b6be9fc147093a9cec995f822', + 'com.fasterxml:classmate:1a381660e2f27912e2c421a70bf816a1739e0475190a8efefbcdd00d89216887', 'org.jetbrains.kotlin:kotlin-stdlib-common:4b161ef619eee0d1a49b1c4f0c4a8e46f4e342573efd8e0106a765f47475fe39', + 'javax.servlet:javax.servlet-api:af456b2dd41c4e82cf54f3e743bc678973d9fe35bd4d3071fa05c7e5333b8482', + 'org.eclipse.jetty:jetty-http:964795275e9ea340e302845630dd441d0c4977d99c990f28537d6e834260d64f', + 'org.eclipse.jetty:jetty-io:3710e8c88f99c8047ad38e4163715c1e63026f3fa586fa7727cf81b54dc420d5', + 'javax.xml.bind:jaxb-api:883007989d373d19f352ba9792b25dec21dc7d0e205a710a93a3815101bb3d03', + 'org.eclipse.jetty:jetty-util:02469929c448d4a3197dc69c5a6b9296bd37d12bcafd2b576ec8fe47ee42ef8f', + 'org.yaml:snakeyaml:81bf4c29d0275dace75fadb5febf5384553422816256023efa83b2b15a9ced60', ] } - diff --git a/settings.gradle b/settings.gradle index 500b2803c47..f615de6fd65 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ include 'assets' include 'common' include 'p2p' include 'core' +include 'api' include 'desktop' include 'monitor' include 'pricenode' From 74b1b614ac9ed1d348bc8a7bfe9f50f41247321f Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 19 Jun 2019 10:46:27 +0200 Subject: [PATCH 2/8] Decrease logs verbosity for integration tests --- api/README.md | 5 +++++ .../java/bisq/api/http/ContainerFactory.java | 2 +- api/src/testIntegration/resources/logback-test.xml | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api/README.md b/api/README.md index 2916638bd25..635db51fe0f 100644 --- a/api/README.md +++ b/api/README.md @@ -90,3 +90,8 @@ Integration tests leverage Docker and run in headless mode. First you need to bu IntelliJ Idea has awesome integration so just right click on `api/src/testIntegration` directory and select `Debug All Tests`. + +### Integration tests logging + +Due to Travis log length limitations the log level is set to WARN, but if you need to see more details locally +go to `ContainerFactory` class and set `ENV_LOG_LEVEL_VALUE` property to `debug`. diff --git a/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java b/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java index 56788120598..9ff9a6e0030 100644 --- a/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java +++ b/api/src/testIntegration/java/bisq/api/http/ContainerFactory.java @@ -31,7 +31,7 @@ public final class ContainerFactory { public static final String ENV_SEED_NODES_KEY = "SEED_NODES"; public static final String ENV_SEED_NODES_VALUE = SEED_NODE_ADDRESS; public static final String ENV_LOG_LEVEL_KEY = "LOG_LEVEL"; - public static final String ENV_LOG_LEVEL_VALUE = "debug"; + public static final String ENV_LOG_LEVEL_VALUE = "warn"; @SuppressWarnings("WeakerAccess") public static ContainerBuilder.ContainerOptionsBuilder createApiContainerBuilder(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin, boolean enableExperimentalFeatures) { diff --git a/api/src/testIntegration/resources/logback-test.xml b/api/src/testIntegration/resources/logback-test.xml index 5ebd64fe58e..d26ff1e63be 100644 --- a/api/src/testIntegration/resources/logback-test.xml +++ b/api/src/testIntegration/resources/logback-test.xml @@ -6,10 +6,10 @@ - + - - + + From 530c4a8c6b06577e17b58c44a7806847b4664edd Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 16 Jan 2019 11:41:33 +0100 Subject: [PATCH 3/8] Add onInitP2pNetwork and onInitWallet to BisqSetupListener - Rename BisqSetupCompleteListener to BisqSetupListener - Add onInitP2pNetwork and onInitWallet to BisqSetupListener - make onInitP2pNetwork and onInitWallet default so no impl. required --- .../java/bisq/core/app/BisqExecutable.java | 4 ++-- .../java/bisq/core/app/BisqHeadlessApp.java | 2 +- .../main/java/bisq/core/app/BisqSetup.java | 20 ++++++++++++++----- .../main/java/bisq/core/app/HeadlessApp.java | 2 +- .../java/bisq/desktop/app/BisqAppMain.java | 2 +- .../java/bisq/desktop/main/MainViewModel.java | 6 +++--- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 86340956333..b673fa52d46 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -74,7 +74,7 @@ import static java.lang.String.format; @Slf4j -public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSetup.BisqSetupCompleteListener { +public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSetup.BisqSetupListener { private final String fullName; private final String scriptName; @@ -270,7 +270,7 @@ protected void onApplicationStarted() { protected void startAppSetup() { BisqSetup bisqSetup = injector.getInstance(BisqSetup.class); - bisqSetup.addBisqSetupCompleteListener(this); + bisqSetup.addBisqSetupListener(this); bisqSetup.start(); } diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index f7ece906fb7..22c23deccf3 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -54,7 +54,7 @@ public BisqHeadlessApp() { public void startApplication() { try { bisqSetup = injector.getInstance(BisqSetup.class); - bisqSetup.addBisqSetupCompleteListener(this); + bisqSetup.addBisqSetupListener(this); corruptedDatabaseFilesHandler = injector.getInstance(CorruptedDatabaseFilesHandler.class); tradeManager = injector.getInstance(TradeManager.class); diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 26cc912e6c7..a2bcbca7af0 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -110,7 +110,15 @@ @Slf4j public class BisqSetup { - public interface BisqSetupCompleteListener { + public interface BisqSetupListener { + default void onInitP2pNetwork() { + log.info("onInitP2pNetwork"); + } + + default void onInitWallet() { + log.info("onInitWallet"); + } + void onSetupComplete(); } @@ -191,7 +199,7 @@ public interface BisqSetupCompleteListener { private boolean allBasicServicesInitialized; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding p2pNetworkAndWalletInitialized; - private List bisqSetupCompleteListeners = new ArrayList<>(); + private List bisqSetupListeners = new ArrayList<>(); @Inject public BisqSetup(P2PNetworkSetup p2PNetworkSetup, @@ -278,8 +286,8 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, // Setup /////////////////////////////////////////////////////////////////////////////////////////// - public void addBisqSetupCompleteListener(BisqSetupCompleteListener listener) { - bisqSetupCompleteListeners.add(listener); + public void addBisqSetupListener(BisqSetupListener listener) { + bisqSetupListeners.add(listener); } public void start() { @@ -305,7 +313,7 @@ private void step4() { private void step5() { initDomainServices(); - bisqSetupCompleteListeners.forEach(BisqSetupCompleteListener::onSetupComplete); + bisqSetupListeners.forEach(BisqSetupListener::onSetupComplete); // We set that after calling the setupCompleteHandler to not trigger a popup from the dev dummy accounts // in MainViewModel @@ -512,6 +520,7 @@ else if (displayTorNetworkSettingsHandler != null) }, STARTUP_TIMEOUT_MINUTES, TimeUnit.MINUTES); + bisqSetupListeners.forEach(BisqSetupListener::onInitP2pNetwork); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); // We only init wallet service here if not using Tor for bitcoinj. @@ -538,6 +547,7 @@ else if (displayTorNetworkSettingsHandler != null) } private void initWallet() { + bisqSetupListeners.forEach(BisqSetupListener::onInitWallet); Runnable walletPasswordHandler = () -> { if (p2pNetworkReady.get()) p2PNetworkSetup.setSplashP2PNetworkAnimationVisible(true); diff --git a/core/src/main/java/bisq/core/app/HeadlessApp.java b/core/src/main/java/bisq/core/app/HeadlessApp.java index 8d105a93398..3808b89b1a0 100644 --- a/core/src/main/java/bisq/core/app/HeadlessApp.java +++ b/core/src/main/java/bisq/core/app/HeadlessApp.java @@ -22,7 +22,7 @@ import com.google.inject.Injector; -public interface HeadlessApp extends UncaughtExceptionHandler, BisqSetup.BisqSetupCompleteListener { +public interface HeadlessApp extends UncaughtExceptionHandler, BisqSetup.BisqSetupListener { void setGracefulShutDownHandler(GracefulShutDownHandler gracefulShutDownHandler); void setInjector(Injector injector); diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java index a7f2d2292fd..10d4ee72cdf 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -61,7 +61,7 @@ public static void main(String[] args) throws Exception { @Override public void onSetupComplete() { - log.debug("onSetupComplete"); + log.info("onSetupComplete"); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index bc180a27d66..ab74698099d 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -93,7 +93,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteListener { +public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { private final BisqSetup bisqSetup; private final WalletsSetup walletsSetup; private final User user; @@ -190,12 +190,12 @@ public MainViewModel(BisqSetup bisqSetup, GUIUtil.setPreferences(preferences); setupHandlers(); - bisqSetup.addBisqSetupCompleteListener(this); + bisqSetup.addBisqSetupListener(this); } /////////////////////////////////////////////////////////////////////////////////////////// - // BisqSetupCompleteListener + // BisqSetupListener /////////////////////////////////////////////////////////////////////////////////////////// @Override From f214270bc124fe1c4a8f872d36711f75e88f1b64 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 16 Jan 2019 12:09:11 +0100 Subject: [PATCH 4/8] Start server at onInitWallet and add wallet password handler - Add onInitWallet to HttpApiMain and start http server there - Add onRequestWalletPassword to BisqSetupListener - Override setupHandlers in HttpApiHeadlessApp and adjust setRequestWalletPasswordHandler (impl. missing) - Add onRequestWalletPassword to HttpApiMain --- .../bisq/api/http/app/HttpApiHeadlessApp.java | 39 +++++++++++++++++++ .../java/bisq/api/http/app/HttpApiMain.java | 16 +++++++- .../java/bisq/core/app/BisqHeadlessApp.java | 2 +- .../main/java/bisq/core/app/BisqSetup.java | 9 +++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java index 877bc6be807..f0b2cc3d79a 100644 --- a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java +++ b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java @@ -12,4 +12,43 @@ */ @Slf4j class HttpApiHeadlessApp extends BisqHeadlessApp implements UncaughtExceptionHandler { + + @Override + protected void setupHandlers() { + super.setupHandlers(); + + bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> { + log.info("onRequestWalletPasswordHandler"); + + // TODO @bernard listen for users input of pw, create aseKey and call handler + // aesKeyHandler.accept(aseKey); + + // here is code from UI + /* String password = passwordTextField.getText(); + checkArgument(password.length() < 500, Res.get("password.tooLong")); + KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); + if (keyCrypterScrypt != null) { + busyAnimation.play(); + deriveStatusLabel.setText(Res.get("password.deriveKey")); + ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> { + if (walletsManager.checkAESKey(aesKey)) { + if (aesKeyHandler != null) + aesKeyHandler.onAesKey(aesKey); + + hide(); + } else { + busyAnimation.stop(); + deriveStatusLabel.setText(""); + + UserThread.runAfter(() -> new Popup<>() + .warning(Res.get("password.wrongPw")) + .onClose(this::blurAgain).show(), Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS); + } + }); + } else { + log.error("wallet.getKeyCrypter() is null, that must not happen."); + } + */ + }); + } } diff --git a/api/src/main/java/bisq/api/http/app/HttpApiMain.java b/api/src/main/java/bisq/api/http/app/HttpApiMain.java index d8fea6f9248..37198ff9594 100644 --- a/api/src/main/java/bisq/api/http/app/HttpApiMain.java +++ b/api/src/main/java/bisq/api/http/app/HttpApiMain.java @@ -58,10 +58,22 @@ protected AppModule getModule() { } @Override - public void onSetupComplete() { - log.info("onSetupComplete"); + public void onInitWallet() { + log.info("onInitWallet: We start the http server now"); HttpApiServer httpApiServer = injector.getInstance(HttpApiServer.class); httpApiServer.startServer(); } + + @Override + public void onRequestWalletPassword() { + log.info("onRequestWalletPassword"); + + // TODO @bernard now we need to get users wallet pw + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } } diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 22c23deccf3..c77acd6d7c5 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -43,7 +43,7 @@ public class BisqHeadlessApp implements HeadlessApp { @Setter private GracefulShutDownHandler gracefulShutDownHandler; private boolean shutDownRequested; - private BisqSetup bisqSetup; + protected BisqSetup bisqSetup; private CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler; private TradeManager tradeManager; diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index a2bcbca7af0..43d1a354573 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -119,6 +119,10 @@ default void onInitWallet() { log.info("onInitWallet"); } + default void onRequestWalletPassword() { + log.info("onRequestWalletPassword"); + } + void onSetupComplete(); } @@ -549,6 +553,8 @@ else if (displayTorNetworkSettingsHandler != null) private void initWallet() { bisqSetupListeners.forEach(BisqSetupListener::onInitWallet); Runnable walletPasswordHandler = () -> { + log.info("Wallet password required"); + bisqSetupListeners.forEach(BisqSetupListener::onRequestWalletPassword); if (p2pNetworkReady.get()) p2PNetworkSetup.setSplashP2PNetworkAnimationVisible(true); @@ -559,6 +565,9 @@ private void initWallet() { if (showFirstPopupIfResyncSPVRequestedHandler != null) showFirstPopupIfResyncSPVRequestedHandler.run(); } else { + // TODO no guarantee here that the wallet is really fully initialized + // We would need a new walletInitializedButNotEncrypted state to track + // Usually init is fast and we have our wallet initialized at that state though. walletInitialized.set(true); } }); From 1500acb1eb863e5e21eab9ae07c89b05c5baa617 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 16 Jan 2019 12:15:48 +0100 Subject: [PATCH 5/8] Add periodic log to remind user to enter pw --- .../java/bisq/api/http/app/HttpApiHeadlessApp.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java index f0b2cc3d79a..ed8b342f4b5 100644 --- a/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java +++ b/api/src/main/java/bisq/api/http/app/HttpApiHeadlessApp.java @@ -2,6 +2,8 @@ import bisq.core.app.BisqHeadlessApp; +import bisq.common.Timer; +import bisq.common.UserThread; import bisq.common.setup.UncaughtExceptionHandler; import lombok.extern.slf4j.Slf4j; @@ -20,8 +22,17 @@ protected void setupHandlers() { bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> { log.info("onRequestWalletPasswordHandler"); + // Add a periodic log so that users get reminded to enter the pw + Timer reminder = UserThread.runPeriodically(() -> { + log.info("Awaiting user's wallet password to be entered via API call"); + }, 10); + + // TODO @bernard listen for users input of pw, create aseKey and call handler // aesKeyHandler.accept(aseKey); + // Once pw is entered we stop periodic log + // reminder.stop(); + // here is code from UI /* String password = passwordTextField.getText(); From 527999c13b5e167b4d2bc546f9270540a8b237cd Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 19 Jun 2019 10:47:15 +0200 Subject: [PATCH 6/8] Security framework for the API --- api/README.md | 33 +- .../java/bisq/api/http/HttpApiModule.java | 6 + .../api/http/exceptions/ExceptionMappers.java | 46 +++ .../exceptions/UnauthorizedException.java | 4 + .../java/bisq/api/http/model/AuthForm.java | 17 + .../java/bisq/api/http/model/AuthResult.java | 14 + .../bisq/api/http/model/ChangePassword.java | 16 + .../api/http/service/HttpApiInterfaceV1.java | 22 +- .../bisq/api/http/service/HttpApiServer.java | 27 +- .../http/service/auth/ApiPasswordManager.java | 135 ++++++ .../api/http/service/auth/AuthFilter.java | 60 +++ .../api/http/service/auth/TokenRegistry.java | 63 +++ .../http/service/endpoint/UserEndpoint.java | 73 ++++ .../service/auth/ApiPasswordManagerTest.java | 391 ++++++++++++++++++ .../api/http/service/auth/AuthFilterTest.java | 162 ++++++++ .../http/service/auth/TokenRegistryTest.java | 116 ++++++ .../java/bisq/api/http/UserEndpointIT.java | 317 ++++++++++++++ build.gradle | 7 +- 18 files changed, 1497 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java create mode 100644 api/src/main/java/bisq/api/http/exceptions/UnauthorizedException.java create mode 100644 api/src/main/java/bisq/api/http/model/AuthForm.java create mode 100644 api/src/main/java/bisq/api/http/model/AuthResult.java create mode 100644 api/src/main/java/bisq/api/http/model/ChangePassword.java create mode 100644 api/src/main/java/bisq/api/http/service/auth/ApiPasswordManager.java create mode 100644 api/src/main/java/bisq/api/http/service/auth/AuthFilter.java create mode 100644 api/src/main/java/bisq/api/http/service/auth/TokenRegistry.java create mode 100644 api/src/main/java/bisq/api/http/service/endpoint/UserEndpoint.java create mode 100644 api/src/test/java/bisq/api/http/service/auth/ApiPasswordManagerTest.java create mode 100644 api/src/test/java/bisq/api/http/service/auth/AuthFilterTest.java create mode 100644 api/src/test/java/bisq/api/http/service/auth/TokenRegistryTest.java create mode 100644 api/src/testIntegration/java/bisq/api/http/UserEndpointIT.java diff --git a/api/README.md b/api/README.md index 635db51fe0f..b37a171fc40 100644 --- a/api/README.md +++ b/api/README.md @@ -9,18 +9,20 @@ You can run it either as the desktop application or as a headless application. On that branch we start to implement feature by feature starting with the most simple one - `version`. -_**Known issues**: Wallet password protection is not supported at the moment for the headless version. So if you have set +**Known issues**: Wallet password protection is not supported at the moment for the headless version. So if you have set a wallet password when running the Desktop version and afterwards run the headless version it will get stuck at startup expecting the wallet password. This feature will be implemented soon._ -_**Note**: -If you have a Bisq application with BTC already set up it is recommended to use the optional `appName` argument to +**Note**: If you have a Bisq application with BTC already set up it is recommended to use the optional `appName` argument to provide a different name and data directory so that the default Bisq application is not exposed via the API. That way your data and wallet from your default Bisq application are completely isolated from the API application. In the below commands we use the argument `--appName=bisq-API` to ensure you are not mixing up your default Bisq setup when experimenting with the API branch. You cannot run the desktop and the headless version in parallel as they would access the same data. +**Security**: Api uses HTTP transport which is not encrypted. Use the API only locally and do not expose it over +public network interfaces. + ## Run the API as Desktop application cd desktop @@ -39,6 +41,31 @@ Documentation is available at http://localhost:8080/docs/ Sample call: curl http://localhost:8080/api/v1/version + +#### Authentication + +By default there is no password required for the API. We recommend that you set password as soon as possible: + + curl -X POST "http://localhost:8080/api/v1/user/password" -H "Content-Type: application/json" -d "{\"newPassword\":\"string\"}" + +Password digest and salt are stored in a `apipasswd` in Bisq data directory. +If you forget your password, just delete that file and restart Bisq. + +Once you set the password you need to trade it for access token. + + curl -X POST "http://localhost:8080/api/v1/user/authenticate" -H "Content-Type: application/json" -d "{\"password\":\"string\"}" + +You should get the token in response looking like this: + + { + "token": "5130bcb6-bee5-47ae-ac78-ee31d6557ed5" + } + +Now you can access other endpoints by adding `Authorization` header to the request: + + curl -X GET "http://localhost:8080/api/v1/version" -H "authorization: 5130bcb6-bee5-47ae-ac78-ee31d6557ed5" + +**NOTE**: Tokens expire after 30 minutes. ## Run the API as headless application diff --git a/api/src/main/java/bisq/api/http/HttpApiModule.java b/api/src/main/java/bisq/api/http/HttpApiModule.java index af199a24c7d..3130576b43c 100644 --- a/api/src/main/java/bisq/api/http/HttpApiModule.java +++ b/api/src/main/java/bisq/api/http/HttpApiModule.java @@ -18,6 +18,9 @@ package bisq.api.http; import bisq.api.http.service.HttpApiServer; +import bisq.api.http.service.auth.ApiPasswordManager; +import bisq.api.http.service.auth.TokenRegistry; +import bisq.api.http.service.endpoint.UserEndpoint; import bisq.api.http.service.endpoint.VersionEndpoint; import bisq.core.app.AppOptionKeys; @@ -38,6 +41,9 @@ public HttpApiModule(Environment environment) { @Override protected void configure() { bind(HttpApiServer.class).in(Singleton.class); + bind(TokenRegistry.class).in(Singleton.class); + bind(ApiPasswordManager.class).in(Singleton.class); + bind(UserEndpoint.class).in(Singleton.class); bind(VersionEndpoint.class).in(Singleton.class); String httpApiHost = environment.getProperty(AppOptionKeys.HTTP_API_HOST, String.class, "127.0.0.1"); diff --git a/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java b/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java new file mode 100644 index 00000000000..9580da65202 --- /dev/null +++ b/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java @@ -0,0 +1,46 @@ +package bisq.api.http.exceptions; + +import com.fasterxml.jackson.core.JsonParseException; + +import lombok.extern.slf4j.Slf4j; + + + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import org.eclipse.jetty.io.EofException; +import org.glassfish.jersey.server.ResourceConfig; + +@Slf4j +public final class ExceptionMappers { + + private ExceptionMappers() { + } + + public static void register(ResourceConfig environment) { + environment.register(new ExceptionMappers.EofExceptionMapper(), 1); + environment.register(new ExceptionMappers.JsonParseExceptionMapper(), 1); + environment.register(new ExceptionMappers.UnauthorizedExceptionMapper()); + } + + public static class EofExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(EofException e) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + } + + public static class JsonParseExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(JsonParseException e) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + } + + public static class UnauthorizedExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(UnauthorizedException exception) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } +} diff --git a/api/src/main/java/bisq/api/http/exceptions/UnauthorizedException.java b/api/src/main/java/bisq/api/http/exceptions/UnauthorizedException.java new file mode 100644 index 00000000000..76f032e6efe --- /dev/null +++ b/api/src/main/java/bisq/api/http/exceptions/UnauthorizedException.java @@ -0,0 +1,4 @@ +package bisq.api.http.exceptions; + +public class UnauthorizedException extends RuntimeException { +} diff --git a/api/src/main/java/bisq/api/http/model/AuthForm.java b/api/src/main/java/bisq/api/http/model/AuthForm.java new file mode 100644 index 00000000000..46eaf0d62ed --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/AuthForm.java @@ -0,0 +1,17 @@ +package bisq.api.http.model; + +import org.hibernate.validator.constraints.NotEmpty; + +public class AuthForm { + + @NotEmpty + public String password; + + @SuppressWarnings("unused") + public AuthForm() { + } + + public AuthForm(String password) { + this.password = password; + } +} diff --git a/api/src/main/java/bisq/api/http/model/AuthResult.java b/api/src/main/java/bisq/api/http/model/AuthResult.java new file mode 100644 index 00000000000..a526528b719 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/AuthResult.java @@ -0,0 +1,14 @@ +package bisq.api.http.model; + +public class AuthResult { + + public String token; + + @SuppressWarnings("unused") + public AuthResult() { + } + + public AuthResult(String token) { + this.token = token; + } +} diff --git a/api/src/main/java/bisq/api/http/model/ChangePassword.java b/api/src/main/java/bisq/api/http/model/ChangePassword.java new file mode 100644 index 00000000000..934a0d7f1b5 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/ChangePassword.java @@ -0,0 +1,16 @@ +package bisq.api.http.model; + +public class ChangePassword { + + public String newPassword; + public String oldPassword; + + @SuppressWarnings("unused") + public ChangePassword() { + } + + public ChangePassword(String newPassword, String oldPassword) { + this.newPassword = newPassword; + this.oldPassword = oldPassword; + } +} diff --git a/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java index 20da4d5e46a..b0e92e3580b 100644 --- a/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java +++ b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java @@ -1,5 +1,6 @@ package bisq.api.http.service; +import bisq.api.http.service.endpoint.UserEndpoint; import bisq.api.http.service.endpoint.VersionEndpoint; import javax.inject.Inject; @@ -7,25 +8,44 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.tags.Tag; import javax.ws.rs.Path; +@SecurityScheme( + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.HEADER, + name = "authorization", + paramName = "authorization" +) @OpenAPIDefinition( info = @Info(version = "0.0.1", title = "Bisq HTTP API"), + security = @SecurityRequirement(name = "authorization"), tags = { + @Tag(name = "user"), @Tag(name = "version") } ) @Path("/api/v1") public class HttpApiInterfaceV1 { + private final UserEndpoint userEndpoint; private final VersionEndpoint versionEndpoint; @Inject - public HttpApiInterfaceV1(VersionEndpoint versionEndpoint) { + public HttpApiInterfaceV1(UserEndpoint userEndpoint, VersionEndpoint versionEndpoint) { + this.userEndpoint = userEndpoint; this.versionEndpoint = versionEndpoint; } + @Path("user") + public UserEndpoint getUserEndpoint() { + return userEndpoint; + } + @Path("version") public VersionEndpoint getVersionEndpoint() { return versionEndpoint; diff --git a/api/src/main/java/bisq/api/http/service/HttpApiServer.java b/api/src/main/java/bisq/api/http/service/HttpApiServer.java index 9822ab499ac..02aac14a4dc 100644 --- a/api/src/main/java/bisq/api/http/service/HttpApiServer.java +++ b/api/src/main/java/bisq/api/http/service/HttpApiServer.java @@ -1,11 +1,20 @@ package bisq.api.http.service; +import bisq.api.http.exceptions.ExceptionMappers; +import bisq.api.http.service.auth.ApiPasswordManager; +import bisq.api.http.service.auth.AuthFilter; +import bisq.api.http.service.auth.TokenRegistry; + import bisq.core.app.BisqEnvironment; +import javax.servlet.DispatcherType; + import javax.inject.Inject; import java.net.InetSocketAddress; +import java.util.EnumSet; + import lombok.extern.slf4j.Slf4j; @@ -16,6 +25,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.server.ResourceConfig; @@ -26,12 +36,16 @@ public class HttpApiServer { private final HttpApiInterfaceV1 httpApiInterfaceV1; private final BisqEnvironment bisqEnvironment; + private final TokenRegistry tokenRegistry; + private final ApiPasswordManager apiPasswordManager; @Inject - public HttpApiServer(HttpApiInterfaceV1 httpApiInterfaceV1, - BisqEnvironment bisqEnvironment) { - this.httpApiInterfaceV1 = httpApiInterfaceV1; + public HttpApiServer(ApiPasswordManager apiPasswordManager, BisqEnvironment bisqEnvironment, HttpApiInterfaceV1 httpApiInterfaceV1, + TokenRegistry tokenRegistry) { + this.apiPasswordManager = apiPasswordManager; this.bisqEnvironment = bisqEnvironment; + this.httpApiInterfaceV1 = httpApiInterfaceV1; + this.tokenRegistry = tokenRegistry; } public void startServer() { @@ -52,11 +66,13 @@ public void startServer() { private ContextHandler buildAPIHandler() { ResourceConfig resourceConfig = new ResourceConfig(); + ExceptionMappers.register(resourceConfig); resourceConfig.register(httpApiInterfaceV1); resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources"); ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS | ServletContextHandler.NO_SECURITY); servletContextHandler.setContextPath("/"); servletContextHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*"); + setupAuth(servletContextHandler); return servletContextHandler; } @@ -77,4 +93,9 @@ private ContextHandler buildSwaggerUIHandler() throws Exception { swaggerUIContext.setHandler(swaggerUIResourceHandler); return swaggerUIContext; } + + private void setupAuth(ServletContextHandler appContextHandler) { + AuthFilter authFilter = new AuthFilter(apiPasswordManager, tokenRegistry); + appContextHandler.addFilter(new FilterHolder(authFilter), "/*", EnumSet.allOf(DispatcherType.class)); + } } diff --git a/api/src/main/java/bisq/api/http/service/auth/ApiPasswordManager.java b/api/src/main/java/bisq/api/http/service/auth/ApiPasswordManager.java new file mode 100644 index 00000000000..c8db1a40bf6 --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/auth/ApiPasswordManager.java @@ -0,0 +1,135 @@ +package bisq.api.http.service.auth; + +import bisq.api.http.exceptions.UnauthorizedException; + +import bisq.core.app.BisqEnvironment; + +import bisq.common.crypto.Hash; + +import com.google.inject.Inject; + +import java.security.SecureRandom; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.IOException; + +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + + +@Slf4j +public class ApiPasswordManager { + + private static final String SEPARATOR = ":"; + + private final Path passwordFilePath; + private final TokenRegistry tokenRegistry; + private final SecureRandom secureRandom; + private boolean passwordSet; + private String salt; + private byte[] passwordDigest; + + @Inject + public ApiPasswordManager(BisqEnvironment bisqEnvironment, TokenRegistry tokenRegistry) { + this.tokenRegistry = tokenRegistry; + String appDataDir = bisqEnvironment.getAppDataDir(); + this.passwordFilePath = Paths.get(appDataDir).resolve("apipasswd"); + this.secureRandom = new SecureRandom(); + readPasswordFromFile(); + } + + public boolean isPasswordSet() { + return passwordSet; + } + + public String authenticate(String password) { + final boolean passwordValid = isPasswordValid(password); + if (!passwordValid) + throw new UnauthorizedException(); + + return tokenRegistry.generateToken(); + } + + @Nullable + public String changePassword(@Nullable String oldPassword, @Nullable String newPassword) { + if (passwordSet && (oldPassword == null || !isPasswordValid(oldPassword))) throw new UnauthorizedException(); + if (newPassword != null && newPassword.length() > 0) { + salt = "" + secureRandom.nextLong(); + passwordDigest = getBytesForSaltedPassword(newPassword); + passwordSet = true; + writePasswordFile(); + tokenRegistry.clear(); + return tokenRegistry.generateToken(); + } else { + passwordSet = false; + removePasswordFile(); + } + return null; + } + + private void removePasswordFile() { + try { + Files.delete(passwordFilePath); + } catch (IOException e) { + throw new RuntimeException("Unable to remove password file: " + passwordFilePath, e); + } + } + + private boolean isPasswordValid(String password) { + final byte[] sha256Hash = getBytesForSaltedPassword(password); + return Arrays.equals(sha256Hash, passwordDigest); + } + + private byte[] getBytesForSaltedPassword(String password) { + final StringBuilder stringBuilder = new StringBuilder(password); + if (salt != null) { + stringBuilder.append(salt); + } + return Hash.getSha256Hash(stringBuilder.toString()); + } + + private void writePasswordFile() { + final String line = salt + SEPARATOR + new String(Base64.getEncoder().encode(passwordDigest)); + try { + Files.write(passwordFilePath, Collections.singleton(line)); + } catch (IOException e) { + throw new RuntimeException("Unable to write password file: " + passwordFilePath, e); + } + } + + private void readPasswordFromFile() { + passwordSet = false; + if (!Files.exists(this.passwordFilePath)) { + return; + } + try { + final List lines = Files.readAllLines(this.passwordFilePath); + final int linesCount = lines.size(); + if (linesCount != 1) { + log.warn("API password file is corrupt. Expected to have 1 line, found {}", linesCount); + return; + } + final String line = lines.get(0); + final String[] segments = line.split(SEPARATOR); + if (segments.length != 2) { + log.warn("API password file is corrupt. Expected 2 segments, found {}", segments.length); + return; + } + passwordSet = true; + this.salt = segments[0]; + this.passwordDigest = Base64.getDecoder().decode(segments[1]); + + } catch (IOException e) { + throw new RuntimeException("Unable to read api password file", e); + } + } +} diff --git a/api/src/main/java/bisq/api/http/service/auth/AuthFilter.java b/api/src/main/java/bisq/api/http/service/auth/AuthFilter.java new file mode 100644 index 00000000000..e086a79308b --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/auth/AuthFilter.java @@ -0,0 +1,60 @@ +package bisq.api.http.service.auth; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class AuthFilter implements Filter { + private final TokenRegistry tokenRegistry; + private final ApiPasswordManager apiPasswordManager; + + + public AuthFilter(ApiPasswordManager apiPasswordManager, TokenRegistry tokenRegistry) { + this.apiPasswordManager = apiPasswordManager; + this.tokenRegistry = tokenRegistry; + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + String pathInfo = httpServletRequest.getPathInfo(); + if (!pathInfo.startsWith("/api") || pathInfo.endsWith("/user/authenticate") || pathInfo.endsWith("/user/password")) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!apiPasswordManager.isPasswordSet()) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + String authorizationHeader = httpServletRequest.getHeader("authorization"); + if (authorizationHeader == null) { + respondWithUnauthorizedStatus(httpServletResponse); + return; + } + if (tokenRegistry.isValidToken(authorizationHeader)) + filterChain.doFilter(servletRequest, servletResponse); + else + respondWithUnauthorizedStatus(httpServletResponse); + } + + @Override + public void destroy() { + } + + private void respondWithUnauthorizedStatus(HttpServletResponse httpServletResponse) { + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/api/src/main/java/bisq/api/http/service/auth/TokenRegistry.java b/api/src/main/java/bisq/api/http/service/auth/TokenRegistry.java new file mode 100644 index 00000000000..9c620eacfc4 --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/auth/TokenRegistry.java @@ -0,0 +1,63 @@ +package bisq.api.http.service.auth; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class TokenRegistry { + + public static final long TTL = 30 * 60 * 1000; + + private final TimeProvider timeProvider; + private final RandomStringGenerator randomStringGenerator; + private Map tokens = new HashMap<>(); + + public TokenRegistry() { + this(() -> UUID.randomUUID().toString(), System::currentTimeMillis); + } + + public TokenRegistry(RandomStringGenerator randomStringGenerator, TimeProvider timeProvider) { + this.timeProvider = timeProvider; + this.randomStringGenerator = randomStringGenerator; + } + + public String generateToken() { + String uuid; + do { + uuid = randomStringGenerator.generateRandomString(); + } while (tokens.containsKey(uuid)); + tokens.put(uuid, timeProvider.getTime()); + removeTimeoutTokens(); + return uuid; + } + + boolean isValidToken(String token) { + Long createDate = tokens.get(token); + if (createDate == null || isTimeout(createDate)) { + tokens.remove(token); + return false; + } else { + return true; + } + } + + private boolean isTimeout(Long createDate) { + return timeProvider.getTime() - createDate > TTL; + } + + private void removeTimeoutTokens() { + tokens.values().removeIf(this::isTimeout); + } + + public void clear() { + tokens.clear(); + } + + public interface TimeProvider { + Long getTime(); + } + + public interface RandomStringGenerator { + String generateRandomString(); + } +} diff --git a/api/src/main/java/bisq/api/http/service/endpoint/UserEndpoint.java b/api/src/main/java/bisq/api/http/service/endpoint/UserEndpoint.java new file mode 100644 index 00000000000..be19f212187 --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/endpoint/UserEndpoint.java @@ -0,0 +1,73 @@ +package bisq.api.http.service.endpoint; + +import bisq.api.http.model.AuthForm; +import bisq.api.http.model.AuthResult; +import bisq.api.http.model.ChangePassword; +import bisq.api.http.service.auth.ApiPasswordManager; + +import bisq.common.UserThread; + +import javax.inject.Inject; + + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + + +@Tag(name = "user") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class UserEndpoint { + + private final ApiPasswordManager apiPasswordManager; + + @Inject + public UserEndpoint(ApiPasswordManager apiPasswordManager) { + this.apiPasswordManager = apiPasswordManager; + } + + @Operation(summary = "Exchange password for access token", responses = @ApiResponse(content = @Content(schema = @Schema(implementation = AuthResult.class)))) + @POST + @Path("/authenticate") + public void authenticate(@Suspended AsyncResponse asyncResponse, @Valid AuthForm authForm) { + UserThread.execute(() -> { + try { + final String token = apiPasswordManager.authenticate(authForm.password); + asyncResponse.resume(new AuthResult(token)); + } catch (Throwable e) { + asyncResponse.resume(e); + } + }); + } + + @Operation(summary = "Change password", responses = @ApiResponse(content = @Content(schema = @Schema(implementation = AuthResult.class)))) + @POST + @Path("/password") + public void changePassword(@Suspended AsyncResponse asyncResponse, @Valid ChangePassword data) { + UserThread.execute(() -> { + try { + final String token = apiPasswordManager.changePassword(data.oldPassword, data.newPassword); + if (token == null) { + asyncResponse.resume(Response.noContent().build()); + } else { + asyncResponse.resume(new AuthResult(token)); + } + } catch (Throwable e) { + asyncResponse.resume(e); + } + }); + } +} diff --git a/api/src/test/java/bisq/api/http/service/auth/ApiPasswordManagerTest.java b/api/src/test/java/bisq/api/http/service/auth/ApiPasswordManagerTest.java new file mode 100644 index 00000000000..d62efe13744 --- /dev/null +++ b/api/src/test/java/bisq/api/http/service/auth/ApiPasswordManagerTest.java @@ -0,0 +1,391 @@ +package bisq.api.http.service.auth; + +import bisq.api.http.exceptions.UnauthorizedException; + +import bisq.core.app.BisqEnvironment; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; + +import org.jetbrains.annotations.NotNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + + +import com.github.javafaker.Faker; + +public class ApiPasswordManagerTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private String dataDir; + private BisqEnvironment bisqEnvironmentMock; + private TokenRegistry tokenRegistryMock; + private ApiPasswordManager apiPasswordManager; + + private static String getRandomPasswordDifferentThan(String otherPassword) { + String newPassword; + do { + newPassword = Faker.instance().internet().password(); + } while (otherPassword.equals(newPassword)); + return newPassword; + } + + @Before + public void setUp() throws Exception { + this.bisqEnvironmentMock = mock(BisqEnvironment.class); + this.tokenRegistryMock = new TokenRegistry(); + this.dataDir = createTempDirectory(); + when(bisqEnvironmentMock.getAppDataDir()).thenReturn(dataDir); + assertPasswordFileDoesNotExist(); + } + + @After + public void tearDown() throws IOException { + FileUtils.deleteDirectory(new File(this.dataDir)); + } + + @Test + public void constructor_passwordFileNotReadable_throwsException() { + // Given + File invalidPasswordFile = getPasswordFile(); + assertTrue(invalidPasswordFile.mkdir()); + invalidPasswordFile.deleteOnExit(); + expectedException.expect(RuntimeException.class); + expectedException.expectMessage("Unable to read api password file"); + + // When + new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + } + + @Test + public void constructor_passwordFileContainsMoreThan2Lines_doesNotSetPassword() throws IOException { + // Given + writePasswordFile("a:b\nd:e"); + + // When + ApiPasswordManager apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + boolean passwordSet = apiPasswordManager.isPasswordSet(); + + // Then + assertFalse(passwordSet); + } + + @Test + public void constructor_passwordFileContainsMoreThan2Separators_doesNotSetPassword() throws IOException { + // Given + writePasswordFile("a:b:e"); + + // When + ApiPasswordManager apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + boolean passwordSet = apiPasswordManager.isPasswordSet(); + + // Then + assertFalse(passwordSet); + } + + @Test + public void isPasswordSet_noPasswordFile_returnsFalse() { + // Given + + // When + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // Then + assertFalse(apiPasswordManager.isPasswordSet()); + } + + @Test + public void isPasswordSet_passwordFileExists_returnsTrue() { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String newPassword = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, newPassword); + + // When + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // Then + assertTrue(this.apiPasswordManager.isPasswordSet()); + assertTrue(anotherPasswordManager.isPasswordSet()); + } + + @Test + public void authenticate_noPasswordFile_throwsUnauthorizedException() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + assertPasswordFileDoesNotExist(); + + // When + apiPasswordManager.authenticate(getRandomPasswordDifferentThan("")); + } + + @Test + public void authenticate_passwordDoesNotMatch_throwsUnauthorizedException() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + + // When + apiPasswordManager.authenticate(getRandomPasswordDifferentThan(password)); + } + + @Test + public void authenticate_passwordDoesNotMatchAndDifferentPasswordManagerInstance_throwsUnauthorizedException() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // When + anotherPasswordManager.authenticate(getRandomPasswordDifferentThan(password)); + } + + @Test + public void authenticate_passwordMatches_returnsTokenFromTokenRegistry() { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // When + String token = anotherPasswordManager.authenticate(password); + + // Then + assertNotNull(token); + assertTrue(tokenRegistryMock.isValidToken(token)); + } + + @Test + public void changePassword_noPasswordFileAndNewPasswordSet_returnsTokenFromRegistry() { + // Given + assertPasswordFileDoesNotExist(); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + + // When + String token = apiPasswordManager.changePassword(null, password); + + // Then + assertNotNull(token); + assertTrue(tokenRegistryMock.isValidToken(token)); + assertNotNull(apiPasswordManager.authenticate(password)); + } + + @Test + public void changePassword_noPasswordFileAndNewPasswordSet_changesThePassword() { + // Given + assertPasswordFileDoesNotExist(); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + + // When + apiPasswordManager.changePassword(null, password); + String token = apiPasswordManager.authenticate(password); + + // Then + assertTrue(tokenRegistryMock.isValidToken(token)); + } + + @Test + public void changePassword_noPasswordFileAndNewPasswordSet_storesThePasswordInPasswordFile() { + // Given + assertPasswordFileDoesNotExist(); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + + // When + apiPasswordManager.changePassword(null, password); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String token = anotherPasswordManager.authenticate(password); + + // Then + assertPasswordFileExists(); + assertTrue(tokenRegistryMock.isValidToken(token)); + } + + @Test + public void changePassword_passwordDoesNotMatch_throwsUnauthorizedException() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String invalidPassword = getRandomPasswordDifferentThan(password); + String newPassword = getRandomPasswordDifferentThan(password); + + // When + anotherPasswordManager.changePassword(invalidPassword, newPassword); + } + + @Test + public void changePassword_oldPasswordIsNull_throwsUnauthorizedException() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String newPassword = getRandomPasswordDifferentThan(password); + + // When + anotherPasswordManager.changePassword(null, newPassword); + } + + @Test + public void changePassword_oldPasswordMatchesTheOneInPasswordFileAndNewPasswordSet_changesThePasswordInPasswordFile() { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + String newPassword = getRandomPasswordDifferentThan(password); + + // When + apiPasswordManager.changePassword(password, newPassword); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String token = anotherPasswordManager.authenticate(newPassword); + + // Then + assertTrue(tokenRegistryMock.isValidToken(token)); + } + + @Test + public void changePassword_oldPasswordMatchesTheOneInPasswordFileAndNewPasswordSet_oldPasswordBecomesInvalid() { + // Given + expectedException.expect(UnauthorizedException.class); + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + String newPassword = getRandomPasswordDifferentThan(password); + + // When + apiPasswordManager.changePassword(password, newPassword); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + anotherPasswordManager.authenticate(password); + } + + @Test + public void changePassword_oldPasswordMatchesTheOneInPasswordFileAndNewPasswordIsNull_unSetsPassword() { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + + // When + apiPasswordManager.changePassword(password, null); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // Then + assertFalse(apiPasswordManager.isPasswordSet()); + assertFalse(anotherPasswordManager.isPasswordSet()); + expectedException.expect(UnauthorizedException.class); + anotherPasswordManager.authenticate(password); + } + + @Test + public void changePassword_oldPasswordMatchesTheOneInPasswordFileAndNewPasswordIsEmptyString_unSetsPassword() { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + + // When + apiPasswordManager.changePassword(password, ""); + ApiPasswordManager anotherPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + + // Then + assertFalse(apiPasswordManager.isPasswordSet()); + assertFalse(anotherPasswordManager.isPasswordSet()); + expectedException.expect(UnauthorizedException.class); + anotherPasswordManager.authenticate(password); + } + + @Test + public void changePassword_newPasswordNullButPasswordFileNotWritable_throwsException() throws IOException { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + File passwordFile = getPasswordFile(); + assertTrue(passwordFile.delete()); + assertTrue(passwordFile.mkdir()); + assertNotNull(File.createTempFile("bisq", "api", passwordFile)); + expectedException.expect(RuntimeException.class); + expectedException.expectMessage("Unable to remove password file: " + passwordFile.getAbsolutePath()); + + // When + apiPasswordManager.changePassword(password, null); + } + + @Test + public void changePassword_passwordFileNotWritable_throwsException() throws IOException { + // Given + apiPasswordManager = new ApiPasswordManager(bisqEnvironmentMock, tokenRegistryMock); + String password = getRandomPasswordDifferentThan(""); + apiPasswordManager.changePassword(null, password); + File passwordFile = getPasswordFile(); + assertTrue(passwordFile.delete()); + assertTrue(passwordFile.mkdir()); + assertNotNull(File.createTempFile("bisq", "api", passwordFile)); + expectedException.expect(RuntimeException.class); + expectedException.expectMessage("Unable to write password file: " + passwordFile.getAbsolutePath()); + + // When + apiPasswordManager.changePassword(password, getRandomPasswordDifferentThan(password)); + } + + private boolean passwordFileExists() { + return getPasswordFile().exists(); + } + + private void assertPasswordFileDoesNotExist() { + assertFalse(passwordFileExists()); + } + + private void assertPasswordFileExists() { + assertTrue(passwordFileExists()); + } + + private String createTempDirectory() throws IOException { + File tempFile = File.createTempFile("bisq", "api"); + if (!tempFile.delete()) { + throw new RuntimeException("Unable to create temporary directory: " + tempFile.getAbsolutePath()); + } + if (!tempFile.mkdir()) { + throw new RuntimeException("Unable to create temporary directory: " + tempFile.getAbsolutePath()); + } + return tempFile.getAbsolutePath(); + } + + @NotNull + private File getPasswordFile() { + return new File(this.dataDir, "apipasswd"); + } + + private void writePasswordFile(String data) throws IOException { + File passwordFile = getPasswordFile(); + passwordFile.deleteOnExit(); + FileUtils.write(passwordFile, data, "UTF-8"); + } +} diff --git a/api/src/test/java/bisq/api/http/service/auth/AuthFilterTest.java b/api/src/test/java/bisq/api/http/service/auth/AuthFilterTest.java new file mode 100644 index 00000000000..43176903835 --- /dev/null +++ b/api/src/test/java/bisq/api/http/service/auth/AuthFilterTest.java @@ -0,0 +1,162 @@ +package bisq.api.http.service.auth; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + + +import com.github.javafaker.Faker; + +public class AuthFilterTest { + + private TokenRegistry tokenRegistryMock; + private ApiPasswordManager apiPasswordManagerMock; + private AuthFilter authFilter; + private HttpServletRequest servletRequestMock; + private HttpServletResponse servletResponseMock; + private FilterChain filterChainMock; + + @Before + public void setUp() { + tokenRegistryMock = mock(TokenRegistry.class); + apiPasswordManagerMock = mock(ApiPasswordManager.class); + authFilter = new AuthFilter(apiPasswordManagerMock, tokenRegistryMock); + + servletRequestMock = mock(HttpServletRequest.class); + servletResponseMock = mock(HttpServletResponse.class); + filterChainMock = mock(FilterChain.class); + } + + @Test + public void doFilter_passwordNotSet_passThrough() throws Exception { + // Given + when(servletRequestMock.getPathInfo()).thenReturn("/api/version"); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void doFilter_invalidAuthorizationToken_forbid() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/api/version"); + String invalidToken = Faker.instance().crypto().md5(); + when(servletRequestMock.getHeader("authorization")).thenReturn(invalidToken); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock, never()).doFilter(any(), any()); + verify(servletResponseMock).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void doFilter_missingAuthorizationToken_forbid() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/api/version"); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock, never()).doFilter(any(), any()); + verify(servletResponseMock).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void doFilter_tokenInvalidButAuthenticationPath_passThrough() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/api/user/authenticate"); + String invalidToken = Faker.instance().crypto().md5(); + when(servletRequestMock.getHeader("authorization")).thenReturn(invalidToken); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void doFilter_tokenInvalidButPasswordChangePath_passThrough() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/api/user/password"); + String invalidToken = Faker.instance().crypto().md5(); + when(servletRequestMock.getHeader("authorization")).thenReturn(invalidToken); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void doFilter_tokenInvalidButNonApiPath_passThrough() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/docs"); + String invalidToken = Faker.instance().crypto().md5(); + when(servletRequestMock.getHeader("authorization")).thenReturn(invalidToken); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void doFilter_tokenValid_passThrough() throws Exception { + // Given + when(apiPasswordManagerMock.isPasswordSet()).thenReturn(true); + when(servletRequestMock.getPathInfo()).thenReturn("/api/version"); + String token = Faker.instance().crypto().md5(); + when(tokenRegistryMock.isValidToken(token)).thenReturn(true); + when(servletRequestMock.getHeader("authorization")).thenReturn(token); + + // When + authFilter.doFilter(servletRequestMock, servletResponseMock, filterChainMock); + + // Then + verify(filterChainMock).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void destroy_always_doesNothing() { + // Given + + // When + authFilter.destroy(); + + // Then + } + + @Test + public void destroy_init_doesNothing() { + // Given + + // When + authFilter.init(null); + + // Then + } +} diff --git a/api/src/test/java/bisq/api/http/service/auth/TokenRegistryTest.java b/api/src/test/java/bisq/api/http/service/auth/TokenRegistryTest.java new file mode 100644 index 00000000000..a8083f7da30 --- /dev/null +++ b/api/src/test/java/bisq/api/http/service/auth/TokenRegistryTest.java @@ -0,0 +1,116 @@ +package bisq.api.http.service.auth; + +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TokenRegistryTest { + + private TokenRegistry tokenRegistry; + + @Before + public void setUp() { + tokenRegistry = new TokenRegistry(); + } + + @Test + public void generateToken_always_returnsNewValidToken() { + // Given + + // When + String token1 = tokenRegistry.generateToken(); + String token2 = tokenRegistry.generateToken(); + + // Then + assertNotNull(token1); + assertNotNull(token2); + assertNotEquals(token1, token2); + assertTrue(tokenRegistry.isValidToken(token1)); + assertTrue(tokenRegistry.isValidToken(token2)); + } + + @Test + public void generateToken_always_returnsNewUniqueToken() { + // Given + TokenRegistry.RandomStringGenerator randomStringGeneratorMock = mock(TokenRegistry.RandomStringGenerator.class); + tokenRegistry = new TokenRegistry(randomStringGeneratorMock, System::currentTimeMillis); + when(randomStringGeneratorMock.generateRandomString()) + .thenReturn("a") + .thenReturn("a") + .thenReturn("b"); + + // When + String token1 = tokenRegistry.generateToken(); + String token2 = tokenRegistry.generateToken(); + + // Then + assertEquals("a", token1); + assertEquals("b", token2); + assertTrue(tokenRegistry.isValidToken(token1)); + assertTrue(tokenRegistry.isValidToken(token2)); + } + + @Test + public void generateToken_always_removesExpiredTokens() { + // Given + TokenRegistry.TimeProvider timeProviderMock = mock(TokenRegistry.TimeProvider.class); + tokenRegistry = new TokenRegistry(() -> UUID.randomUUID().toString(), timeProviderMock); + when(timeProviderMock.getTime()).thenReturn(0L); + String token1 = tokenRegistry.generateToken(); + when(timeProviderMock.getTime()).thenReturn(TokenRegistry.TTL + 1); + + // When + String token2 = tokenRegistry.generateToken(); + when(timeProviderMock.getTime()).thenReturn(TokenRegistry.TTL + 1 + TokenRegistry.TTL); + + // Then + assertTrue(tokenRegistry.isValidToken(token2)); + assertFalse(tokenRegistry.isValidToken(token1)); + } + + @Test + public void isValidToken_invalidToken_returnsFalse() { + // Given + + // When + boolean result = tokenRegistry.isValidToken(UUID.randomUUID().toString()); + + // Then + assertFalse(result); + } + + @Test + public void isValidToken_expiredToken_returnsFalse() { + // Given + TokenRegistry.TimeProvider timeProviderMock = mock(TokenRegistry.TimeProvider.class); + tokenRegistry = new TokenRegistry(() -> UUID.randomUUID().toString(), timeProviderMock); + when(timeProviderMock.getTime()).thenReturn(0L); + String token = tokenRegistry.generateToken(); + when(timeProviderMock.getTime()).thenReturn(TokenRegistry.TTL + 1); + + // When + boolean result = tokenRegistry.isValidToken(token); + + // Then + assertFalse(result); + } + + @Test + public void clear_always_removesAllTokens() { + // Given + String token1 = tokenRegistry.generateToken(); + String token2 = tokenRegistry.generateToken(); + + // When + tokenRegistry.clear(); + + // Then + assertFalse(tokenRegistry.isValidToken(token1)); + assertFalse(tokenRegistry.isValidToken(token2)); + } +} diff --git a/api/src/testIntegration/java/bisq/api/http/UserEndpointIT.java b/api/src/testIntegration/java/bisq/api/http/UserEndpointIT.java new file mode 100644 index 00000000000..4c2838c24ca --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/UserEndpointIT.java @@ -0,0 +1,317 @@ +package bisq.api.http; + +import bisq.api.http.model.AuthForm; +import bisq.api.http.model.AuthResult; +import bisq.api.http.model.ChangePassword; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + + + +import com.github.javafaker.Faker; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class UserEndpointIT { + + private static String validPassword = new Faker().internet().password(); + private static String invalidPassword = getRandomPasswordDifferentThan(validPassword); + private static String accessToken; + @DockerContainer + Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + private static String getRandomPasswordDifferentThan(String otherPassword) { + String newPassword; + do { + newPassword = new Faker().internet().password(); + } while (otherPassword.equals(newPassword)); + return newPassword; + } + + @InSequence + @Test + public void waitForAllServicesToBeReady() throws InterruptedException { + ApiTestHelper.waitForAllServicesToBeReady(); + verifyThatAuthenticationIsDisabled(); + } + + @InSequence(1) + @Test + public void authenticate_noPasswordSet_returns401() { + int alicePort = getAlicePort(); + given(). + port(alicePort). + body(new AuthForm(validPassword)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(401) + ; + } + + @InSequence(1) + @Test + public void authenticate_badJson_returns400() { + int alicePort = getAlicePort(); + given(). + port(alicePort). + body("{"). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(400) + ; + } + + @InSequence(2) + @Test + public void changePassword_settingFirstPassword_enablesAuthentication() { + int alicePort = getAlicePort(); + accessToken = given(). + port(alicePort). + body(new ChangePassword(validPassword, null)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(200). + and().body("token", isA(String.class)). + extract().as(AuthResult.class).token; + verifyThatAuthenticationIsEnabled(); + verifyThatAccessTokenIsValid(accessToken); + } + + @InSequence(3) + @Test + public void changePassword_invalidOldPassword_returns401() { + int alicePort = getAlicePort(); + String newPassword = getRandomPasswordDifferentThan(validPassword); + given(). + port(alicePort). + body(new ChangePassword(newPassword, invalidPassword)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(401) + ; + verifyThatAuthenticationIsEnabled(); + verifyThatAccessTokenIsValid(accessToken); + verifyThatPasswordIsValid(validPassword); + verifyThatPasswordIsInvalid(newPassword); + } + + @InSequence(3) + @Test + public void changePassword_emptyOldPassword_returns401() { + int alicePort = getAlicePort(); + String newPassword = getRandomPasswordDifferentThan(validPassword); + given(). + port(alicePort). + body(new ChangePassword(newPassword, null)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(401) + ; + verifyThatAuthenticationIsEnabled(); + verifyThatAccessTokenIsValid(accessToken); + verifyThatPasswordIsValid(validPassword); + verifyThatPasswordIsInvalid(newPassword); + } + + @InSequence(4) + @Test + public void authenticate_invalidCredentials_returns401() { + int alicePort = getAlicePort(); + given(). + port(alicePort). + body(new AuthForm(invalidPassword)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(401) + ; + } + + @InSequence(4) + @Test + public void authenticate_invalidCredentials_returnsNoAccessToken() { + int alicePort = getAlicePort(); + String responseBody = given(). + port(alicePort). + body(new AuthForm(invalidPassword)). + accept(ContentType.JSON). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + extract().asString(); + assertEquals("", responseBody); + } + + @InSequence(5) + @Test + public void authenticate_validCredentials_returnsAccessToken() { + String token = authenticate(validPassword); + verifyThatAccessTokenIsValid(token); + String anotherToken = authenticate(validPassword); + + verifyThatAccessTokenIsValid(accessToken); + verifyThatAccessTokenIsValid(token); + verifyThatAccessTokenIsValid(anotherToken); + } + + @InSequence(5) + @Test + public void authenticate_validCredentials_returnsDifferentAccessTokenEachTime() { + String token = authenticate(validPassword); + String anotherToken = authenticate(validPassword); + + assertNotEquals(accessToken, token); + assertNotEquals(accessToken, anotherToken); + assertNotEquals(token, anotherToken); + } + + @InSequence(6) + @Test + public void changePassword_settingAnotherPassword_keepsAuthenticationEnabled() { + int alicePort = getAlicePort(); + String oldPassword = validPassword; + String newPassword = getRandomPasswordDifferentThan(validPassword); + validPassword = newPassword; + invalidPassword = getRandomPasswordDifferentThan(validPassword); + String oldAccessToken = accessToken; + accessToken = given(). + port(alicePort). + body(new ChangePassword(newPassword, oldPassword)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(200). + and().body("token", isA(String.class)). + extract().as(AuthResult.class).token + ; + verifyThatPasswordIsInvalid(oldPassword); + verifyThatPasswordIsValid(newPassword); + verifyThatAuthenticationIsEnabled(); + verifyThatAccessTokenIsInvalid(oldAccessToken); + verifyThatAccessTokenIsValid(accessToken); + } + + @InSequence(7) + @Test + public void changePassword_validOldPasswordAndNoNewPassword_disablesAuthentication() { + int alicePort = getAlicePort(); + given(). + port(alicePort). + body(new ChangePassword(null, validPassword)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(204) + ; + verifyThatAuthenticationIsDisabled(); + } + + private String authenticate(String password) { + int alicePort = getAlicePort(); + return given(). + port(alicePort). + body(new AuthForm(password)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(200). + and().body("token", isA(String.class)). + extract().as(AuthResult.class).token; + } + + private void verifyThatAccessTokenIsValid(String accessToken) { + accessTokenVerificationRequest(accessToken).then().statusCode(200); + } + + private void verifyThatAccessTokenIsInvalid(String accessToken) { + accessTokenVerificationRequest(accessToken).then().statusCode(401); + } + + private void verifyThatAuthenticationIsDisabled() { + authenticationVerificationRequest().then().statusCode(200); + } + + private void verifyThatAuthenticationIsEnabled() { + authenticationVerificationRequest().then().statusCode(401); + } + + private void verifyThatPasswordIsInvalid(String password) { + int alicePort = getAlicePort(); + given(). + port(alicePort). + body(new AuthForm(password)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(401) + ; + } + + private void verifyThatPasswordIsValid(String password) { + authenticate(password); + } + + private Response authenticationVerificationRequest() { + int alicePort = getAlicePort(); + return given().port(alicePort).when().get("/api/v1/version"); + } + + private Response accessTokenVerificationRequest(String accessToken) { + int alicePort = getAlicePort(); + return given().port(alicePort).header("authorization", accessToken).when().get("/api/v1/version"); + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/build.gradle b/build.gradle index 4e12f5d5ba4..922854ca6c9 100644 --- a/build.gradle +++ b/build.gradle @@ -299,14 +299,11 @@ configure(project(':api')) { annotationProcessor 'org.projectlombok:lombok:1.18.2' testCompile 'junit:junit:4.12' - testCompile('org.mockito:mockito-core:2.8.9') { - exclude(module: 'objenesis') - } + testCompile "org.mockito:mockito-core:$mockitoVersion" testCompileOnly 'org.projectlombok:lombok:1.18.2' testAnnotationProcessor 'org.projectlombok:lombok:1.18.2' - testCompile "junit:junit:4.12" - testCompile "org.mockito:mockito-core:2.7.5" testCompile "com.github.javafaker:javafaker:0.14" + testCompile "org.apache.commons:commons-lang3:$langVersion" testCompile "org.arquillian.universe:arquillian-junit:1.2.0.1" testCompile "org.arquillian.universe:arquillian-cube-docker:1.2.0.1" testCompile "org.arquillian.cube:arquillian-cube-docker:1.15.3" From b626b92c5ef5a97ba57b97af0a2408f664a6bd46 Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 19 Jun 2019 10:47:27 +0200 Subject: [PATCH 7/8] API over TOR --- .../bisq/api/http/service/HttpApiServer.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/bisq/api/http/service/HttpApiServer.java b/api/src/main/java/bisq/api/http/service/HttpApiServer.java index 02aac14a4dc..7e69472c826 100644 --- a/api/src/main/java/bisq/api/http/service/HttpApiServer.java +++ b/api/src/main/java/bisq/api/http/service/HttpApiServer.java @@ -9,10 +9,16 @@ import javax.servlet.DispatcherType; +import org.berndpruenster.netlayer.tor.HsContainer; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; + import javax.inject.Inject; import java.net.InetSocketAddress; +import java.io.IOException; + import java.util.EnumSet; import lombok.extern.slf4j.Slf4j; @@ -31,13 +37,12 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; -@SuppressWarnings("Duplicates") @Slf4j public class HttpApiServer { - private final HttpApiInterfaceV1 httpApiInterfaceV1; + private final ApiPasswordManager apiPasswordManager; private final BisqEnvironment bisqEnvironment; + private final HttpApiInterfaceV1 httpApiInterfaceV1; private final TokenRegistry tokenRegistry; - private final ApiPasswordManager apiPasswordManager; @Inject public HttpApiServer(ApiPasswordManager apiPasswordManager, BisqEnvironment bisqEnvironment, HttpApiInterfaceV1 httpApiInterfaceV1, @@ -59,7 +64,8 @@ public void startServer() { server.setRequestLog(new Slf4jRequestLog()); server.start(); log.info("HTTP API started on {}", socketAddress); - } catch (Exception e) { + startTorIfNeeded(); + } catch (Exception | TorCtlException e) { throw new RuntimeException(e); } } @@ -98,4 +104,19 @@ private void setupAuth(ServletContextHandler appContextHandler) { AuthFilter authFilter = new AuthFilter(apiPasswordManager, tokenRegistry); appContextHandler.addFilter(new FilterHolder(authFilter), "/*", EnumSet.allOf(DispatcherType.class)); } + + /** + * If Bisq is configured to use start Tor then the default Tor instance should be available + * by the time this method is executed. + */ + private void startTorIfNeeded() throws IOException, TorCtlException { + Tor tor = Tor.getDefault(); + if (null == tor) { + log.info("Tor not started so API will be available only locally"); + return; + } + // TODO how to log that service has been published? + final HsContainer hsContainer = tor.publishHiddenService("api", 80, bisqEnvironment.getHttpApiPort()); + log.info("HTTP API Tor hostname: {}", hsContainer.getHostname()); + } } From 6f5f05f03511c1045d7ec863041c44e77a5a1da3 Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 19 Jun 2019 10:47:37 +0200 Subject: [PATCH 8/8] Payment account support for the API --- .../java/bisq/api/http/HttpApiModule.java | 2 + .../api/http/exceptions/ExceptionMappers.java | 102 ++ .../ExperimentalFeatureException.java | 7 + .../http/exceptions/NotFoundException.java | 7 + .../api/http/facade/PaymentAccountFacade.java | 54 + .../api/http/model/PaymentAccountList.java | 11 + .../AbstractPaymentAccountConverter.java | 70 + .../payment/AdvancedCashPaymentAccount.java | 20 + .../AdvancedCashPaymentAccountConverter.java | 32 + .../model/payment/AliPayPaymentAccount.java | 20 + .../AliPayPaymentAccountConverter.java | 32 + .../payment/CashDepositPaymentAccount.java | 43 + .../CashDepositPaymentAccountConverter.java | 51 + .../payment/ChaseQuickPayPaymentAccount.java | 23 + .../ChaseQuickPayPaymentAccountConverter.java | 34 + .../payment/ClearXchangePaymentAccount.java | 23 + .../ClearXchangePaymentAccountConverter.java | 34 + .../payment/CryptoCurrencyPaymentAccount.java | 20 + ...CryptoCurrencyPaymentAccountConverter.java | 32 + .../http/model/payment/F2FPaymentAccount.java | 25 + .../payment/F2FPaymentAccountConverter.java | 36 + .../payment/FasterPaymentsPaymentAccount.java | 23 + ...FasterPaymentsPaymentAccountConverter.java | 34 + .../model/payment/HalCashPaymentAccount.java | 20 + .../HalCashPaymentAccountConverter.java | 32 + .../InteracETransferPaymentAccount.java | 29 + ...teracETransferPaymentAccountConverter.java | 38 + .../payment/MoneyBeamPaymentAccount.java | 20 + .../MoneyBeamPaymentAccountConverter.java | 32 + .../payment/MoneyGramPaymentAccount.java | 31 + .../MoneyGramPaymentAccountConverter.java | 39 + .../NationalBankAccountPaymentAccount.java | 39 + ...nalBankAccountPaymentAccountConverter.java | 48 + .../http/model/payment/PaymentAccount.java | 104 ++ .../payment/PaymentAccountConverter.java | 13 + .../model/payment/PaymentAccountHelper.java | 77 + .../payment/PerfectMoneyPaymentAccount.java | 20 + .../PerfectMoneyPaymentAccountConverter.java | 32 + .../model/payment/PopmoneyPaymentAccount.java | 23 + .../PopmoneyPaymentAccountConverter.java | 35 + .../payment/PromptPayPaymentAccount.java | 20 + .../PromptPayPaymentAccountConverter.java | 32 + .../model/payment/RevolutPaymentAccount.java | 20 + .../RevolutPaymentAccountConverter.java | 33 + .../SameBankAccountPaymentAccount.java | 39 + ...ameBankAccountPaymentAccountConverter.java | 47 + .../payment/SepaInstantPaymentAccount.java | 39 + .../SepaInstantPaymentAccountConverter.java | 48 + .../model/payment/SepaPaymentAccount.java | 39 + .../payment/SepaPaymentAccountConverter.java | 48 + .../SpecificBanksAccountPaymentAccount.java | 46 + ...icBanksAccountPaymentAccountConverter.java | 54 + .../model/payment/SwishPaymentAccount.java | 23 + .../payment/SwishPaymentAccountConverter.java | 35 + .../USPostalMoneyOrderPaymentAccount.java | 23 + ...stalMoneyOrderPaymentAccountConverter.java | 34 + .../model/payment/UpholdPaymentAccount.java | 20 + .../UpholdPaymentAccountConverter.java | 33 + .../payment/WeChatPayPaymentAccount.java | 20 + .../WeChatPayPaymentAccountConverter.java | 33 + .../payment/WesternUnionPaymentAccount.java | 34 + .../WesternUnionPaymentAccountConverter.java | 42 + .../http/model/validation/CountryCode.java | 27 + .../validation/CountryCodeValidator.java | 20 + .../api/http/service/ExperimentalFeature.java | 25 + .../api/http/service/HttpApiInterfaceV1.java | 11 +- .../http/service/ValidationErrorMessage.java | 20 + .../endpoint/PaymentAccountEndpoint.java | 88 ++ .../validation/CountryCodeValidatorTest.java | 51 + .../java/bisq/api/http/ApiTestHelper.java | 45 + .../bisq/api/http/ExperimentalFeatureIT.java | 107 ++ .../api/http/PaymentAccountEndpointIT.java | 1236 +++++++++++++++++ .../core/exceptions/ValidationException.java | 39 + .../core/payment/PaymentAccountManager.java | 74 + .../validation/AltCoinAddressValidator.java | 12 +- .../main/java/bisq/core/user/Preferences.java | 2 +- .../payment/PaymentAccountManagerTest.java | 247 ++++ .../AltCoinAccountsDataModel.java | 34 +- .../fiataccounts/FiatAccountsDataModel.java | 37 +- 79 files changed, 4135 insertions(+), 69 deletions(-) create mode 100644 api/src/main/java/bisq/api/http/exceptions/ExperimentalFeatureException.java create mode 100644 api/src/main/java/bisq/api/http/exceptions/NotFoundException.java create mode 100644 api/src/main/java/bisq/api/http/facade/PaymentAccountFacade.java create mode 100644 api/src/main/java/bisq/api/http/model/PaymentAccountList.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/AbstractPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PaymentAccountHelper.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccount.java create mode 100644 api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccountConverter.java create mode 100644 api/src/main/java/bisq/api/http/model/validation/CountryCode.java create mode 100644 api/src/main/java/bisq/api/http/model/validation/CountryCodeValidator.java create mode 100644 api/src/main/java/bisq/api/http/service/ExperimentalFeature.java create mode 100644 api/src/main/java/bisq/api/http/service/ValidationErrorMessage.java create mode 100644 api/src/main/java/bisq/api/http/service/endpoint/PaymentAccountEndpoint.java create mode 100644 api/src/test/java/bisq/api/http/model/validation/CountryCodeValidatorTest.java create mode 100644 api/src/testIntegration/java/bisq/api/http/ExperimentalFeatureIT.java create mode 100644 api/src/testIntegration/java/bisq/api/http/PaymentAccountEndpointIT.java create mode 100644 core/src/main/java/bisq/core/exceptions/ValidationException.java create mode 100644 core/src/main/java/bisq/core/payment/PaymentAccountManager.java create mode 100644 core/src/test/java/bisq/core/payment/PaymentAccountManagerTest.java diff --git a/api/src/main/java/bisq/api/http/HttpApiModule.java b/api/src/main/java/bisq/api/http/HttpApiModule.java index 3130576b43c..7b38a8b3052 100644 --- a/api/src/main/java/bisq/api/http/HttpApiModule.java +++ b/api/src/main/java/bisq/api/http/HttpApiModule.java @@ -20,6 +20,7 @@ import bisq.api.http.service.HttpApiServer; import bisq.api.http.service.auth.ApiPasswordManager; import bisq.api.http.service.auth.TokenRegistry; +import bisq.api.http.service.endpoint.PaymentAccountEndpoint; import bisq.api.http.service.endpoint.UserEndpoint; import bisq.api.http.service.endpoint.VersionEndpoint; @@ -43,6 +44,7 @@ protected void configure() { bind(HttpApiServer.class).in(Singleton.class); bind(TokenRegistry.class).in(Singleton.class); bind(ApiPasswordManager.class).in(Singleton.class); + bind(PaymentAccountEndpoint.class).in(Singleton.class); bind(UserEndpoint.class).in(Singleton.class); bind(VersionEndpoint.class).in(Singleton.class); diff --git a/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java b/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java index 9580da65202..7556e1da7b7 100644 --- a/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java +++ b/api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java @@ -1,11 +1,25 @@ package bisq.api.http.exceptions; +import bisq.api.http.service.ValidationErrorMessage; + +import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; + +import com.google.common.collect.ImmutableList; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import javax.validation.ConstraintViolationException; +import javax.validation.Path; +import javax.validation.ValidationException; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import org.eclipse.jetty.io.EofException; @@ -20,9 +34,33 @@ private ExceptionMappers() { public static void register(ResourceConfig environment) { environment.register(new ExceptionMappers.EofExceptionMapper(), 1); environment.register(new ExceptionMappers.JsonParseExceptionMapper(), 1); + environment.register(new ExceptionMappers.BisqValidationExceptionMapper()); + environment.register(new ExceptionMappers.ExperimentalFeatureExceptionMapper()); + environment.register(new ExceptionMappers.InvalidTypeIdExceptionMapper()); + environment.register(new ExceptionMappers.NotFoundExceptionMapper()); + environment.register(new ExceptionMappers.ValidationExceptionMapper()); environment.register(new ExceptionMappers.UnauthorizedExceptionMapper()); } + private static Response toResponse(Throwable throwable, Response.Status status) { + Response.ResponseBuilder responseBuilder = Response.status(status); + String message = throwable.getMessage(); + if (message != null) { + responseBuilder.entity(new ValidationErrorMessage(ImmutableList.of(message))); + } + return responseBuilder.type(MediaType.APPLICATION_JSON).build(); + } + + public static class BisqValidationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(bisq.core.exceptions.ValidationException exception) { + Response.ResponseBuilder responseBuilder = Response.status(422); + String message = exception.getMessage(); + responseBuilder.entity(new ValidationErrorMessage(ImmutableList.of(message))); + return responseBuilder.build(); + } + } + public static class EofExceptionMapper implements ExceptionMapper { @Override public Response toResponse(EofException e) { @@ -37,6 +75,70 @@ public Response toResponse(JsonParseException e) { } } + public static class InvalidTypeIdExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(InvalidTypeIdException exception) { + Class rawClass = exception.getBaseType().getRawClass(); + StringBuilder builder = new StringBuilder("Unable to recognize sub type of ") + .append(rawClass.getSimpleName()) + .append(". Value '") + .append(exception.getTypeId()) + .append("' is invalid."); + + JsonSubTypes annotation = rawClass.getAnnotation(JsonSubTypes.class); + if (annotation != null && annotation.value().length > 0) { + builder.append(" Allowed values are: "); + String separator = ", "; + for (JsonSubTypes.Type subType : annotation.value()) + builder.append(subType.name()).append(separator); + builder.delete(builder.length() - separator.length(), builder.length()); + } + + return Response.status(422).entity(new ValidationErrorMessage(ImmutableList.of(builder.toString()))).build(); + } + } + + public static class ExperimentalFeatureExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ExperimentalFeatureException exception) { + return ExceptionMappers.toResponse(exception, Response.Status.NOT_IMPLEMENTED); + } + } + + public static class NotFoundExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(NotFoundException exception) { + return Response.status(404).entity(exception.getMessage()).type(MediaType.TEXT_PLAIN_TYPE).build(); + } + } + + public static class ValidationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ValidationException exception) { + Response.ResponseBuilder responseBuilder = Response.status(422); + String message = exception.getMessage(); + if (exception instanceof ConstraintViolationException) { + List messages = ((ConstraintViolationException) exception).getConstraintViolations().stream().map(constraintViolation -> { + StringBuilder stringBuilder = new StringBuilder(); + Path propertyPath = constraintViolation.getPropertyPath(); + if (propertyPath != null) { + Iterator pathIterator = constraintViolation.getPropertyPath().iterator(); + String node = null; + while (pathIterator.hasNext()) + node = pathIterator.next().getName(); + if (node != null) + stringBuilder.append(node).append(" "); + } + return stringBuilder.append(constraintViolation.getMessage()).toString(); + }).collect(Collectors.toList()); + responseBuilder.entity(new ValidationErrorMessage(ImmutableList.copyOf(messages))); + } else if (message != null) { + responseBuilder.entity(new ValidationErrorMessage(ImmutableList.of(message))); + } + return responseBuilder.build(); + } + } + public static class UnauthorizedExceptionMapper implements ExceptionMapper { @Override public Response toResponse(UnauthorizedException exception) { diff --git a/api/src/main/java/bisq/api/http/exceptions/ExperimentalFeatureException.java b/api/src/main/java/bisq/api/http/exceptions/ExperimentalFeatureException.java new file mode 100644 index 00000000000..09917ab8b41 --- /dev/null +++ b/api/src/main/java/bisq/api/http/exceptions/ExperimentalFeatureException.java @@ -0,0 +1,7 @@ +package bisq.api.http.exceptions; + +public class ExperimentalFeatureException extends RuntimeException { + public ExperimentalFeatureException() { + super("Experimental features disabled"); + } +} diff --git a/api/src/main/java/bisq/api/http/exceptions/NotFoundException.java b/api/src/main/java/bisq/api/http/exceptions/NotFoundException.java new file mode 100644 index 00000000000..7bc66d3886f --- /dev/null +++ b/api/src/main/java/bisq/api/http/exceptions/NotFoundException.java @@ -0,0 +1,7 @@ +package bisq.api.http.exceptions; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/api/src/main/java/bisq/api/http/facade/PaymentAccountFacade.java b/api/src/main/java/bisq/api/http/facade/PaymentAccountFacade.java new file mode 100644 index 00000000000..3c0b5b9440c --- /dev/null +++ b/api/src/main/java/bisq/api/http/facade/PaymentAccountFacade.java @@ -0,0 +1,54 @@ +package bisq.api.http.facade; + +import bisq.api.http.exceptions.NotFoundException; +import bisq.api.http.model.PaymentAccountList; +import bisq.api.http.model.payment.PaymentAccountHelper; + +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountManager; +import bisq.core.user.User; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class PaymentAccountFacade { + + private final PaymentAccountManager paymentAccountManager; + private final User user; + + @Inject + public PaymentAccountFacade(PaymentAccountManager paymentAccountManager, User user) { + this.paymentAccountManager = paymentAccountManager; + this.user = user; + } + + public PaymentAccount addPaymentAccount(PaymentAccount paymentAccount) { + return paymentAccountManager.addPaymentAccount(paymentAccount); + } + + public void removePaymentAccount(String id) { + PaymentAccount paymentAccount = user.getPaymentAccount(id); + if (paymentAccount == null) { + throw new NotFoundException("Payment account not found: " + id); + } + user.removePaymentAccount(paymentAccount); + } + + public PaymentAccountList getAccountList() { + PaymentAccountList paymentAccountList = new PaymentAccountList(); + paymentAccountList.paymentAccounts = getPaymentAccountList().stream() + .map(PaymentAccountHelper::toRestModel) + .collect(Collectors.toList()); + return paymentAccountList; + } + + private List getPaymentAccountList() { + Set paymentAccounts = user.getPaymentAccounts(); + return null == paymentAccounts ? Collections.emptyList() : new ArrayList<>(paymentAccounts); + } +} diff --git a/api/src/main/java/bisq/api/http/model/PaymentAccountList.java b/api/src/main/java/bisq/api/http/model/PaymentAccountList.java new file mode 100644 index 00000000000..e7029da6667 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/PaymentAccountList.java @@ -0,0 +1,11 @@ +package bisq.api.http.model; + +import bisq.api.http.model.payment.PaymentAccount; + +import java.util.List; + +public class PaymentAccountList { + + public List paymentAccounts; + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/AbstractPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/AbstractPaymentAccountConverter.java new file mode 100644 index 00000000000..35beff20d21 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/AbstractPaymentAccountConverter.java @@ -0,0 +1,70 @@ +package bisq.api.http.model.payment; + +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; + +import java.util.List; +import java.util.Optional; + + + +import javax.validation.ValidationException; + +public abstract class AbstractPaymentAccountConverter implements PaymentAccountConverter { + + protected void toBusinessModel(B business, R rest) { + if (rest.accountName != null) + business.setAccountName(rest.accountName); + business.getTradeCurrencies().clear(); + CurrencyConverter currencyConverter; + if (rest instanceof CryptoCurrencyPaymentAccount) + currencyConverter = new CryptoCurrencyConverter(); + else + currencyConverter = new FiatCurrencyConverter(); + + if (rest.selectedTradeCurrency != null) + business.setSelectedTradeCurrency(currencyConverter.convert(rest.selectedTradeCurrency)); + if (rest.tradeCurrencies != null) + rest.tradeCurrencies.forEach(currencyCode -> business.addCurrency(currencyConverter.convert(currencyCode))); + } + + protected void toRestModel(R rest, B business) { + rest.id = business.getId(); + rest.accountName = business.getAccountName(); + TradeCurrency selectedTradeCurrency = business.getSelectedTradeCurrency(); + if (selectedTradeCurrency != null) + rest.selectedTradeCurrency = selectedTradeCurrency.getCode(); + List tradeCurrencies = business.getTradeCurrencies(); + if (tradeCurrencies != null) + tradeCurrencies.forEach(currency -> rest.tradeCurrencies.add(currency.getCode())); + } + + protected void toRestModel(R rest, BP business) { + rest.paymentDetails = business.getPaymentDetails(); + } + + private interface CurrencyConverter { + TradeCurrency convert(String currencyCode); + } + + private static class FiatCurrencyConverter implements CurrencyConverter { + @Override + public TradeCurrency convert(String currencyCode) { + return new FiatCurrency(currencyCode); + } + } + + private static class CryptoCurrencyConverter implements CurrencyConverter { + @Override + public TradeCurrency convert(String currencyCode) { + Optional cryptoCurrencyOptional = CurrencyUtil.getCryptoCurrency(currencyCode); + if (!cryptoCurrencyOptional.isPresent()) { + throw new ValidationException("Unsupported crypto currency code: " + currencyCode); + } + return cryptoCurrencyOptional.get(); + } + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccount.java new file mode 100644 index 00000000000..177cb735d4a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.ADVANCED_CASH_ID) +public class AdvancedCashPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public AdvancedCashPaymentAccount() { + super(PaymentMethod.ADVANCED_CASH_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccountConverter.java new file mode 100644 index 00000000000..a58ff1f110d --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/AdvancedCashPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.AdvancedCashAccount; +import bisq.core.payment.payload.AdvancedCashAccountPayload; + +public class AdvancedCashPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public AdvancedCashAccount toBusinessModel(AdvancedCashPaymentAccount rest) { + AdvancedCashAccount business = new AdvancedCashAccount(); + business.init(); + business.setAccountNr(rest.accountNr); + toBusinessModel(business, rest); + return business; + } + + @Override + public AdvancedCashPaymentAccount toRestModel(AdvancedCashAccount business) { + AdvancedCashPaymentAccount rest = toRestModel((AdvancedCashAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public AdvancedCashPaymentAccount toRestModel(AdvancedCashAccountPayload business) { + AdvancedCashPaymentAccount rest = new AdvancedCashPaymentAccount(); + rest.accountNr = business.getAccountNr(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccount.java new file mode 100644 index 00000000000..b17b492324e --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.ALI_PAY_ID) +public class AliPayPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public AliPayPaymentAccount() { + super(PaymentMethod.ALI_PAY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccountConverter.java new file mode 100644 index 00000000000..4da06fa0d47 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/AliPayPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.AliPayAccount; +import bisq.core.payment.payload.AliPayAccountPayload; + +public class AliPayPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public AliPayAccount toBusinessModel(AliPayPaymentAccount rest) { + AliPayAccount business = new AliPayAccount(); + business.init(); + business.setAccountNr(rest.accountNr); + toBusinessModel(business, rest); + return business; + } + + @Override + public AliPayPaymentAccount toRestModel(AliPayAccount business) { + AliPayPaymentAccount rest = toRestModel((AliPayAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public AliPayPaymentAccount toRestModel(AliPayAccountPayload business) { + AliPayPaymentAccount rest = new AliPayPaymentAccount(); + rest.accountNr = business.getAccountNr(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccount.java new file mode 100644 index 00000000000..3c9bab0dda4 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccount.java @@ -0,0 +1,43 @@ +package bisq.api.http.model.payment; + +import bisq.api.http.model.validation.CountryCode; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.CASH_DEPOSIT_ID) +public class CashDepositPaymentAccount extends PaymentAccount { + + public String accountNr; + + public String accountType; + + public String bankId; + + public String bankName; + + public String branchId; + + @CountryCode + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + @NotBlank + public String holderEmail; + + public String holderTaxId; + + public String requirements; + + public CashDepositPaymentAccount() { + super(PaymentMethod.CASH_DEPOSIT_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccountConverter.java new file mode 100644 index 00000000000..4c74593358b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/CashDepositPaymentAccountConverter.java @@ -0,0 +1,51 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.CashDepositAccount; +import bisq.core.payment.payload.CashDepositAccountPayload; + +public class CashDepositPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public CashDepositAccount toBusinessModel(CashDepositPaymentAccount rest) { + CashDepositAccount business = new CashDepositAccount(); + business.init(); + business.setRequirements(rest.requirements); + CashDepositAccountPayload paymentAccountPayload = (CashDepositAccountPayload) business.getPaymentAccountPayload(); + paymentAccountPayload.setAccountNr(rest.accountNr); + paymentAccountPayload.setAccountType(rest.accountType); + paymentAccountPayload.setBankId(rest.bankId); + paymentAccountPayload.setBankName(rest.bankName); + paymentAccountPayload.setBranchId(rest.branchId); + paymentAccountPayload.setCountryCode(rest.countryCode); + paymentAccountPayload.setHolderEmail(rest.holderEmail); + paymentAccountPayload.setHolderName(rest.holderName); + paymentAccountPayload.setHolderTaxId(rest.holderTaxId); + toBusinessModel(business, rest); + return business; + } + + @Override + public CashDepositPaymentAccount toRestModel(CashDepositAccount business) { + CashDepositPaymentAccount rest = toRestModel((CashDepositAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public CashDepositPaymentAccount toRestModel(CashDepositAccountPayload business) { + CashDepositPaymentAccount rest = new CashDepositPaymentAccount(); + rest.requirements = business.getRequirements(); + rest.accountNr = business.getAccountNr(); + rest.accountType = business.getAccountType(); + rest.bankId = business.getBankId(); + rest.bankName = business.getBankName(); + rest.branchId = business.getBranchId(); + rest.countryCode = business.getCountryCode(); + rest.holderEmail = business.getHolderEmail(); + rest.holderName = business.getHolderName(); + rest.holderTaxId = business.getHolderTaxId(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccount.java new file mode 100644 index 00000000000..29db2a4d93c --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.CHASE_QUICK_PAY_ID) +public class ChaseQuickPayPaymentAccount extends PaymentAccount { + + @NotBlank + public String email; + + @NotBlank + public String holderName; + + public ChaseQuickPayPaymentAccount() { + super(PaymentMethod.CHASE_QUICK_PAY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccountConverter.java new file mode 100644 index 00000000000..806b92e177b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/ChaseQuickPayPaymentAccountConverter.java @@ -0,0 +1,34 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.ChaseQuickPayAccount; +import bisq.core.payment.payload.ChaseQuickPayAccountPayload; + +public class ChaseQuickPayPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public ChaseQuickPayAccount toBusinessModel(ChaseQuickPayPaymentAccount rest) { + ChaseQuickPayAccount business = new ChaseQuickPayAccount(); + business.init(); + business.setEmail(rest.email); + business.setHolderName(rest.holderName); + toBusinessModel(business, rest); + return business; + } + + @Override + public ChaseQuickPayPaymentAccount toRestModel(ChaseQuickPayAccount business) { + ChaseQuickPayPaymentAccount rest = toRestModel((ChaseQuickPayAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public ChaseQuickPayPaymentAccount toRestModel(ChaseQuickPayAccountPayload business) { + ChaseQuickPayPaymentAccount rest = new ChaseQuickPayPaymentAccount(); + rest.email = business.getEmail(); + rest.holderName = business.getHolderName(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccount.java new file mode 100644 index 00000000000..2e6e8f5b38f --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.CLEAR_X_CHANGE_ID) +public class ClearXchangePaymentAccount extends PaymentAccount { + + @NotBlank + public String emailOrMobileNr; + + @NotBlank + public String holderName; + + public ClearXchangePaymentAccount() { + super(PaymentMethod.CLEAR_X_CHANGE_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccountConverter.java new file mode 100644 index 00000000000..1d92950f22f --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/ClearXchangePaymentAccountConverter.java @@ -0,0 +1,34 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.payload.ClearXchangeAccountPayload; + +public class ClearXchangePaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public ClearXchangeAccount toBusinessModel(ClearXchangePaymentAccount rest) { + ClearXchangeAccount business = new ClearXchangeAccount(); + business.init(); + business.setEmailOrMobileNr(rest.emailOrMobileNr); + business.setHolderName(rest.holderName); + toBusinessModel(business, rest); + return business; + } + + @Override + public ClearXchangePaymentAccount toRestModel(ClearXchangeAccount business) { + ClearXchangePaymentAccount rest = toRestModel((ClearXchangeAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public ClearXchangePaymentAccount toRestModel(ClearXchangeAccountPayload business) { + ClearXchangePaymentAccount rest = new ClearXchangePaymentAccount(); + rest.emailOrMobileNr = business.getEmailOrMobileNr(); + rest.holderName = business.getHolderName(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccount.java new file mode 100644 index 00000000000..4092b6cd8f1 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.BLOCK_CHAINS_ID) +public class CryptoCurrencyPaymentAccount extends PaymentAccount { + + @NotBlank + public String address; + + public CryptoCurrencyPaymentAccount() { + super(PaymentMethod.BLOCK_CHAINS_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccountConverter.java new file mode 100644 index 00000000000..f3ce28cb12f --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/CryptoCurrencyPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.payload.CryptoCurrencyAccountPayload; + +public class CryptoCurrencyPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public CryptoCurrencyAccount toBusinessModel(CryptoCurrencyPaymentAccount rest) { + CryptoCurrencyAccount business = new CryptoCurrencyAccount(); + business.init(); + business.setAddress(rest.address); + toBusinessModel(business, rest); + return business; + } + + @Override + public CryptoCurrencyPaymentAccount toRestModel(CryptoCurrencyAccount business) { + CryptoCurrencyPaymentAccount rest = toRestModel((CryptoCurrencyAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public CryptoCurrencyPaymentAccount toRestModel(CryptoCurrencyAccountPayload business) { + CryptoCurrencyPaymentAccount rest = new CryptoCurrencyPaymentAccount(); + rest.address = business.getAddress(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccount.java new file mode 100644 index 00000000000..c57ec252924 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccount.java @@ -0,0 +1,25 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.F2F_ID) +public class F2FPaymentAccount extends PaymentAccount { + + @NotBlank + public String contact; + + @NotBlank + public String city; + + public String extraInfo; + + public F2FPaymentAccount() { + super(PaymentMethod.F2F_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccountConverter.java new file mode 100644 index 00000000000..b22f680d500 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/F2FPaymentAccountConverter.java @@ -0,0 +1,36 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.F2FAccount; +import bisq.core.payment.payload.F2FAccountPayload; + +public class F2FPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public F2FAccount toBusinessModel(F2FPaymentAccount rest) { + F2FAccount business = new F2FAccount(); + business.init(); + business.setCity(rest.city); + business.setContact(rest.contact); + business.setExtraInfo(rest.extraInfo); + toBusinessModel(business, rest); + return business; + } + + @Override + public F2FPaymentAccount toRestModel(F2FAccount business) { + F2FPaymentAccount rest = toRestModel((F2FAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public F2FPaymentAccount toRestModel(F2FAccountPayload business) { + F2FPaymentAccount rest = new F2FPaymentAccount(); + rest.city = business.getCity(); + rest.contact = business.getContact(); + rest.extraInfo = business.getExtraInfo(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccount.java new file mode 100644 index 00000000000..991c6d84a2e --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.FASTER_PAYMENTS_ID) +public class FasterPaymentsPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + @NotBlank + public String sortCode; + + public FasterPaymentsPaymentAccount() { + super(PaymentMethod.FASTER_PAYMENTS_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccountConverter.java new file mode 100644 index 00000000000..e09871fcd0b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/FasterPaymentsPaymentAccountConverter.java @@ -0,0 +1,34 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.FasterPaymentsAccount; +import bisq.core.payment.payload.FasterPaymentsAccountPayload; + +public class FasterPaymentsPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public FasterPaymentsAccount toBusinessModel(FasterPaymentsPaymentAccount rest) { + FasterPaymentsAccount business = new FasterPaymentsAccount(); + business.init(); + business.setAccountNr(rest.accountNr); + business.setSortCode(rest.sortCode); + toBusinessModel(business, rest); + return business; + } + + @Override + public FasterPaymentsPaymentAccount toRestModel(FasterPaymentsAccount business) { + FasterPaymentsPaymentAccount rest = toRestModel((FasterPaymentsAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public FasterPaymentsPaymentAccount toRestModel(FasterPaymentsAccountPayload business) { + FasterPaymentsPaymentAccount rest = new FasterPaymentsPaymentAccount(); + rest.accountNr = business.getAccountNr(); + rest.sortCode = business.getSortCode(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccount.java new file mode 100644 index 00000000000..331460d8d46 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.HAL_CASH_ID) +public class HalCashPaymentAccount extends PaymentAccount { + + @NotBlank + public String mobileNr; + + public HalCashPaymentAccount() { + super(PaymentMethod.HAL_CASH_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccountConverter.java new file mode 100644 index 00000000000..60c33bb8d7a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/HalCashPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.payload.HalCashAccountPayload; + +public class HalCashPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public HalCashAccount toBusinessModel(HalCashPaymentAccount rest) { + HalCashAccount business = new HalCashAccount(); + business.init(); + business.setMobileNr(rest.mobileNr); + toBusinessModel(business, rest); + return business; + } + + @Override + public HalCashPaymentAccount toRestModel(HalCashAccount business) { + HalCashPaymentAccount rest = toRestModel((HalCashAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public HalCashPaymentAccount toRestModel(HalCashAccountPayload business) { + HalCashPaymentAccount rest = new HalCashPaymentAccount(); + rest.mobileNr = business.getMobileNr(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccount.java new file mode 100644 index 00000000000..f3a37a6c36c --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccount.java @@ -0,0 +1,29 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.INTERAC_E_TRANSFER_ID) +public class InteracETransferPaymentAccount extends PaymentAccount { + + @NotBlank + public String emailOrMobileNr; + + @NotBlank + public String holderName; + + @NotBlank + public String question; + + @NotBlank + public String answer; + + public InteracETransferPaymentAccount() { + super(PaymentMethod.INTERAC_E_TRANSFER_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccountConverter.java new file mode 100644 index 00000000000..a4d7a6e0892 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/InteracETransferPaymentAccountConverter.java @@ -0,0 +1,38 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.InteracETransferAccount; +import bisq.core.payment.payload.InteracETransferAccountPayload; + +public class InteracETransferPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public InteracETransferAccount toBusinessModel(InteracETransferPaymentAccount rest) { + InteracETransferAccount business = new InteracETransferAccount(); + business.init(); + business.setHolderName(rest.holderName); + business.setEmail(rest.emailOrMobileNr); + business.setQuestion(rest.question); + business.setAnswer(rest.answer); + toBusinessModel(business, rest); + return business; + } + + @Override + public InteracETransferPaymentAccount toRestModel(InteracETransferAccount business) { + InteracETransferPaymentAccount rest = toRestModel((InteracETransferAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public InteracETransferPaymentAccount toRestModel(InteracETransferAccountPayload business) { + InteracETransferPaymentAccount rest = new InteracETransferPaymentAccount(); + rest.answer = business.getAnswer(); + rest.question = business.getQuestion(); + rest.holderName = business.getHolderName(); + rest.emailOrMobileNr = business.getEmail(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccount.java new file mode 100644 index 00000000000..ed3658676ce --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.MONEY_BEAM_ID) +public class MoneyBeamPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountId; + + public MoneyBeamPaymentAccount() { + super(PaymentMethod.MONEY_BEAM_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccountConverter.java new file mode 100644 index 00000000000..32966be6d24 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/MoneyBeamPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.MoneyBeamAccount; +import bisq.core.payment.payload.MoneyBeamAccountPayload; + +public class MoneyBeamPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public MoneyBeamAccount toBusinessModel(MoneyBeamPaymentAccount rest) { + MoneyBeamAccount business = new MoneyBeamAccount(); + business.init(); + business.setAccountId(rest.accountId); + toBusinessModel(business, rest); + return business; + } + + @Override + public MoneyBeamPaymentAccount toRestModel(MoneyBeamAccount business) { + MoneyBeamPaymentAccount rest = toRestModel((MoneyBeamAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public MoneyBeamPaymentAccount toRestModel(MoneyBeamAccountPayload business) { + MoneyBeamPaymentAccount rest = new MoneyBeamPaymentAccount(); + rest.accountId = business.getAccountId(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccount.java new file mode 100644 index 00000000000..162b5d0f32e --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccount.java @@ -0,0 +1,31 @@ +package bisq.api.http.model.payment; + +import bisq.api.http.model.validation.CountryCode; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.MONEY_GRAM_ID) +public class MoneyGramPaymentAccount extends PaymentAccount { + + @NotBlank + public String holderName; + + @CountryCode + @NotBlank + public String countryCode; + + public String state; + + @NotBlank + public String email; + + public MoneyGramPaymentAccount() { + super(PaymentMethod.MONEY_GRAM_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccountConverter.java new file mode 100644 index 00000000000..1c074ee209c --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/MoneyGramPaymentAccountConverter.java @@ -0,0 +1,39 @@ +package bisq.api.http.model.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.MoneyGramAccount; +import bisq.core.payment.payload.MoneyGramAccountPayload; + +public class MoneyGramPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public MoneyGramAccount toBusinessModel(MoneyGramPaymentAccount rest) { + MoneyGramAccount business = new MoneyGramAccount(); + business.init(); + CountryUtil.findCountryByCode(rest.countryCode).ifPresent(business::setCountry); + business.setEmail(rest.email); + business.setFullName(rest.holderName); + business.setState(rest.state); + toBusinessModel(business, rest); + return business; + } + + @Override + public MoneyGramPaymentAccount toRestModel(MoneyGramAccount business) { + MoneyGramPaymentAccount rest = toRestModel((MoneyGramAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public MoneyGramPaymentAccount toRestModel(MoneyGramAccountPayload business) { + MoneyGramPaymentAccount rest = new MoneyGramPaymentAccount(); + rest.countryCode = business.getCountryCode(); + rest.email = business.getEmail(); + rest.holderName = business.getHolderName(); + rest.state = business.getState(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccount.java new file mode 100644 index 00000000000..74b4fe8664a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccount.java @@ -0,0 +1,39 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.NATIONAL_BANK_ID) +public class NationalBankAccountPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public String accountType; + + @NotBlank + public String bankId; + + @NotBlank + public String bankName; + + @NotBlank + public String branchId; + + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + public String holderTaxId; + + public NationalBankAccountPaymentAccount() { + super(PaymentMethod.NATIONAL_BANK_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccountConverter.java new file mode 100644 index 00000000000..bdb21bf03d9 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/NationalBankAccountPaymentAccountConverter.java @@ -0,0 +1,48 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.NationalBankAccount; +import bisq.core.payment.payload.NationalBankAccountPayload; + +public class NationalBankAccountPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public NationalBankAccount toBusinessModel(NationalBankAccountPaymentAccount rest) { + NationalBankAccount business = new NationalBankAccount(); + business.init(); + NationalBankAccountPayload paymentAccountPayload = (NationalBankAccountPayload) business.getPaymentAccountPayload(); + paymentAccountPayload.setAccountNr(rest.accountNr); + paymentAccountPayload.setAccountType(rest.accountType); + paymentAccountPayload.setBankId(rest.bankId); + paymentAccountPayload.setBankName(rest.bankName); + paymentAccountPayload.setBranchId(rest.branchId); + paymentAccountPayload.setCountryCode(rest.countryCode); + paymentAccountPayload.setHolderName(rest.holderName); + paymentAccountPayload.setHolderTaxId(rest.holderTaxId); + toBusinessModel(business, rest); + return business; + } + + @Override + public NationalBankAccountPaymentAccount toRestModel(NationalBankAccount business) { + NationalBankAccountPaymentAccount rest = toRestModel((NationalBankAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public NationalBankAccountPaymentAccount toRestModel(NationalBankAccountPayload business) { + NationalBankAccountPaymentAccount rest = new NationalBankAccountPaymentAccount(); + rest.accountNr = business.getAccountNr(); + rest.accountType = business.getAccountType(); + rest.bankId = business.getBankId(); + rest.bankName = business.getBankName(); + rest.branchId = business.getBranchId(); + rest.countryCode = business.getCountryCode(); + rest.holderName = business.getHolderName(); + rest.holderTaxId = business.getHolderTaxId(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/PaymentAccount.java new file mode 100644 index 00000000000..2807a447a5a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PaymentAccount.java @@ -0,0 +1,104 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.util.ArrayList; +import java.util.List; + + + +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; + +@Schema( + discriminatorProperty = "paymentMethod", + discriminatorMapping = { + @DiscriminatorMapping(value = PaymentMethod.ADVANCED_CASH_ID, schema = AdvancedCashPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.ALI_PAY_ID, schema = AliPayPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.CASH_DEPOSIT_ID, schema = CashDepositPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.CHASE_QUICK_PAY_ID, schema = ChaseQuickPayPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.CLEAR_X_CHANGE_ID, schema = ClearXchangePaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.BLOCK_CHAINS_ID, schema = CryptoCurrencyPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.F2F_ID, schema = F2FPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.FASTER_PAYMENTS_ID, schema = FasterPaymentsPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.HAL_CASH_ID, schema = HalCashPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.INTERAC_E_TRANSFER_ID, schema = InteracETransferPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.MONEY_BEAM_ID, schema = MoneyBeamPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.MONEY_GRAM_ID, schema = MoneyGramPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.NATIONAL_BANK_ID, schema = NationalBankAccountPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.PERFECT_MONEY_ID, schema = PerfectMoneyPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.POPMONEY_ID, schema = PopmoneyPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.PROMPT_PAY_ID, schema = PromptPayPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.REVOLUT_ID, schema = RevolutPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.SAME_BANK_ID, schema = SameBankAccountPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.SEPA_ID, schema = SepaPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.SEPA_INSTANT_ID, schema = SepaInstantPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.SPECIFIC_BANKS_ID, schema = SpecificBanksAccountPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.SWISH_ID, schema = SwishPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.UPHOLD_ID, schema = UpholdPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.US_POSTAL_MONEY_ORDER_ID, schema = USPostalMoneyOrderPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.WECHAT_PAY_ID, schema = WeChatPayPaymentAccount.class), + @DiscriminatorMapping(value = PaymentMethod.WESTERN_UNION_ID, schema = WesternUnionPaymentAccount.class) + } +) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "paymentMethod", include = JsonTypeInfo.As.EXISTING_PROPERTY) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AdvancedCashPaymentAccount.class, name = PaymentMethod.ADVANCED_CASH_ID), + @JsonSubTypes.Type(value = AliPayPaymentAccount.class, name = PaymentMethod.ALI_PAY_ID), + @JsonSubTypes.Type(value = CashDepositPaymentAccount.class, name = PaymentMethod.CASH_DEPOSIT_ID), + @JsonSubTypes.Type(value = ChaseQuickPayPaymentAccount.class, name = PaymentMethod.CHASE_QUICK_PAY_ID), + @JsonSubTypes.Type(value = ClearXchangePaymentAccount.class, name = PaymentMethod.CLEAR_X_CHANGE_ID), + @JsonSubTypes.Type(value = CryptoCurrencyPaymentAccount.class, name = PaymentMethod.BLOCK_CHAINS_ID), + @JsonSubTypes.Type(value = F2FPaymentAccount.class, name = PaymentMethod.F2F_ID), + @JsonSubTypes.Type(value = FasterPaymentsPaymentAccount.class, name = PaymentMethod.FASTER_PAYMENTS_ID), + @JsonSubTypes.Type(value = HalCashPaymentAccount.class, name = PaymentMethod.HAL_CASH_ID), + @JsonSubTypes.Type(value = InteracETransferPaymentAccount.class, name = PaymentMethod.INTERAC_E_TRANSFER_ID), + @JsonSubTypes.Type(value = MoneyBeamPaymentAccount.class, name = PaymentMethod.MONEY_BEAM_ID), + @JsonSubTypes.Type(value = MoneyGramPaymentAccount.class, name = PaymentMethod.MONEY_GRAM_ID), + @JsonSubTypes.Type(value = NationalBankAccountPaymentAccount.class, name = PaymentMethod.NATIONAL_BANK_ID), + @JsonSubTypes.Type(value = PerfectMoneyPaymentAccount.class, name = PaymentMethod.PERFECT_MONEY_ID), + @JsonSubTypes.Type(value = PopmoneyPaymentAccount.class, name = PaymentMethod.POPMONEY_ID), + @JsonSubTypes.Type(value = PromptPayPaymentAccount.class, name = PaymentMethod.PROMPT_PAY_ID), + @JsonSubTypes.Type(value = RevolutPaymentAccount.class, name = PaymentMethod.REVOLUT_ID), + @JsonSubTypes.Type(value = SameBankAccountPaymentAccount.class, name = PaymentMethod.SAME_BANK_ID), + @JsonSubTypes.Type(value = SepaPaymentAccount.class, name = PaymentMethod.SEPA_ID), + @JsonSubTypes.Type(value = SepaInstantPaymentAccount.class, name = PaymentMethod.SEPA_INSTANT_ID), + @JsonSubTypes.Type(value = SpecificBanksAccountPaymentAccount.class, name = PaymentMethod.SPECIFIC_BANKS_ID), + @JsonSubTypes.Type(value = SwishPaymentAccount.class, name = PaymentMethod.SWISH_ID), + @JsonSubTypes.Type(value = UpholdPaymentAccount.class, name = PaymentMethod.UPHOLD_ID), + @JsonSubTypes.Type(value = USPostalMoneyOrderPaymentAccount.class, name = PaymentMethod.US_POSTAL_MONEY_ORDER_ID), + @JsonSubTypes.Type(value = WeChatPayPaymentAccount.class, name = PaymentMethod.WECHAT_PAY_ID), + @JsonSubTypes.Type(value = WesternUnionPaymentAccount.class, name = PaymentMethod.WESTERN_UNION_ID) +}) +public abstract class PaymentAccount { + + public String id; + + @NotBlank + public String accountName; + + @SuppressWarnings("WeakerAccess") + public String paymentDetails; + + @NotNull + @NotBlank + public String paymentMethod; + + @NotBlank + public String selectedTradeCurrency; + + @NotEmpty + public List tradeCurrencies = new ArrayList<>(); + + public PaymentAccount(String paymentMethod) { + this.paymentMethod = paymentMethod; + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/PaymentAccountConverter.java new file mode 100644 index 00000000000..87c4ac037f2 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PaymentAccountConverter.java @@ -0,0 +1,13 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; + +public interface PaymentAccountConverter { + + B toBusinessModel(R rest); + + R toRestModel(B business); + + R toRestModel(BP business); + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PaymentAccountHelper.java b/api/src/main/java/bisq/api/http/model/payment/PaymentAccountHelper.java new file mode 100644 index 00000000000..8fcde0b584a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PaymentAccountHelper.java @@ -0,0 +1,77 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.HashMap; +import java.util.Map; + + + +import javax.ws.rs.WebApplicationException; + +@SuppressWarnings("deprecation") +public final class PaymentAccountHelper { + + private static Map> converters = new HashMap<>(); + + static { + converters.put(PaymentMethod.ADVANCED_CASH_ID, new AdvancedCashPaymentAccountConverter()); + converters.put(PaymentMethod.ALI_PAY_ID, new AliPayPaymentAccountConverter()); + converters.put(PaymentMethod.BLOCK_CHAINS_ID, new CryptoCurrencyPaymentAccountConverter()); + converters.put(PaymentMethod.CASH_DEPOSIT_ID, new CashDepositPaymentAccountConverter()); + converters.put(PaymentMethod.CHASE_QUICK_PAY_ID, new ChaseQuickPayPaymentAccountConverter()); + converters.put(PaymentMethod.CLEAR_X_CHANGE_ID, new ClearXchangePaymentAccountConverter()); + converters.put(PaymentMethod.F2F_ID, new F2FPaymentAccountConverter()); + converters.put(PaymentMethod.FASTER_PAYMENTS_ID, new FasterPaymentsPaymentAccountConverter()); + converters.put(PaymentMethod.HAL_CASH_ID, new HalCashPaymentAccountConverter()); + converters.put(PaymentMethod.INTERAC_E_TRANSFER_ID, new InteracETransferPaymentAccountConverter()); + converters.put(PaymentMethod.MONEY_BEAM_ID, new MoneyBeamPaymentAccountConverter()); + converters.put(PaymentMethod.MONEY_GRAM_ID, new MoneyGramPaymentAccountConverter()); + converters.put(PaymentMethod.NATIONAL_BANK_ID, new NationalBankAccountPaymentAccountConverter()); + converters.put(PaymentMethod.PERFECT_MONEY_ID, new PerfectMoneyPaymentAccountConverter()); + converters.put(PaymentMethod.POPMONEY_ID, new PopmoneyPaymentAccountConverter()); + converters.put(PaymentMethod.PROMPT_PAY_ID, new PromptPayPaymentAccountConverter()); + converters.put(PaymentMethod.REVOLUT_ID, new RevolutPaymentAccountConverter()); + converters.put(PaymentMethod.SAME_BANK_ID, new SameBankAccountPaymentAccountConverter()); + converters.put(PaymentMethod.SEPA_ID, new SepaPaymentAccountConverter()); + converters.put(PaymentMethod.SEPA_INSTANT_ID, new SepaInstantPaymentAccountConverter()); + converters.put(PaymentMethod.SPECIFIC_BANKS_ID, new SpecificBanksAccountPaymentAccountConverter()); + converters.put(PaymentMethod.SWISH_ID, new SwishPaymentAccountConverter()); + converters.put(PaymentMethod.UPHOLD_ID, new UpholdPaymentAccountConverter()); + converters.put(PaymentMethod.US_POSTAL_MONEY_ORDER_ID, new USPostalMoneyOrderPaymentAccountConverter()); + converters.put(PaymentMethod.WECHAT_PAY_ID, new WeChatPayPaymentAccountConverter()); + converters.put(PaymentMethod.WESTERN_UNION_ID, new WesternUnionPaymentAccountConverter()); + } + + @SuppressWarnings("unchecked") + public static bisq.core.payment.PaymentAccount toBusinessModel(PaymentAccount rest) { + PaymentAccountConverter converter = converters.get(rest.paymentMethod); + if (converter != null) { + return converter.toBusinessModel(rest); + } + throw new WebApplicationException("Unsupported paymentMethod:" + rest.paymentMethod, 400); + } + + @SuppressWarnings("unchecked") + public static PaymentAccount toRestModel(bisq.core.payment.PaymentAccount business) { + String paymentMethodId = business.getPaymentMethod().getId(); + PaymentAccountConverter converter = converters.get(paymentMethodId); + if (converter != null) { + return converter.toRestModel(business); + } + throw new IllegalArgumentException("Unsupported paymentMethod:" + paymentMethodId); + } + + @SuppressWarnings("unchecked") + public static PaymentAccount toRestModel(PaymentAccountPayload business) { + String paymentMethodId = business.getPaymentMethodId(); + PaymentAccountConverter converter = converters.get(paymentMethodId); + if (converter != null) { + PaymentAccount paymentAccount = converter.toRestModel(business); + paymentAccount.paymentDetails = business.getPaymentDetails(); + return paymentAccount; + } + throw new IllegalArgumentException("Unsupported paymentMethod:" + paymentMethodId); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccount.java new file mode 100644 index 00000000000..94d6d278f39 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.PERFECT_MONEY_ID) +public class PerfectMoneyPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public PerfectMoneyPaymentAccount() { + super(PaymentMethod.PERFECT_MONEY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccountConverter.java new file mode 100644 index 00000000000..45d21bca819 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PerfectMoneyPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.PerfectMoneyAccount; +import bisq.core.payment.payload.PerfectMoneyAccountPayload; + +public class PerfectMoneyPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public PerfectMoneyAccount toBusinessModel(PerfectMoneyPaymentAccount rest) { + PerfectMoneyAccount business = new PerfectMoneyAccount(); + business.init(); + business.setAccountNr(rest.accountNr); + toBusinessModel(business, rest); + return business; + } + + @Override + public PerfectMoneyPaymentAccount toRestModel(PerfectMoneyAccount business) { + PerfectMoneyPaymentAccount rest = toRestModel((PerfectMoneyAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public PerfectMoneyPaymentAccount toRestModel(PerfectMoneyAccountPayload business) { + PerfectMoneyPaymentAccount rest = new PerfectMoneyPaymentAccount(); + rest.accountNr = business.getAccountNr(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccount.java new file mode 100644 index 00000000000..f1e7e2e1cd2 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.POPMONEY_ID) +public class PopmoneyPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountId; + + @NotBlank + public String holderName; + + public PopmoneyPaymentAccount() { + super(PaymentMethod.POPMONEY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccountConverter.java new file mode 100644 index 00000000000..dca800ca8ae --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PopmoneyPaymentAccountConverter.java @@ -0,0 +1,35 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.PopmoneyAccount; +import bisq.core.payment.payload.PopmoneyAccountPayload; + +public class PopmoneyPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public PopmoneyAccount toBusinessModel(PopmoneyPaymentAccount rest) { + PopmoneyAccount business = new PopmoneyAccount(); + business.init(); + business.setAccountId(rest.accountId); + business.setHolderName(rest.holderName); + toBusinessModel(business, rest); + return business; + } + + @Override + public PopmoneyPaymentAccount toRestModel(PopmoneyAccount business) { + PopmoneyPaymentAccount rest = toRestModel((PopmoneyAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public PopmoneyPaymentAccount toRestModel(PopmoneyAccountPayload business) { + PopmoneyPaymentAccount rest = new PopmoneyPaymentAccount(); + rest.accountId = business.getAccountId(); + rest.holderName = business.getHolderName(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccount.java new file mode 100644 index 00000000000..5259e07425c --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.PROMPT_PAY_ID) +public class PromptPayPaymentAccount extends PaymentAccount { + + @NotBlank + public String promptPayId; + + public PromptPayPaymentAccount() { + super(PaymentMethod.PROMPT_PAY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccountConverter.java new file mode 100644 index 00000000000..f839beccd27 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/PromptPayPaymentAccountConverter.java @@ -0,0 +1,32 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.PromptPayAccount; +import bisq.core.payment.payload.PromptPayAccountPayload; + +public class PromptPayPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public PromptPayAccount toBusinessModel(PromptPayPaymentAccount rest) { + PromptPayAccount business = new PromptPayAccount(); + business.init(); + business.setPromptPayId(rest.promptPayId); + toBusinessModel(business, rest); + return business; + } + + @Override + public PromptPayPaymentAccount toRestModel(PromptPayAccount business) { + PromptPayPaymentAccount rest = toRestModel((PromptPayAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public PromptPayPaymentAccount toRestModel(PromptPayAccountPayload business) { + PromptPayPaymentAccount rest = new PromptPayPaymentAccount(); + rest.promptPayId = business.getPromptPayId(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccount.java new file mode 100644 index 00000000000..ac81bfdd31d --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.REVOLUT_ID) +public class RevolutPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountId; + + public RevolutPaymentAccount() { + super(PaymentMethod.REVOLUT_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccountConverter.java new file mode 100644 index 00000000000..e510adee739 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/RevolutPaymentAccountConverter.java @@ -0,0 +1,33 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.RevolutAccount; +import bisq.core.payment.payload.RevolutAccountPayload; + +public class RevolutPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public RevolutAccount toBusinessModel(RevolutPaymentAccount rest) { + RevolutAccount business = new RevolutAccount(); + business.init(); + business.setAccountId(rest.accountId); + toBusinessModel(business, rest); + return business; + } + + @Override + public RevolutPaymentAccount toRestModel(RevolutAccount business) { + RevolutPaymentAccount rest = toRestModel((RevolutAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public RevolutPaymentAccount toRestModel(RevolutAccountPayload business) { + RevolutPaymentAccount rest = new RevolutPaymentAccount(); + rest.accountId = business.getAccountId(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccount.java new file mode 100644 index 00000000000..a8864c94fb5 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccount.java @@ -0,0 +1,39 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.SAME_BANK_ID) +public class SameBankAccountPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public String accountType; + + @NotBlank + public String bankId; + + @NotBlank + public String bankName; + + @NotBlank + public String branchId; + + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + public String holderTaxId; + + public SameBankAccountPaymentAccount() { + super(PaymentMethod.SAME_BANK_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccountConverter.java new file mode 100644 index 00000000000..ca75914d420 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SameBankAccountPaymentAccountConverter.java @@ -0,0 +1,47 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.SameBankAccount; +import bisq.core.payment.payload.SameBankAccountPayload; + +public class SameBankAccountPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public SameBankAccount toBusinessModel(SameBankAccountPaymentAccount rest) { + SameBankAccount business = new SameBankAccount(); + business.init(); + SameBankAccountPayload paymentAccountPayload = (SameBankAccountPayload) business.getPaymentAccountPayload(); + paymentAccountPayload.setAccountNr(rest.accountNr); + paymentAccountPayload.setAccountType(rest.accountType); + paymentAccountPayload.setBankId(rest.bankId); + paymentAccountPayload.setBankName(rest.bankName); + paymentAccountPayload.setBranchId(rest.branchId); + paymentAccountPayload.setCountryCode(rest.countryCode); + paymentAccountPayload.setHolderName(rest.holderName); + paymentAccountPayload.setHolderTaxId(rest.holderTaxId); + toBusinessModel(business, rest); + return business; + } + + @Override + public SameBankAccountPaymentAccount toRestModel(SameBankAccount business) { + SameBankAccountPaymentAccount rest = toRestModel((SameBankAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public SameBankAccountPaymentAccount toRestModel(SameBankAccountPayload business) { + SameBankAccountPaymentAccount rest = new SameBankAccountPaymentAccount(); + rest.accountNr = business.getAccountNr(); + rest.accountType = business.getAccountType(); + rest.bankId = business.getBankId(); + rest.bankName = business.getBankName(); + rest.branchId = business.getBranchId(); + rest.countryCode = business.getCountryCode(); + rest.holderName = business.getHolderName(); + rest.holderTaxId = business.getHolderTaxId(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccount.java new file mode 100644 index 00000000000..ad959008691 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccount.java @@ -0,0 +1,39 @@ +package bisq.api.http.model.payment; + +import bisq.api.http.model.validation.CountryCode; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.ArrayList; +import java.util.List; + + + +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; + +@JsonTypeName(PaymentMethod.SEPA_INSTANT_ID) +public class SepaInstantPaymentAccount extends PaymentAccount { + + @CountryCode + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + @NotBlank + public String bic; + + @NotBlank + public String iban; + + @NotEmpty + public List acceptedCountries = new ArrayList<>(); + + public SepaInstantPaymentAccount() { + super(PaymentMethod.SEPA_INSTANT_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccountConverter.java new file mode 100644 index 00000000000..e05bb074339 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SepaInstantPaymentAccountConverter.java @@ -0,0 +1,48 @@ +package bisq.api.http.model.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.SepaInstantAccount; +import bisq.core.payment.payload.SepaInstantAccountPayload; + +import java.util.List; + +public class SepaInstantPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public SepaInstantAccount toBusinessModel(SepaInstantPaymentAccount rest) { + SepaInstantAccount business = new SepaInstantAccount(); + business.init(); + business.setBic(rest.bic); + business.setIban(rest.iban); + business.setHolderName(rest.holderName); + CountryUtil.findCountryByCode(rest.countryCode).ifPresent(business::setCountry); + business.getAcceptedCountryCodes().clear(); + if (rest.acceptedCountries != null) + rest.acceptedCountries.forEach(business::addAcceptedCountry); + toBusinessModel(business, rest); + return business; + } + + @Override + public SepaInstantPaymentAccount toRestModel(SepaInstantAccount business) { + SepaInstantPaymentAccount rest = toRestModel((SepaInstantAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public SepaInstantPaymentAccount toRestModel(SepaInstantAccountPayload business) { + SepaInstantPaymentAccount rest = new SepaInstantPaymentAccount(); + rest.iban = business.getIban(); + rest.bic = business.getBic(); + rest.countryCode = business.getCountryCode(); + rest.holderName = business.getHolderName(); + List tradeCurrencies = business.getAcceptedCountryCodes(); + if (tradeCurrencies != null) + rest.acceptedCountries.addAll(tradeCurrencies); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccount.java new file mode 100644 index 00000000000..145c7851acb --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccount.java @@ -0,0 +1,39 @@ +package bisq.api.http.model.payment; + +import bisq.api.http.model.validation.CountryCode; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.ArrayList; +import java.util.List; + + + +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; + +@JsonTypeName(PaymentMethod.SEPA_ID) +public class SepaPaymentAccount extends PaymentAccount { + + @CountryCode + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + @NotBlank + public String bic; + + @NotBlank + public String iban; + + @NotEmpty + public List acceptedCountries = new ArrayList<>(); + + public SepaPaymentAccount() { + super(PaymentMethod.SEPA_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccountConverter.java new file mode 100644 index 00000000000..90a6e7f9019 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SepaPaymentAccountConverter.java @@ -0,0 +1,48 @@ +package bisq.api.http.model.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.payload.SepaAccountPayload; + +import java.util.List; + +public class SepaPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public SepaAccount toBusinessModel(SepaPaymentAccount rest) { + SepaAccount business = new SepaAccount(); + business.init(); + business.setBic(rest.bic); + business.setIban(rest.iban); + business.setHolderName(rest.holderName); + CountryUtil.findCountryByCode(rest.countryCode).ifPresent(business::setCountry); + business.getAcceptedCountryCodes().clear(); + if (rest.acceptedCountries != null) + rest.acceptedCountries.forEach(business::addAcceptedCountry); + toBusinessModel(business, rest); + return business; + } + + @Override + public SepaPaymentAccount toRestModel(SepaAccount business) { + SepaPaymentAccount rest = toRestModel((SepaAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public SepaPaymentAccount toRestModel(SepaAccountPayload business) { + SepaPaymentAccount rest = new SepaPaymentAccount(); + rest.iban = business.getIban(); + rest.bic = business.getBic(); + rest.countryCode = business.getCountryCode(); + rest.holderName = business.getHolderName(); + List tradeCurrencies = business.getAcceptedCountryCodes(); + if (tradeCurrencies != null) + rest.acceptedCountries.addAll(tradeCurrencies); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccount.java new file mode 100644 index 00000000000..1d88a3afc25 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccount.java @@ -0,0 +1,46 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.ArrayList; +import java.util.List; + + + +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; + +@JsonTypeName(PaymentMethod.SPECIFIC_BANKS_ID) +public class SpecificBanksAccountPaymentAccount extends PaymentAccount { + + @NotEmpty + public List acceptedBanks = new ArrayList<>(); + + @NotBlank + public String accountNr; + + public String accountType; + + @NotBlank + public String bankId; + + @NotBlank + public String bankName; + + @NotBlank + public String branchId; + + @NotBlank + public String countryCode; + + @NotBlank + public String holderName; + + public String holderTaxId; + + public SpecificBanksAccountPaymentAccount() { + super(PaymentMethod.SPECIFIC_BANKS_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccountConverter.java new file mode 100644 index 00000000000..cf08972d4be --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SpecificBanksAccountPaymentAccountConverter.java @@ -0,0 +1,54 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.SpecificBanksAccount; +import bisq.core.payment.payload.SpecificBanksAccountPayload; + +import java.util.List; + +public class SpecificBanksAccountPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public SpecificBanksAccount toBusinessModel(SpecificBanksAccountPaymentAccount rest) { + SpecificBanksAccount business = new SpecificBanksAccount(); + business.init(); + SpecificBanksAccountPayload paymentAccountPayload = (SpecificBanksAccountPayload) business.getPaymentAccountPayload(); + paymentAccountPayload.setAccountNr(rest.accountNr); + paymentAccountPayload.setAccountType(rest.accountType); + paymentAccountPayload.setBankId(rest.bankId); + paymentAccountPayload.setBankName(rest.bankName); + paymentAccountPayload.setBranchId(rest.branchId); + paymentAccountPayload.setCountryCode(rest.countryCode); + paymentAccountPayload.setHolderName(rest.holderName); + paymentAccountPayload.setHolderTaxId(rest.holderTaxId); + rest.acceptedBanks.forEach(paymentAccountPayload::addAcceptedBank); + toBusinessModel(business, rest); + return business; + } + + @Override + public SpecificBanksAccountPaymentAccount toRestModel(SpecificBanksAccount business) { + SpecificBanksAccountPaymentAccount rest = toRestModel((SpecificBanksAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public SpecificBanksAccountPaymentAccount toRestModel(SpecificBanksAccountPayload business) { + SpecificBanksAccountPaymentAccount rest = new SpecificBanksAccountPaymentAccount(); + rest.accountNr = business.getAccountNr(); + rest.accountType = business.getAccountType(); + rest.bankId = business.getBankId(); + rest.bankName = business.getBankName(); + rest.branchId = business.getBranchId(); + rest.countryCode = business.getCountryCode(); + rest.holderName = business.getHolderName(); + rest.holderTaxId = business.getHolderTaxId(); + List acceptedBanks = business.getAcceptedBanks(); + if (acceptedBanks != null) + rest.acceptedBanks.addAll(acceptedBanks); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccount.java new file mode 100644 index 00000000000..3f9f992dd75 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.SWISH_ID) +public class SwishPaymentAccount extends PaymentAccount { + + @NotBlank + public String mobileNr; + + @NotBlank + public String holderName; + + public SwishPaymentAccount() { + super(PaymentMethod.SWISH_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccountConverter.java new file mode 100644 index 00000000000..fabde67c98a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/SwishPaymentAccountConverter.java @@ -0,0 +1,35 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.SwishAccount; +import bisq.core.payment.payload.SwishAccountPayload; + +public class SwishPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public SwishAccount toBusinessModel(SwishPaymentAccount rest) { + SwishAccount business = new SwishAccount(); + business.init(); + business.setMobileNr(rest.mobileNr); + business.setHolderName(rest.holderName); + toBusinessModel(business, rest); + return business; + } + + @Override + public SwishPaymentAccount toRestModel(SwishAccount business) { + SwishPaymentAccount rest = toRestModel((SwishAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public SwishPaymentAccount toRestModel(SwishAccountPayload business) { + SwishPaymentAccount rest = new SwishPaymentAccount(); + rest.mobileNr = business.getMobileNr(); + rest.holderName = business.getHolderName(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccount.java new file mode 100644 index 00000000000..fd0bb4aef4b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccount.java @@ -0,0 +1,23 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.US_POSTAL_MONEY_ORDER_ID) +public class USPostalMoneyOrderPaymentAccount extends PaymentAccount { + + @NotBlank + public String holderName; + + @NotBlank + public String postalAddress; + + public USPostalMoneyOrderPaymentAccount() { + super(PaymentMethod.US_POSTAL_MONEY_ORDER_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccountConverter.java new file mode 100644 index 00000000000..327f8063906 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/USPostalMoneyOrderPaymentAccountConverter.java @@ -0,0 +1,34 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; + +public class USPostalMoneyOrderPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public USPostalMoneyOrderAccount toBusinessModel(USPostalMoneyOrderPaymentAccount rest) { + USPostalMoneyOrderAccount business = new USPostalMoneyOrderAccount(); + business.init(); + business.setHolderName(rest.holderName); + business.setPostalAddress(rest.postalAddress); + toBusinessModel(business, rest); + return business; + } + + @Override + public USPostalMoneyOrderPaymentAccount toRestModel(USPostalMoneyOrderAccount business) { + USPostalMoneyOrderPaymentAccount rest = toRestModel((USPostalMoneyOrderAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public USPostalMoneyOrderPaymentAccount toRestModel(USPostalMoneyOrderAccountPayload business) { + USPostalMoneyOrderPaymentAccount rest = new USPostalMoneyOrderPaymentAccount(); + rest.holderName = business.getHolderName(); + rest.postalAddress = business.getPostalAddress(); + toRestModel(rest, business); + return rest; + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccount.java new file mode 100644 index 00000000000..095bd36ea8b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.UPHOLD_ID) +public class UpholdPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountId; + + public UpholdPaymentAccount() { + super(PaymentMethod.UPHOLD_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccountConverter.java new file mode 100644 index 00000000000..23904a92712 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/UpholdPaymentAccountConverter.java @@ -0,0 +1,33 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.UpholdAccount; +import bisq.core.payment.payload.UpholdAccountPayload; + +public class UpholdPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public UpholdAccount toBusinessModel(UpholdPaymentAccount rest) { + UpholdAccount business = new UpholdAccount(); + business.init(); + business.setAccountId(rest.accountId); + toBusinessModel(business, rest); + return business; + } + + @Override + public UpholdPaymentAccount toRestModel(UpholdAccount business) { + UpholdPaymentAccount rest = toRestModel((UpholdAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public UpholdPaymentAccount toRestModel(UpholdAccountPayload business) { + UpholdPaymentAccount rest = new UpholdPaymentAccount(); + rest.accountId = business.getAccountId(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccount.java new file mode 100644 index 00000000000..c820a1a3b4a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccount.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.WECHAT_PAY_ID) +public class WeChatPayPaymentAccount extends PaymentAccount { + + @NotBlank + public String accountNr; + + public WeChatPayPaymentAccount() { + super(PaymentMethod.WECHAT_PAY_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccountConverter.java new file mode 100644 index 00000000000..94a21f441e0 --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/WeChatPayPaymentAccountConverter.java @@ -0,0 +1,33 @@ +package bisq.api.http.model.payment; + +import bisq.core.payment.WeChatPayAccount; +import bisq.core.payment.payload.WeChatPayAccountPayload; + +public class WeChatPayPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public WeChatPayAccount toBusinessModel(WeChatPayPaymentAccount rest) { + WeChatPayAccount business = new WeChatPayAccount(); + business.init(); + business.setAccountNr(rest.accountNr); + toBusinessModel(business, rest); + return business; + } + + @Override + public WeChatPayPaymentAccount toRestModel(WeChatPayAccount business) { + WeChatPayPaymentAccount rest = toRestModel((WeChatPayAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public WeChatPayPaymentAccount toRestModel(WeChatPayAccountPayload business) { + WeChatPayPaymentAccount rest = new WeChatPayPaymentAccount(); + rest.accountNr = business.getAccountNr(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccount.java b/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccount.java new file mode 100644 index 00000000000..10759c3d73e --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccount.java @@ -0,0 +1,34 @@ +package bisq.api.http.model.payment; + +import bisq.api.http.model.validation.CountryCode; + +import bisq.core.payment.payload.PaymentMethod; + +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +import org.hibernate.validator.constraints.NotBlank; + +@JsonTypeName(PaymentMethod.WESTERN_UNION_ID) +public class WesternUnionPaymentAccount extends PaymentAccount { + + @NotBlank + public String city; + + @CountryCode + @NotBlank + public String countryCode; + + @NotBlank + public String email; + + @NotBlank + public String holderName; + + public String state; + + public WesternUnionPaymentAccount() { + super(PaymentMethod.WESTERN_UNION_ID); + } +} diff --git a/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccountConverter.java b/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccountConverter.java new file mode 100644 index 00000000000..db973834aaf --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/payment/WesternUnionPaymentAccountConverter.java @@ -0,0 +1,42 @@ +package bisq.api.http.model.payment; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.WesternUnionAccount; +import bisq.core.payment.payload.WesternUnionAccountPayload; + +public class WesternUnionPaymentAccountConverter extends AbstractPaymentAccountConverter { + + @Override + public WesternUnionAccount toBusinessModel(WesternUnionPaymentAccount rest) { + WesternUnionAccount business = new WesternUnionAccount(); + business.init(); + business.setFullName(rest.holderName); + business.setCity(rest.city); + CountryUtil.findCountryByCode(rest.countryCode).ifPresent(business::setCountry); + business.setEmail(rest.email); + business.setState(rest.state); + toBusinessModel(business, rest); + return business; + } + + @Override + public WesternUnionPaymentAccount toRestModel(WesternUnionAccount business) { + WesternUnionPaymentAccount rest = toRestModel((WesternUnionAccountPayload) business.getPaymentAccountPayload()); + toRestModel(rest, business); + return rest; + } + + @Override + public WesternUnionPaymentAccount toRestModel(WesternUnionAccountPayload business) { + WesternUnionPaymentAccount rest = new WesternUnionPaymentAccount(); + rest.holderName = business.getHolderName(); + rest.city = business.getCity(); + rest.countryCode = business.getCountryCode(); + rest.email = business.getEmail(); + rest.state = business.getState(); + toRestModel(rest, business); + return rest; + + } + +} diff --git a/api/src/main/java/bisq/api/http/model/validation/CountryCode.java b/api/src/main/java/bisq/api/http/model/validation/CountryCode.java new file mode 100644 index 00000000000..0cf89ea5d7a --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/validation/CountryCode.java @@ -0,0 +1,27 @@ +package bisq.api.http.model.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = CountryCodeValidator.class) +@Documented +public @interface CountryCode { + + String message() default "is not valid country code"; + + @SuppressWarnings("unused") + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/api/src/main/java/bisq/api/http/model/validation/CountryCodeValidator.java b/api/src/main/java/bisq/api/http/model/validation/CountryCodeValidator.java new file mode 100644 index 00000000000..4cb453e517b --- /dev/null +++ b/api/src/main/java/bisq/api/http/model/validation/CountryCodeValidator.java @@ -0,0 +1,20 @@ +package bisq.api.http.model.validation; + +import bisq.core.locale.CountryUtil; + + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class CountryCodeValidator implements ConstraintValidator { + + @Override + public void initialize(CountryCode constraintAnnotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || CountryUtil.findCountryByCode(value).isPresent(); + } +} diff --git a/api/src/main/java/bisq/api/http/service/ExperimentalFeature.java b/api/src/main/java/bisq/api/http/service/ExperimentalFeature.java new file mode 100644 index 00000000000..385d25106ec --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/ExperimentalFeature.java @@ -0,0 +1,25 @@ +package bisq.api.http.service; + +import bisq.api.http.exceptions.ExperimentalFeatureException; + +import bisq.core.app.AppOptionKeys; +import bisq.core.app.BisqEnvironment; + +import javax.inject.Inject; + +public class ExperimentalFeature { + + public static final String NOTE = "This is EXPERIMENTAL FEATURE! Run it at your own risk! Requires --" + AppOptionKeys.HTTP_API_EXPERIMENTAL_FEATURES_ENABLED + " flag at startup."; + private final BisqEnvironment environment; + + @Inject + public ExperimentalFeature(BisqEnvironment environment) { + this.environment = environment; + } + + public void assertEnabled() { + if (!environment.isHttpApiExperimentalFeaturesEnabled()) { + throw new ExperimentalFeatureException(); + } + } +} diff --git a/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java index b0e92e3580b..a17d84da6bf 100644 --- a/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java +++ b/api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java @@ -1,5 +1,6 @@ package bisq.api.http.service; +import bisq.api.http.service.endpoint.PaymentAccountEndpoint; import bisq.api.http.service.endpoint.UserEndpoint; import bisq.api.http.service.endpoint.VersionEndpoint; @@ -26,21 +27,29 @@ info = @Info(version = "0.0.1", title = "Bisq HTTP API"), security = @SecurityRequirement(name = "authorization"), tags = { + @Tag(name = "payment-accounts"), @Tag(name = "user"), @Tag(name = "version") } ) @Path("/api/v1") public class HttpApiInterfaceV1 { + private final PaymentAccountEndpoint paymentAccountEndpoint; private final UserEndpoint userEndpoint; private final VersionEndpoint versionEndpoint; @Inject - public HttpApiInterfaceV1(UserEndpoint userEndpoint, VersionEndpoint versionEndpoint) { + public HttpApiInterfaceV1(PaymentAccountEndpoint paymentAccountEndpoint, UserEndpoint userEndpoint, VersionEndpoint versionEndpoint) { + this.paymentAccountEndpoint = paymentAccountEndpoint; this.userEndpoint = userEndpoint; this.versionEndpoint = versionEndpoint; } + @Path("payment-accounts") + public PaymentAccountEndpoint getPaymentAccountEndpoint() { + return paymentAccountEndpoint; + } + @Path("user") public UserEndpoint getUserEndpoint() { return userEndpoint; diff --git a/api/src/main/java/bisq/api/http/service/ValidationErrorMessage.java b/api/src/main/java/bisq/api/http/service/ValidationErrorMessage.java new file mode 100644 index 00000000000..853e8e08a6f --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/ValidationErrorMessage.java @@ -0,0 +1,20 @@ +package bisq.api.http.service; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import com.google.common.collect.ImmutableList; + +public class ValidationErrorMessage { + private final ImmutableList errors; + + @JsonCreator + public ValidationErrorMessage(@JsonProperty("errors") ImmutableList errors) { + this.errors = errors; + } + + @JsonProperty + public ImmutableList getErrors() { + return errors; + } +} diff --git a/api/src/main/java/bisq/api/http/service/endpoint/PaymentAccountEndpoint.java b/api/src/main/java/bisq/api/http/service/endpoint/PaymentAccountEndpoint.java new file mode 100644 index 00000000000..f65deabfc2e --- /dev/null +++ b/api/src/main/java/bisq/api/http/service/endpoint/PaymentAccountEndpoint.java @@ -0,0 +1,88 @@ +package bisq.api.http.service.endpoint; + +import bisq.api.http.facade.PaymentAccountFacade; +import bisq.api.http.model.PaymentAccountList; +import bisq.api.http.model.payment.PaymentAccount; +import bisq.api.http.model.payment.PaymentAccountHelper; +import bisq.api.http.service.ExperimentalFeature; + +import bisq.common.UserThread; + +import javax.inject.Inject; + + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Tag(name = "payment-accounts") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class PaymentAccountEndpoint { + + private final ExperimentalFeature experimentalFeature; + private final PaymentAccountFacade paymentAccountFacade; + + @Inject + public PaymentAccountEndpoint(ExperimentalFeature experimentalFeature, PaymentAccountFacade paymentAccountFacade) { + this.experimentalFeature = experimentalFeature; + this.paymentAccountFacade = paymentAccountFacade; + } + + @Operation(summary = "Remove payment account", description = ExperimentalFeature.NOTE) + @DELETE + @Path("/{id}") + public void removeById(@Suspended AsyncResponse asyncResponse, @PathParam("id") String id) { + UserThread.execute(() -> { + try { + experimentalFeature.assertEnabled(); + paymentAccountFacade.removePaymentAccount(id); + asyncResponse.resume(Response.status(Response.Status.NO_CONTENT).build()); + } catch (Throwable e) { + asyncResponse.resume(e); + } + }); + } + + @Operation(summary = "Create payment account", description = ExperimentalFeature.NOTE + "\nInspect models section at the bottom of the page for valid PaymentAccount sub-types schemas", responses = @ApiResponse(content = @Content(schema = @Schema(implementation = PaymentAccount.class)))) + @POST + public void create(@Suspended AsyncResponse asyncResponse, @Valid PaymentAccount account) { + UserThread.execute(() -> { + try { + experimentalFeature.assertEnabled(); + bisq.core.payment.PaymentAccount paymentAccount = PaymentAccountHelper.toBusinessModel(account); + PaymentAccount result = PaymentAccountHelper.toRestModel(paymentAccountFacade.addPaymentAccount(paymentAccount)); + asyncResponse.resume(result); + } catch (Throwable e) { + asyncResponse.resume(e); + } + }); + } + + @Operation(summary = "Get existing payment accounts", responses = @ApiResponse(content = @Content(schema = @Schema(implementation = PaymentAccountList.class)))) + @GET + public void find(@Suspended AsyncResponse asyncResponse) { + UserThread.execute(() -> { + try { + asyncResponse.resume(paymentAccountFacade.getAccountList()); + } catch (Throwable e) { + asyncResponse.resume(e); + } + }); + } +} diff --git a/api/src/test/java/bisq/api/http/model/validation/CountryCodeValidatorTest.java b/api/src/test/java/bisq/api/http/model/validation/CountryCodeValidatorTest.java new file mode 100644 index 00000000000..0ca21a4d0e9 --- /dev/null +++ b/api/src/test/java/bisq/api/http/model/validation/CountryCodeValidatorTest.java @@ -0,0 +1,51 @@ +package bisq.api.http.model.validation; + +import bisq.core.locale.CountryUtil; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CountryCodeValidatorTest { + + @Test + public void isValid_nullValue_returnTrue() { + // Given + CountryCodeValidator validator = new CountryCodeValidator(); + + // When + boolean result = validator.isValid(null, null); + + // Then + assertTrue(result); + } + + @Test + public void isValid_unsupportedCountryCode_returnFalse() { + // Given + CountryCodeValidator validator = new CountryCodeValidator(); + String countryCode = "USA"; + assertFalse(CountryUtil.findCountryByCode(countryCode).isPresent()); + + // When + boolean result = validator.isValid(countryCode, null); + + // Then + assertFalse(result); + } + + @Test + public void isValid_supportedCountryCode_returnTrue() { + // Given + CountryCodeValidator validator = new CountryCodeValidator(); + String currencyCode = "US"; + assertTrue(CountryUtil.findCountryByCode(currencyCode).isPresent()); + + // When + boolean result = validator.isValid(currencyCode, null); + + // Then + assertTrue(result); + } +} diff --git a/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java b/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java index 66530893655..445c1ede0a3 100644 --- a/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java +++ b/api/src/testIntegration/java/bisq/api/http/ApiTestHelper.java @@ -1,5 +1,20 @@ package bisq.api.http; +import bisq.api.http.model.payment.PaymentAccount; +import bisq.api.http.model.payment.SepaPaymentAccount; + +import bisq.core.locale.CountryUtil; +import bisq.core.payment.payload.PaymentMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + + + +import com.github.javafaker.Faker; + @SuppressWarnings("WeakerAccess") public final class ApiTestHelper { @@ -10,4 +25,34 @@ public static void waitForAllServicesToBeReady() throws InterruptedException { Thread.sleep(ALL_SERVICES_INITIALIZED_DELAY); } + public static SepaPaymentAccount randomValidCreateSepaAccountPayload(String tradeCurrency, String countryCode) { + Faker faker = new Faker(); + SepaPaymentAccount accountToCreate = new SepaPaymentAccount(); + if (countryCode == null) + countryCode = faker.options().nextElement(CountryUtil.getAllSepaCountries()).code; + accountToCreate.paymentMethod = PaymentMethod.SEPA_ID; + accountToCreate.accountName = faker.commerce().productName(); + accountToCreate.bic = faker.finance().bic(); + accountToCreate.iban = faker.finance().iban(); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.countryCode = countryCode; + accountToCreate.acceptedCountries = new ArrayList<>(new HashSet<>(Arrays.asList("PL", "GB", countryCode))); + accountToCreate.selectedTradeCurrency = faker.options().option("PLN", "USD", "EUR", "GBP"); + if (tradeCurrency != null) + accountToCreate.selectedTradeCurrency = tradeCurrency; + accountToCreate.tradeCurrencies = Collections.singletonList(accountToCreate.selectedTradeCurrency); + return accountToCreate; + } + + public static void randomizeAccountPayload(PaymentAccount accountToCreate) { + Faker faker = new Faker(); + accountToCreate.accountName = faker.commerce().productName(); + accountToCreate.selectedTradeCurrency = faker.options().option("PLN", "USD", "EUR", "GBP"); + accountToCreate.tradeCurrencies = Collections.singletonList(accountToCreate.selectedTradeCurrency); + } + + public static SepaPaymentAccount randomValidCreateSepaAccountPayload() { + return randomValidCreateSepaAccountPayload(null, null); + } + } diff --git a/api/src/testIntegration/java/bisq/api/http/ExperimentalFeatureIT.java b/api/src/testIntegration/java/bisq/api/http/ExperimentalFeatureIT.java new file mode 100644 index 00000000000..b70ff61c8cd --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/ExperimentalFeatureIT.java @@ -0,0 +1,107 @@ +package bisq.api.http; + +import bisq.api.http.model.AuthForm; +import bisq.api.http.model.ChangePassword; +import bisq.api.http.model.payment.SepaPaymentAccount; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + + + +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class ExperimentalFeatureIT { + + @DockerContainer + Container alice = ContainerFactory.createApiContainer("alice", "8080->8080", 3333, false, false, false); + + @InSequence + @Test + public void waitForAllServicesToBeReady() throws InterruptedException { + ApiTestHelper.waitForAllServicesToBeReady(); + } + + @InSequence(1) + @Test + public void createPaymentAccount_always_returns501() { + SepaPaymentAccount accountToCreate = ApiTestHelper.randomValidCreateSepaAccountPayload(); + Response response = given(). + port(getAlicePort()). + contentType(ContentType.JSON). + body(accountToCreate). + when(). + post("/api/v1/payment-accounts"); + expect501(response); + } + + @InSequence(1) + @Test + public void removePaymentAccountById_always_returns501() { + expect501(given().port(getAlicePort()).when().delete("/api/v1/payment-accounts/xyz")); + } + + @InSequence(1) + @Test + public void searchPaymentAccounts_always_returns200() { + given().port(getAlicePort()).when().get("/api/v1/payment-accounts").then().statusCode(200); + } + + @InSequence(1) + @Test + public void getVersionDetails_always_returns200() { + given().port(getAlicePort()).when().get("/api/v1/payment-accounts").then().statusCode(200); + } + + @InSequence(1) + @Test + public void authenticate_always_returns501() { +// + given(). + port(getAlicePort()). + body(new AuthForm("abc")). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/authenticate"). +// + then(). + statusCode(200); + } + + @InSequence(1) + @Test + public void changePassword_always_returns200() { +// + given(). + port(getAlicePort()). + body(new ChangePassword("abc", null)). + contentType(ContentType.JSON). +// + when(). + post("/api/v1/user/password"). +// + then(). + statusCode(200); + } + + private void expect501(Response response) { + response.then(). + statusCode(501). + body("errors[0]", equalTo("Experimental features disabled")). + body("errors.size()", equalTo(1)); + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } +} diff --git a/api/src/testIntegration/java/bisq/api/http/PaymentAccountEndpointIT.java b/api/src/testIntegration/java/bisq/api/http/PaymentAccountEndpointIT.java new file mode 100644 index 00000000000..8b7ada7c867 --- /dev/null +++ b/api/src/testIntegration/java/bisq/api/http/PaymentAccountEndpointIT.java @@ -0,0 +1,1236 @@ +package bisq.api.http; + +import bisq.api.http.model.payment.AdvancedCashPaymentAccount; +import bisq.api.http.model.payment.AliPayPaymentAccount; +import bisq.api.http.model.payment.CashDepositPaymentAccount; +import bisq.api.http.model.payment.ChaseQuickPayPaymentAccount; +import bisq.api.http.model.payment.ClearXchangePaymentAccount; +import bisq.api.http.model.payment.CryptoCurrencyPaymentAccount; +import bisq.api.http.model.payment.F2FPaymentAccount; +import bisq.api.http.model.payment.FasterPaymentsPaymentAccount; +import bisq.api.http.model.payment.HalCashPaymentAccount; +import bisq.api.http.model.payment.InteracETransferPaymentAccount; +import bisq.api.http.model.payment.MoneyBeamPaymentAccount; +import bisq.api.http.model.payment.MoneyGramPaymentAccount; +import bisq.api.http.model.payment.NationalBankAccountPaymentAccount; +import bisq.api.http.model.payment.PaymentAccount; +import bisq.api.http.model.payment.PerfectMoneyPaymentAccount; +import bisq.api.http.model.payment.PopmoneyPaymentAccount; +import bisq.api.http.model.payment.PromptPayPaymentAccount; +import bisq.api.http.model.payment.RevolutPaymentAccount; +import bisq.api.http.model.payment.SameBankAccountPaymentAccount; +import bisq.api.http.model.payment.SepaInstantPaymentAccount; +import bisq.api.http.model.payment.SepaPaymentAccount; +import bisq.api.http.model.payment.SpecificBanksAccountPaymentAccount; +import bisq.api.http.model.payment.SwishPaymentAccount; +import bisq.api.http.model.payment.USPostalMoneyOrderPaymentAccount; +import bisq.api.http.model.payment.UpholdPaymentAccount; +import bisq.api.http.model.payment.WeChatPayPaymentAccount; +import bisq.api.http.model.payment.WesternUnionPaymentAccount; + +import bisq.core.locale.CountryUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + + + +import com.github.javafaker.Faker; +import io.restassured.http.ContentType; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class PaymentAccountEndpointIT { + + @DockerContainer + private Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + @InSequence + @Test + public void waitForAllServicesToBeReady() throws InterruptedException { +// PaymentMethod initializes it's static values after all services get initialized + ApiTestHelper.waitForAllServicesToBeReady(); + } + + @InSequence(1) + @Test + public void create_validSepa_returnsCreatedAccount() { + int alicePort = getAlicePort(); + + SepaPaymentAccount accountToCreate = ApiTestHelper.randomValidCreateSepaAccountPayload(); + + String expectedPaymentDetails = String.format("SEPA - Account owner full name: %s, IBAN: %s, BIC: %s, Country of bank: %s", accountToCreate.holderName, accountToCreate.iban, accountToCreate.bic, accountToCreate.countryCode); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("bic", equalTo(accountToCreate.bic)). + and().body("iban", equalTo(accountToCreate.iban)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("acceptedCountries", equalTo(accountToCreate.acceptedCountries)). + and().body("size()", equalTo(11)) + ; + + given(). + port(alicePort). +// + when(). + get("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("paymentAccounts.size()", equalTo(1)). + and().body("paymentAccounts[0].id", isA(String.class)). + and().body("paymentAccounts[0].paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("paymentAccounts[0].accountName", equalTo(accountToCreate.accountName)). + and().body("paymentAccounts[0].paymentDetails", equalTo(expectedPaymentDetails)). + and().body("paymentAccounts[0].holderName", equalTo(accountToCreate.holderName)). + and().body("paymentAccounts[0].countryCode", equalTo(accountToCreate.countryCode)). + and().body("paymentAccounts[0].bic", equalTo(accountToCreate.bic)). + and().body("paymentAccounts[0].iban", equalTo(accountToCreate.iban)). + and().body("paymentAccounts[0].selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("paymentAccounts[0].tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("paymentAccounts[0].acceptedCountries", equalTo(accountToCreate.acceptedCountries)). + and().body("paymentAccounts[0].size()", equalTo(11)) + ; + } + + @InSequence(1) + @Test + public void create_validAdvancedCash_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + AdvancedCashPaymentAccount accountToCreate = new AdvancedCashPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + + String expectedPaymentDetails = String.format("Advanced Cash - Wallet ID: %s", accountToCreate.accountNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(1) + @Test + public void create_validAliPay_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + AliPayPaymentAccount accountToCreate = new AliPayPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + + String expectedPaymentDetails = String.format("AliPay - Account no.: %s", accountToCreate.accountNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validCashDeposit_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + CashDepositPaymentAccount accountToCreate = new CashDepositPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + accountToCreate.accountType = faker.options().option("savings", "avista"); + accountToCreate.bankId = faker.finance().bic(); + accountToCreate.bankName = faker.company().name(); + accountToCreate.branchId = faker.company().buzzword(); + accountToCreate.countryCode = "DE"; + accountToCreate.holderEmail = faker.internet().emailAddress(); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.holderTaxId = faker.finance().creditCard(); + accountToCreate.requirements = faker.witcher().quote(); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", isA(String.class)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("accountType", equalTo(accountToCreate.accountType)). + and().body("bankId", equalTo(accountToCreate.bankId)). + and().body("bankName", equalTo(accountToCreate.bankName)). + and().body("branchId", equalTo(accountToCreate.branchId)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("holderEmail", equalTo(accountToCreate.holderEmail)). + and().body("holderTaxId", equalTo(accountToCreate.holderTaxId)). + and().body("requirements", equalTo(accountToCreate.requirements)). + and().body("size()", equalTo(16)) + ; + } + + @InSequence(2) + @Test + public void create_validChaseQuickPay_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + ChaseQuickPayPaymentAccount accountToCreate = new ChaseQuickPayPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.email = faker.internet().emailAddress(); + accountToCreate.holderName = faker.name().fullName(); + + String expectedPaymentDetails = String.format("Chase QuickPay - Account owner full name: %s, Email %s", accountToCreate.holderName, accountToCreate.email); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("email", equalTo(accountToCreate.email)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(2) + @Test + public void create_validClearXchange_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + ClearXchangePaymentAccount accountToCreate = new ClearXchangePaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.emailOrMobileNr = faker.internet().emailAddress(); + accountToCreate.holderName = faker.name().fullName(); + + String expectedPaymentDetails = String.format("Zelle (ClearXchange) - Account owner full name: %s, Email or mobile nr: %s", accountToCreate.holderName, accountToCreate.emailOrMobileNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("emailOrMobileNr", equalTo(accountToCreate.emailOrMobileNr)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(2) + @Test + public void create_validCryptoCurrency_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + CryptoCurrencyPaymentAccount accountToCreate = new CryptoCurrencyPaymentAccount(); + accountToCreate.accountName = faker.commerce().productName(); + accountToCreate.selectedTradeCurrency = "LTC"; + accountToCreate.tradeCurrencies = Collections.singletonList(accountToCreate.selectedTradeCurrency); + accountToCreate.address = "Ldsb1sfBw4KVv8hvKm9y9EEqBMonKT6Akn"; + + String expectedPaymentDetails = String.format("Receiver's altcoin address: %s", accountToCreate.address); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("address", equalTo(accountToCreate.address)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validF2F_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + F2FPaymentAccount accountToCreate = new F2FPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.city = faker.address().city(); + accountToCreate.contact = faker.phoneNumber().cellPhone(); + accountToCreate.extraInfo = faker.address().fullAddress(); + + String expectedPaymentDetails = String.format("Face to face (in person) - Contact info: %s, City for 'Face to face' meeting: %s, Additional information: %s", accountToCreate.contact, accountToCreate.city, accountToCreate.extraInfo); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("city", equalTo(accountToCreate.city)). + and().body("contact", equalTo(accountToCreate.contact)). + and().body("extraInfo", equalTo(accountToCreate.extraInfo)). + and().body("size()", equalTo(9)) + ; + } + + @InSequence(2) + @Test + public void create_validFasterPayments_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + FasterPaymentsPaymentAccount accountToCreate = new FasterPaymentsPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + accountToCreate.sortCode = faker.address().zipCode(); + + String expectedPaymentDetails = String.format("Faster Payments - UK Sort code: %s, Account number: %s", accountToCreate.sortCode, accountToCreate.accountNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("sortCode", equalTo(accountToCreate.sortCode)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(2) + @Test + public void create_validHalCash_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + HalCashPaymentAccount accountToCreate = new HalCashPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.mobileNr = faker.phoneNumber().cellPhone(); + + String expectedPaymentDetails = String.format("HalCash - Mobile no.: %s", accountToCreate.mobileNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("mobileNr", equalTo(accountToCreate.mobileNr)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validInteracETransfer_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + InteracETransferPaymentAccount accountToCreate = new InteracETransferPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.emailOrMobileNr = faker.internet().emailAddress(); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.question = faker.witcher().quote(); + accountToCreate.answer = faker.witcher().character(); + + String expectedPaymentDetails = String.format("Interac e-Transfer - Account owner full name: %s, Email %s, Secret question: %s, Answer: %s", accountToCreate.holderName, accountToCreate.emailOrMobileNr, accountToCreate.question, accountToCreate.answer); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("emailOrMobileNr", equalTo(accountToCreate.emailOrMobileNr)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("question", equalTo(accountToCreate.question)). + and().body("answer", equalTo(accountToCreate.answer)). + and().body("size()", equalTo(10)) + ; + } + + @InSequence(2) + @Test + public void create_validMoneyBeam_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + MoneyBeamPaymentAccount accountToCreate = new MoneyBeamPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountId = faker.idNumber().valid(); + + String expectedPaymentDetails = String.format("MoneyBeam (N26) - Account: %s", accountToCreate.accountId); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountId", equalTo(accountToCreate.accountId)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validMoneyGram_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + MoneyGramPaymentAccount accountToCreate = new MoneyGramPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.countryCode = "US"; + accountToCreate.state = faker.address().state(); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.email = faker.internet().emailAddress(); + + String expectedPaymentDetails = String.format("MoneyGram - Full name (first, middle, last): %s, State/Province/Region: %s, Country of bank: %s, Email: %s", accountToCreate.holderName, accountToCreate.state, CountryUtil.getNameByCode(accountToCreate.countryCode), accountToCreate.email); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("state", equalTo(accountToCreate.state)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("email", equalTo(accountToCreate.email)). + and().body("size()", equalTo(10)) + ; + } + + @InSequence(2) + @Test + public void create_validNationalBankAccount_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + NationalBankAccountPaymentAccount accountToCreate = new NationalBankAccountPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + accountToCreate.accountType = faker.options().option("savings", "avista"); + accountToCreate.bankId = faker.finance().bic(); + accountToCreate.bankName = faker.company().name(); + accountToCreate.branchId = faker.company().buzzword(); + accountToCreate.countryCode = "DE"; + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.holderTaxId = faker.finance().creditCard(); + + String expectedPaymentDetails = String.format("National bank transfer - Account owner full name: %s, Bank name: %s, Bank ID (BIC/SWIFT): %s, Branch no.: %s, Account no. (IBAN): %s, Country of bank: %s", accountToCreate.holderName, accountToCreate.bankName, accountToCreate.bankId, accountToCreate.branchId, accountToCreate.accountNr, CountryUtil.getNameByCode(accountToCreate.countryCode)); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("accountType", equalTo(accountToCreate.accountType)). + and().body("bankId", equalTo(accountToCreate.bankId)). + and().body("bankName", equalTo(accountToCreate.bankName)). + and().body("branchId", equalTo(accountToCreate.branchId)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("holderTaxId", equalTo(accountToCreate.holderTaxId)). + and().body("size()", equalTo(14)) + ; + } + + @InSequence(2) + @Test + public void create_validPerfectMoney_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + PerfectMoneyPaymentAccount accountToCreate = new PerfectMoneyPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.idNumber().valid(); + + String expectedPaymentDetails = String.format("Perfect Money - Account no.: %s", accountToCreate.accountNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validPopmoney_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + PopmoneyPaymentAccount accountToCreate = new PopmoneyPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountId = faker.idNumber().valid(); + accountToCreate.holderName = faker.name().fullName(); + + String expectedPaymentDetails = String.format("Popmoney - Account owner full name: %s, Email or phone no.: %s", accountToCreate.holderName, accountToCreate.accountId); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountId", equalTo(accountToCreate.accountId)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(1) + @Test + public void create_validPromptPay_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + PromptPayPaymentAccount accountToCreate = new PromptPayPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.promptPayId = faker.finance().iban(); + + String expectedPaymentDetails = String.format("Citizen ID/Tax ID or phone no.: %s", accountToCreate.promptPayId); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("promptPayId", equalTo(accountToCreate.promptPayId)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validRevolut_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + RevolutPaymentAccount accountToCreate = new RevolutPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountId = faker.idNumber().valid(); + + String expectedPaymentDetails = String.format("Revolut - Account: %s", accountToCreate.accountId); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountId", equalTo(accountToCreate.accountId)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validSameBankAccount_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + SameBankAccountPaymentAccount accountToCreate = new SameBankAccountPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + accountToCreate.accountType = faker.options().option("savings", "avista"); + accountToCreate.bankId = faker.finance().bic(); + accountToCreate.bankName = faker.company().name(); + accountToCreate.branchId = faker.company().buzzword(); + accountToCreate.countryCode = "PL"; + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.holderTaxId = faker.finance().creditCard(); + + String expectedPaymentDetails = String.format("Transfer with same bank - Account owner full name: %s, Bank name: %s, Bank ID (BIC/SWIFT): %s, Branch no.: %s, Account no. (IBAN): %s, Country of bank: %s", accountToCreate.holderName, accountToCreate.bankName, accountToCreate.bankId, accountToCreate.branchId, accountToCreate.accountNr, CountryUtil.getNameByCode(accountToCreate.countryCode)); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("accountType", equalTo(accountToCreate.accountType)). + and().body("bankId", equalTo(accountToCreate.bankId)). + and().body("bankName", equalTo(accountToCreate.bankName)). + and().body("branchId", equalTo(accountToCreate.branchId)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("holderTaxId", equalTo(accountToCreate.holderTaxId)). + and().body("size()", equalTo(14)) + ; + } + + @InSequence(2) + @Test + public void create_validSepaInstant_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + SepaInstantPaymentAccount accountToCreate = new SepaInstantPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountName = faker.commerce().productName(); + accountToCreate.bic = faker.finance().bic(); + accountToCreate.iban = faker.finance().iban(); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.countryCode = faker.address().countryCode(); + accountToCreate.acceptedCountries = Arrays.asList("PL", "GB"); + accountToCreate.selectedTradeCurrency = faker.options().option("PLN", "USD", "EUR", "GBP"); + accountToCreate.tradeCurrencies = Collections.singletonList(accountToCreate.selectedTradeCurrency); + + String expectedPaymentDetails = String.format("SEPA Instant Payments - Account owner full name: %s, IBAN: %s, BIC: %s, Country of bank: %s", accountToCreate.holderName, accountToCreate.iban, accountToCreate.bic, accountToCreate.countryCode); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("bic", equalTo(accountToCreate.bic)). + and().body("iban", equalTo(accountToCreate.iban)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("acceptedCountries", equalTo(accountToCreate.acceptedCountries)). + and().body("size()", equalTo(11)) + ; + } + + @InSequence(2) + @Test + public void create_validSpecificBanks_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + SpecificBanksAccountPaymentAccount accountToCreate = new SpecificBanksAccountPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().iban(); + accountToCreate.accountType = faker.options().option("savings", "avista"); + accountToCreate.bankId = faker.finance().bic(); + accountToCreate.bankName = faker.company().name(); + accountToCreate.branchId = faker.company().buzzword(); + accountToCreate.countryCode = "AT"; + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.holderTaxId = faker.finance().creditCard(); + accountToCreate.acceptedBanks = Arrays.asList(faker.finance().bic(), faker.finance().bic()); + + String acceptedBanks = accountToCreate.acceptedBanks.stream().reduce((i, a) -> a.length() > 0 ? i + ", " + a : i).orElse(""); + String expectedPaymentDetails = String.format("Transfers with specific banks - Account owner full name: %s, Bank name: %s, Bank ID (BIC/SWIFT): %s, Branch no.: %s, Account no. (IBAN): %s, Country of bank: %s, Accepted banks (ID): %s", accountToCreate.holderName, accountToCreate.bankName, accountToCreate.bankId, accountToCreate.branchId, accountToCreate.accountNr, CountryUtil.getNameByCode(accountToCreate.countryCode), acceptedBanks); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("accountType", equalTo(accountToCreate.accountType)). + and().body("bankId", equalTo(accountToCreate.bankId)). + and().body("bankName", equalTo(accountToCreate.bankName)). + and().body("branchId", equalTo(accountToCreate.branchId)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("holderTaxId", equalTo(accountToCreate.holderTaxId)). + and().body("acceptedBanks", equalTo(accountToCreate.acceptedBanks)). + and().body("size()", equalTo(15)) + ; + } + + @InSequence(2) + @Test + public void create_validSwish_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + SwishPaymentAccount accountToCreate = new SwishPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.mobileNr = faker.phoneNumber().cellPhone(); + + String expectedPaymentDetails = String.format("Swish - Account owner full name: %s, Mobile no.: %s", accountToCreate.holderName, accountToCreate.mobileNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("mobileNr", equalTo(accountToCreate.mobileNr)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(2) + @Test + public void create_validUphold_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + UpholdPaymentAccount accountToCreate = new UpholdPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountId = faker.idNumber().valid(); + + String expectedPaymentDetails = String.format("Uphold - Account: %s", accountToCreate.accountId); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountId", equalTo(accountToCreate.accountId)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validUSPostalMoneyOrder_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + USPostalMoneyOrderPaymentAccount accountToCreate = new USPostalMoneyOrderPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.postalAddress = faker.address().fullAddress(); + + String expectedPaymentDetails = String.format("US Postal Money Order - Account owner full name: %s, Postal address: %s", accountToCreate.holderName, accountToCreate.postalAddress); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("postalAddress", equalTo(accountToCreate.postalAddress)). + and().body("size()", equalTo(8)) + ; + } + + @InSequence(2) + @Test + public void create_validWeChatPay_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + WeChatPayPaymentAccount accountToCreate = new WeChatPayPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.accountNr = faker.finance().bic(); + + String expectedPaymentDetails = String.format("WeChat Pay - Account no.: %s", accountToCreate.accountNr); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("accountNr", equalTo(accountToCreate.accountNr)). + and().body("size()", equalTo(7)) + ; + } + + @InSequence(2) + @Test + public void create_validWesternUnion_returnsCreatedAccount() { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + WesternUnionPaymentAccount accountToCreate = new WesternUnionPaymentAccount(); + ApiTestHelper.randomizeAccountPayload(accountToCreate); + accountToCreate.holderName = faker.name().fullName(); + accountToCreate.city = faker.address().city(); + accountToCreate.countryCode = "DE"; + accountToCreate.email = faker.internet().emailAddress(); + accountToCreate.state = faker.address().state(); + + String expectedPaymentDetails = String.format("Western Union - Full name (first, middle, last): %s, City: %s, Country: %s, Email: %s", accountToCreate.holderName, accountToCreate.city, CountryUtil.getNameByCode(accountToCreate.countryCode), accountToCreate.email); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("id", isA(String.class)). + and().body("paymentMethod", equalTo(accountToCreate.paymentMethod)). + and().body("accountName", equalTo(accountToCreate.accountName)). + and().body("paymentDetails", equalTo(expectedPaymentDetails)). + and().body("selectedTradeCurrency", equalTo(accountToCreate.selectedTradeCurrency)). + and().body("tradeCurrencies", equalTo(accountToCreate.tradeCurrencies)). + and().body("holderName", equalTo(accountToCreate.holderName)). + and().body("city", equalTo(accountToCreate.city)). + and().body("countryCode", equalTo(accountToCreate.countryCode)). + and().body("email", equalTo(accountToCreate.email)). + and().body("state", equalTo(accountToCreate.state)). + and().body("size()", equalTo(11)) + ; + } + + @InSequence(3) + @Test + public void removeById_existingAccount_returns204() { + int alicePort = getAlicePort(); + + List accountList = given(). + port(alicePort). + contentType(ContentType.JSON). + when(). + get("/api/v1/payment-accounts/"). + getBody().jsonPath().get("paymentAccounts.id"); + + Assert.assertThat(accountList, not(empty())); + + accountList.forEach(id -> given().port(alicePort).when().delete("/api/v1/payment-accounts/" + id).then().statusCode(204)); + + given(). + port(alicePort). +// + when(). + get("/api/v1/payment-accounts"). +// + then(). + statusCode(200). + and().body("paymentAccounts.size()", equalTo(0)) + ; + } + + @InSequence(4) + @Test + public void removeById_nonExistingAccount_returns404() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + delete("/api/v1/payment-accounts/abc"). +// + then(). + statusCode(404); + } + + @Test + public void create_missingAccountName_returnsError() throws Exception { + create_missingAttributeTemplate("accountName", null); + create_missingAttributeTemplate("accountName", " "); + } + + @InSequence(1) + @Test + public void create_unsupportedCryptoSelectedTradeCurrency_returnsError() throws Exception { + create_cryptoValidationFailureTemplate("selectedTradeCurrency", "BCHX", "Unsupported crypto currency code: BCHX"); + } + + @InSequence(1) + @Test + public void create_unsupportedCryptoTradeCurrency_returnsError() throws Exception { + create_cryptoValidationFailureTemplate("tradeCurrencies", Collections.singletonList("XYZ"), "Unsupported crypto currency code: XYZ"); + } + + @InSequence(1) + @Test + public void create_invalidCryptoAddress_returnsError() throws Exception { + create_cryptoValidationFailureTemplate("address", "abc", "Address is not a valid LTC address! Input too short"); + } + + @InSequence(1) + @Test + public void create_missingCountryCode_returnsError() throws Exception { + create_missingAttributeTemplate("countryCode", null); + create_missingAttributeTemplate("countryCode", " "); + } + + @InSequence(1) + @Test + public void create_missingHolderName_returnsError() throws Exception { + create_missingAttributeTemplate("holderName", null); + create_missingAttributeTemplate("holderName", " "); + } + + @InSequence(1) + @Test + public void create_missingBic_returnsError() throws Exception { + create_missingAttributeTemplate("bic", null); + create_missingAttributeTemplate("bic", " "); + } + + @InSequence(1) + @Test + public void create_missingIban_returnsError() throws Exception { + create_missingAttributeTemplate("iban", null); + create_missingAttributeTemplate("iban", " "); + } + + @InSequence(1) + @Test + public void create_invalidCountryCode_returnsError() throws Exception { + create_sepaValidationFailureTemplate("countryCode", "PLNX", "countryCode is not valid country code"); + } + + @InSequence(1) + @Test + public void create_invalidPaymentMethod_returnsError() { + int alicePort = getAlicePort(); + + PaymentAccount accountToCreate = new PaymentAccount("") { + }; + ApiTestHelper.randomizeAccountPayload(accountToCreate); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(422). + and().body("errors.size()", equalTo(1)). + and().body("errors[0]", equalTo("Unable to recognize sub type of PaymentAccount. Value 'null' is invalid. Allowed values are: ADVANCED_CASH, ALI_PAY, CASH_DEPOSIT, CHASE_QUICK_PAY, CLEAR_X_CHANGE, BLOCK_CHAINS, F2F, FASTER_PAYMENTS, HAL_CASH, INTERAC_E_TRANSFER, MONEY_BEAM, MONEY_GRAM, NATIONAL_BANK, PERFECT_MONEY, POPMONEY, PROMPT_PAY, REVOLUT, SAME_BANK, SEPA, SEPA_INSTANT, SPECIFIC_BANKS, SWISH, UPHOLD, US_POSTAL_MONEY_ORDER, WECHAT_PAY, WESTERN_UNION")) + ; + } + + private void create_missingAttributeTemplate(String fieldName, Object fieldValue) throws Exception { + create_sepaValidationFailureTemplate(fieldName, fieldValue, fieldName + " may not be empty"); + } + + private void create_sepaValidationFailureTemplate(String fieldName, Object fieldValue, String expectedValidationMessage) throws Exception { + int alicePort = getAlicePort(); + + SepaPaymentAccount accountToCreate = ApiTestHelper.randomValidCreateSepaAccountPayload(); + SepaPaymentAccount.class.getField(fieldName).set(accountToCreate, fieldValue); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(422). + and().body("errors", hasItem(expectedValidationMessage)) + ; + } + + private void create_cryptoValidationFailureTemplate(String fieldName, Object fieldValue, String expectedValidationMessage) throws Exception { + int alicePort = getAlicePort(); + Faker faker = new Faker(); + + CryptoCurrencyPaymentAccount accountToCreate = new CryptoCurrencyPaymentAccount(); + accountToCreate.accountName = faker.commerce().productName(); + accountToCreate.selectedTradeCurrency = "LTC"; + accountToCreate.tradeCurrencies = Collections.singletonList(accountToCreate.selectedTradeCurrency); + accountToCreate.address = "1ab616x3JxQsXsExCKX4iirdFwVDDXuwo"; + CryptoCurrencyPaymentAccount.class.getField(fieldName).set(accountToCreate, fieldValue); + + given(). + port(alicePort). + contentType(ContentType.JSON). + body(accountToCreate). +// + when(). + post("/api/v1/payment-accounts"). +// + then(). + statusCode(422). + and().body("errors", hasItem(expectedValidationMessage)) + ; + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/core/src/main/java/bisq/core/exceptions/ValidationException.java b/core/src/main/java/bisq/core/exceptions/ValidationException.java new file mode 100644 index 00000000000..ac0de27971d --- /dev/null +++ b/core/src/main/java/bisq/core/exceptions/ValidationException.java @@ -0,0 +1,39 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.exceptions; + +/** + * Copy of ValidationException from javax.validation:validation-api + */ +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } + + public ValidationException() { + super(); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ValidationException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountManager.java b/core/src/main/java/bisq/core/payment/PaymentAccountManager.java new file mode 100644 index 00000000000..a35b901cd43 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/PaymentAccountManager.java @@ -0,0 +1,74 @@ +package bisq.core.payment; + +import bisq.core.exceptions.ValidationException; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.validation.AltCoinAddressValidator; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.validation.InputValidator; + +import com.google.inject.Inject; + +import java.util.List; + +public class PaymentAccountManager { + + private final AccountAgeWitnessService accountAgeWitnessService; + private final Preferences preferences; + private final User user; + private AltCoinAddressValidator altCoinAddressValidator; + + @Inject + public PaymentAccountManager(AccountAgeWitnessService accountAgeWitnessService, AltCoinAddressValidator altCoinAddressValidator, Preferences preferences, User user) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.altCoinAddressValidator = altCoinAddressValidator; + this.preferences = preferences; + this.user = user; + } + + public PaymentAccount addPaymentAccount(PaymentAccount paymentAccount) { + if (paymentAccount instanceof CryptoCurrencyAccount) { + CryptoCurrencyAccount cryptoCurrencyAccount = (CryptoCurrencyAccount) paymentAccount; + TradeCurrency tradeCurrency = cryptoCurrencyAccount.getSingleTradeCurrency(); + if (tradeCurrency == null) { + throw new ValidationException("CryptoCurrency account must have exactly one trade currency"); + } + altCoinAddressValidator.setCurrencyCode(tradeCurrency.getCode()); + InputValidator.ValidationResult validationResult = altCoinAddressValidator.validate(cryptoCurrencyAccount.getAddress()); + if (!validationResult.isValid) { + throw new ValidationException(validationResult.errorMessage); + } + } +// TODO we should validate payment account here as well + user.addPaymentAccount(paymentAccount); + TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); + List tradeCurrencies = paymentAccount.getTradeCurrencies(); + if (singleTradeCurrency != null) { + if (singleTradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); + } else if (!tradeCurrencies.isEmpty()) { + tradeCurrencies.forEach(tradeCurrency -> { + if (tradeCurrency instanceof FiatCurrency) + preferences.addFiatCurrency((FiatCurrency) tradeCurrency); + else + preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); + }); + } + if (!(paymentAccount instanceof CryptoCurrencyAccount)) { + if (singleTradeCurrency == null && !tradeCurrencies.isEmpty()) { + if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency())) + paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); + else + paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0)); + + } + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + } + return paymentAccount; + } +} diff --git a/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java b/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java index c6028c52e64..fe9a614fea3 100644 --- a/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java +++ b/core/src/main/java/bisq/core/payment/validation/AltCoinAddressValidator.java @@ -22,6 +22,10 @@ import bisq.core.locale.Res; import bisq.core.util.validation.InputValidator; +import bisq.asset.AddressValidationResult; +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; + import bisq.common.app.DevEnv; import com.google.inject.Inject; @@ -30,14 +34,8 @@ import lombok.extern.slf4j.Slf4j; - - -import bisq.asset.AddressValidationResult; -import bisq.asset.Asset; -import bisq.asset.AssetRegistry; - @Slf4j -public final class AltCoinAddressValidator extends InputValidator { +public class AltCoinAddressValidator extends InputValidator { private final AssetRegistry assetRegistry; private String currencyCode; diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index d8023c56098..e9a58007225 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -74,7 +74,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public final class Preferences implements PersistedDataHost, BridgeAddressProvider { +public class Preferences implements PersistedDataHost, BridgeAddressProvider { private static final ArrayList BTC_MAIN_NET_EXPLORERS = new ArrayList<>(Arrays.asList( new BlockChainExplorer("Blockstream.info", "https://blockstream.info/tx/", "https://blockstream.info/address/"), diff --git a/core/src/test/java/bisq/core/payment/PaymentAccountManagerTest.java b/core/src/test/java/bisq/core/payment/PaymentAccountManagerTest.java new file mode 100644 index 00000000000..39d38d31fd5 --- /dev/null +++ b/core/src/test/java/bisq/core/payment/PaymentAccountManagerTest.java @@ -0,0 +1,247 @@ +package bisq.core.payment; + +import bisq.core.exceptions.ValidationException; +import bisq.core.locale.CryptoCurrency; +import bisq.core.locale.FiatCurrency; +import bisq.core.locale.GlobalSettings; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.validation.AltCoinAddressValidator; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.validation.InputValidator; + +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PaymentAccountManagerTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private AccountAgeWitnessService accountAgeWitnessService; + private AltCoinAddressValidator altCoinAddressValidator; + private PaymentAccountManager paymentAccountManager; + private Preferences preferences; + private User user; + private TradeCurrency globalDefaultTradeCurrency; + private FiatCurrency defaultTradeCurrency; + + @Before + public void setUp() { + accountAgeWitnessService = mock(AccountAgeWitnessService.class); + altCoinAddressValidator = mock(AltCoinAddressValidator.class); + preferences = mock(Preferences.class); + user = mock(User.class); + paymentAccountManager = new PaymentAccountManager(accountAgeWitnessService, altCoinAddressValidator, preferences, user); + globalDefaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); + defaultTradeCurrency = new FiatCurrency("CAD"); + GlobalSettings.setDefaultTradeCurrency(defaultTradeCurrency); + } + + @After + public void tearDown() { + GlobalSettings.setDefaultTradeCurrency(globalDefaultTradeCurrency); + } + + @Test + public void addPaymentAccount_cryptoAccountMissingSingleTradeCurrency_throwsValidationException() { + // Given + CryptoCurrencyAccount account = new CryptoCurrencyAccount(); + expectedException.expect(ValidationException.class); + expectedException.expectMessage("CryptoCurrency account must have exactly one trade currency"); + + // When + paymentAccountManager.addPaymentAccount(account); + } + + @Test + public void addPaymentAccount_invalidAddressForCryptoAccount_throwsValidationException() { + // Given + CryptoCurrencyAccount account = new CryptoCurrencyAccount(); + account.init(); + account.setSingleTradeCurrency(new CryptoCurrency("ABCDEF", "Abc")); + String validationErrorMessage = UUID.randomUUID().toString(); + String address = UUID.randomUUID().toString(); + account.setAddress(address); + InputValidator.ValidationResult validationResult = new InputValidator.ValidationResult(false, validationErrorMessage); + when(altCoinAddressValidator.validate(address)).thenReturn(validationResult); + expectedException.expect(ValidationException.class); + expectedException.expectMessage(validationErrorMessage); + + // When + paymentAccountManager.addPaymentAccount(account); + } + + @Ignore("Not implemented yet") + @Test + public void addPaymentAccount_validationFailsForFiatAccount_throwsValidationException() { + // Given + + // When + + // Then + fail("Not implemented yet"); + } + + @Test + public void addPaymentAccount_validationSuccess_callsAddPaymentAccountOnTheUser() { + // Given + AliPayAccount account = new AliPayAccount(); + account.init(); + String address = UUID.randomUUID().toString(); + account.setAccountNr(address); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + verify(user).addPaymentAccount(account); + } + + @Test + public void addPaymentAccount_validationSuccess_returnsSamePaymentAccount() { + // Given + CashDepositAccount account = new CashDepositAccount(); + account.init(); + String requirements = UUID.randomUUID().toString(); + account.setRequirements(requirements); + + // When + PaymentAccount result = paymentAccountManager.addPaymentAccount(account); + + // Then + assertSame(account, result); + } + + @Test + public void addPaymentAccount_singleTradeCurrencyIsFiat_addsCurrencyToPreferences() { + // Given + CashDepositAccount account = new CashDepositAccount(); + account.init(); + FiatCurrency tradeCurrency = new FiatCurrency("USD"); + account.setSingleTradeCurrency(tradeCurrency); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + verify(preferences).addFiatCurrency(tradeCurrency); + } + + @Test + public void addPaymentAccount_singleTradeCurrencyIsCrypto_addsCurrencyToPreferences() { + // Given + CashDepositAccount account = new CashDepositAccount(); + account.init(); + CryptoCurrency tradeCurrency = new CryptoCurrency("XBT", "Bitcoin"); + account.setSingleTradeCurrency(tradeCurrency); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + verify(preferences).addCryptoCurrency(tradeCurrency); + } + + @Test + public void addPaymentAccount_oneFiatAndOneCryptoTradeCurrency_addsCurrenciesToPreferences() { + // Given + CashDepositAccount account = new CashDepositAccount(); + account.init(); + FiatCurrency fiatCurrency = new FiatCurrency("GBP"); + CryptoCurrency cryptoCurrency = new CryptoCurrency("XMR", "Monero"); + account.getTradeCurrencies().add(fiatCurrency); + account.getTradeCurrencies().add(cryptoCurrency); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + verify(preferences).addCryptoCurrency(cryptoCurrency); + verify(preferences).addFiatCurrency(fiatCurrency); + } + + @Test + public void addPaymentAccount_cryptoAccount_doesNotPublishAccountAgeWitness() { + // Given + when(altCoinAddressValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(true)); + CryptoCurrencyAccount account = new CryptoCurrencyAccount(); + account.init(); + account.setSingleTradeCurrency(new CryptoCurrency("XMR", "Monero")); + account.setAddress(UUID.randomUUID().toString()); + PaymentAccountPayload paymentAccountPayload = account.getPaymentAccountPayload(); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + assertNotNull(paymentAccountPayload); + verify(accountAgeWitnessService, never()).publishMyAccountAgeWitness(paymentAccountPayload); + } + + @Test + public void addPaymentAccount_fiatAccount_publishesAccountAgeWitness() { + // Given + RevolutAccount account = new RevolutAccount(); + account.init(); + PaymentAccountPayload paymentAccountPayload = account.getPaymentAccountPayload(); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + assertNotNull(paymentAccountPayload); + verify(accountAgeWitnessService).publishMyAccountAgeWitness(paymentAccountPayload); + } + + @Test + public void addPaymentAccount_fiatAccountHasDefaultTradeCurrencyAsSecondCurrency_setDefaultTradeCurrencyAsSelectedTradeCurrency() { + // Given + RevolutAccount account = new RevolutAccount(); + account.init(); + account.getTradeCurrencies().clear(); + FiatCurrency aud = new FiatCurrency("AUD"); + account.getTradeCurrencies().add(aud); + account.getTradeCurrencies().add(defaultTradeCurrency); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + assertNotEquals(defaultTradeCurrency, aud); + assertEquals(defaultTradeCurrency, account.getSelectedTradeCurrency()); + } + + @Test + public void addPaymentAccount_multipleTradeCurrencies_setFirstOneAsSelectedTradeCurrency() { + // Given + RevolutAccount account = new RevolutAccount(); + account.init(); + account.getTradeCurrencies().clear(); + FiatCurrency gbp = new FiatCurrency("GBP"); + FiatCurrency aud = new FiatCurrency("AUD"); + account.getTradeCurrencies().add(gbp); + account.getTradeCurrencies().add(aud); + + // When + paymentAccountManager.addPaymentAccount(account); + + // Then + assertNotEquals(defaultTradeCurrency, aud); + assertNotEquals(defaultTradeCurrency, gbp); + assertEquals(gbp, account.getSelectedTradeCurrency()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java index 11fbec3a87b..f361e0f42da 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java @@ -20,13 +20,11 @@ import bisq.desktop.common.model.ActivatableDataModel; import bisq.desktop.util.GUIUtil; -import bisq.core.locale.CryptoCurrency; -import bisq.core.locale.FiatCurrency; -import bisq.core.locale.TradeCurrency; import bisq.core.offer.OpenOfferManager; -import bisq.core.payment.AccountAgeWitnessService; import bisq.core.payment.AssetAccount; +import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountManager; import bisq.core.trade.TradeManager; import bisq.core.user.Preferences; import bisq.core.user.User; @@ -42,33 +40,31 @@ import javafx.collections.SetChangeListener; import java.util.ArrayList; -import java.util.List; import java.util.stream.Collectors; class AltCoinAccountsDataModel extends ActivatableDataModel { + private final PaymentAccountManager paymentAccountManager; private final User user; private final Preferences preferences; private final OpenOfferManager openOfferManager; private final TradeManager tradeManager; - private final AccountAgeWitnessService accountAgeWitnessService; final ObservableList paymentAccounts = FXCollections.observableArrayList(); private final SetChangeListener setChangeListener; private final String accountsFileName = "AltcoinPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; @Inject - public AltCoinAccountsDataModel(User user, + public AltCoinAccountsDataModel(PaymentAccountManager paymentAccountManager, User user, Preferences preferences, OpenOfferManager openOfferManager, TradeManager tradeManager, - AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver) { + this.paymentAccountManager = paymentAccountManager; this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; - this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; setChangeListener = change -> fillAndSortPaymentAccounts(); } @@ -99,25 +95,7 @@ protected void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { - user.addPaymentAccount(paymentAccount); - TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); - List tradeCurrencies = paymentAccount.getTradeCurrencies(); - if (singleTradeCurrency != null) { - if (singleTradeCurrency instanceof FiatCurrency) - preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency); - else - preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); - } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { - tradeCurrencies.stream().forEach(tradeCurrency -> { - if (tradeCurrency instanceof FiatCurrency) - preferences.addFiatCurrency((FiatCurrency) tradeCurrency); - else - preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); - }); - } - - if (!(paymentAccount instanceof AssetAccount)) - accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + paymentAccountManager.addPaymentAccount(paymentAccount); } public boolean onDeleteAccount(PaymentAccount paymentAccount) { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java index a60cc95e076..a060a2f5ae0 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java @@ -20,14 +20,10 @@ import bisq.desktop.common.model.ActivatableDataModel; import bisq.desktop.util.GUIUtil; -import bisq.core.locale.CryptoCurrency; -import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.FiatCurrency; -import bisq.core.locale.TradeCurrency; import bisq.core.offer.OpenOfferManager; -import bisq.core.payment.AccountAgeWitnessService; import bisq.core.payment.AssetAccount; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountManager; import bisq.core.trade.TradeManager; import bisq.core.user.Preferences; import bisq.core.user.User; @@ -49,28 +45,27 @@ class FiatAccountsDataModel extends ActivatableDataModel { + private final PaymentAccountManager paymentAccountManager; private final User user; private final Preferences preferences; private final OpenOfferManager openOfferManager; private final TradeManager tradeManager; - private final AccountAgeWitnessService accountAgeWitnessService; final ObservableList paymentAccounts = FXCollections.observableArrayList(); private final SetChangeListener setChangeListener; private final String accountsFileName = "FiatPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; @Inject - public FiatAccountsDataModel(User user, + public FiatAccountsDataModel(PaymentAccountManager paymentAccountManager, User user, Preferences preferences, OpenOfferManager openOfferManager, TradeManager tradeManager, - AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver) { + this.paymentAccountManager = paymentAccountManager; this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; - this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; setChangeListener = change -> fillAndSortPaymentAccounts(); } @@ -102,29 +97,7 @@ protected void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { - user.addPaymentAccount(paymentAccount); - TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); - List tradeCurrencies = paymentAccount.getTradeCurrencies(); - if (singleTradeCurrency != null) { - if (singleTradeCurrency instanceof FiatCurrency) - preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency); - else - preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); - } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { - if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency())) - paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); - else - paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0)); - - tradeCurrencies.forEach(tradeCurrency -> { - if (tradeCurrency instanceof FiatCurrency) - preferences.addFiatCurrency((FiatCurrency) tradeCurrency); - else - preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); - }); - } - - accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + paymentAccountManager.addPaymentAccount(paymentAccount); } public boolean onDeleteAccount(PaymentAccount paymentAccount) {