diff --git a/.github/workflows/on_pull_request.yml b/.github/workflows/on_pull_request.yml new file mode 100644 index 0000000000..cef4c96b33 --- /dev/null +++ b/.github/workflows/on_pull_request.yml @@ -0,0 +1,31 @@ +name: On Pull Request Workflow + +on: + pull_request: + +jobs: + gradle_build: + uses: ./.github/workflows/sub_gradle_build.yml + + jacoco_report: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_jacoco_report.yml + + essential_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_essential_tests.yml + + extended_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_extended_tests.yml + + codeql_analysis: + uses: ./.github/workflows/sub_codeql_analysis.yml + + complete: + if: always() + needs: [ gradle_build, essential_tests, extended_tests, jacoco_report, codeql_analysis ] + runs-on: ubuntu-22.04 + steps: + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/.github/workflows/on_pull_request_comments.yml b/.github/workflows/on_pull_request_comments.yml new file mode 100644 index 0000000000..fbfa014ce4 --- /dev/null +++ b/.github/workflows/on_pull_request_comments.yml @@ -0,0 +1,24 @@ +name: Pull Request Comments + +on: + issue_comment: + types: [ created, edited ] + +jobs: + run-extended-tests: + if: github.event.issue.pull_request && contains(github.event.comment.body, '/run-extended-tests') + uses: ./.github/workflows/sub_extended_tests.yml + + run-essential-tests: + if: github.event.issue.pull_request && contains(github.event.comment.body, '/run-essential-tests') + uses: ./.github/workflows/sub_essential_tests.yml + + run-codeql-analysis: + if: github.event.issue.pull_request && contains(github.event.comment.body, '/run-codeql-analysis') + uses: ./.github/workflows/sub_codeql_analysis.yml + + run-jacoco-report: + if: github.event.issue.pull_request && contains(github.event.comment.body, '/run-jacoco-report') + uses: ./.github/workflows/sub_jacoco_report.yml + with: + forceRebuild: true diff --git a/.github/workflows/wk_push_to_develop.yml b/.github/workflows/on_push_to_develop.yml similarity index 55% rename from .github/workflows/wk_push_to_develop.yml rename to .github/workflows/on_push_to_develop.yml index e1d77fef87..1948fc8c57 100644 --- a/.github/workflows/wk_push_to_develop.yml +++ b/.github/workflows/on_push_to_develop.yml @@ -7,18 +7,31 @@ name: Push/Merge to `develop` Branch on: push: - # when commits are pushed or merged onto `develop` branch + # when commits are pushed or pull requests merged onto `develop` branch branches: [ develop ] jobs: - gradle_test_and_build: - uses: ./.github/workflows/sub_gradle_test_and_build.yml + gradle_build: + uses: ./.github/workflows/sub_gradle_build.yml + + jacoco_report: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_jacoco_report.yml + + essential_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_essential_tests.yml + + extended_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_extended_tests.yml + + codeql_analysis: + uses: ./.github/workflows/sub_codeql_analysis.yml build_and_push_docker_image: - # stellar/anchor-platform:edge-${{ steps.get_date.outputs.date }}-${{ steps.get_sha.outputs.SHA }} - # and stellar/anchor-platform:edge - name: Push to DockerHub (tag=stellar/anchor-platform:edge) - # needs: [ gradle_test_and_build ] + name: Push to DockerHub + needs: [ gradle_build, essential_tests, extended_tests, codeql_analysis ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -45,24 +58,23 @@ jobs: tags: stellar/anchor-platform:edge,stellar/anchor-platform:edge-${{ steps.get_date.outputs.DATE }}-${{ steps.get_sha.outputs.SHA }} file: Dockerfile - # TODO: enable purge-image when we got the dockerhub token - # purge-image: - # name: Purge Docker edge Images (tags=edge-*) - # runs-on: ubuntu-22.04 - # steps: - # - name: Delete image - # uses: bots-house/ghcr-delete-image-action@v1.1.0 - # with: - # owner: stellarproducteng - # name: ${{ secrets.DOCKERHUB_USERNAME }} - # token: ${{ secrets.DOCKERHUB_TOKEN }} - # tag: edge-* + # TODO: enable purge-image when we got the dockerhub token + # purge-image: + # name: Purge Docker edge Images (tags=edge-*) + # runs-on: ubuntu-22.04 + # steps: + # - name: Delete image + # uses: bots-house/ghcr-delete-image-action@v1.1.0 + # with: + # owner: stellarproducteng + # name: ${{ secrets.DOCKERHUB_USERNAME }} + # token: ${{ secrets.DOCKERHUB_TOKEN }} + # tag: edge-* complete: if: always() - # TODO: add sep_validation_suite when Jenkins develop build is complete - needs: [ gradle_test_and_build, build_and_push_docker_image ] + needs: [ essential_tests, extended_tests, build_and_push_docker_image ] runs-on: ubuntu-latest steps: - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - run: exit 1 \ No newline at end of file + run: exit 1 diff --git a/.github/workflows/wf_release_created_or_updated.yml b/.github/workflows/on_release_created_or_updated.yml similarity index 71% rename from .github/workflows/wf_release_created_or_updated.yml rename to .github/workflows/on_release_created_or_updated.yml index e34393abb2..b4c2e84b7c 100644 --- a/.github/workflows/wf_release_created_or_updated.yml +++ b/.github/workflows/on_release_created_or_updated.yml @@ -6,9 +6,28 @@ on: types: [ created, edited ] jobs: + gradle_build: + uses: ./.github/workflows/sub_gradle_build.yml + + jacoco_report: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_jacoco_report.yml + + essential_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_essential_tests.yml + + extended_tests: + needs: [ gradle_build ] + uses: ./.github/workflows/sub_extended_tests.yml + + codeql_analysis: + uses: ./.github/workflows/sub_codeql_analysis.yml + build_and_push_docker_image: + needs: [ gradle_build, essential_tests, extended_tests, codeql_analysis ] name: Push to DockerHub (tag=stellar/anchor-platform:${{ github.event.release.tag_name }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -41,9 +60,7 @@ jobs: complete: if: always() - # TODO: Enable end_to_end tests after it is fixed. - # needs: [ end_to_end_tests, build_and_push_docker_image ] - needs: [ build_and_push_docker_image ] + needs: [ essential_tests, extended_tests, build_and_push_docker_image ] runs-on: ubuntu-latest steps: - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/sub_codeql_analysis.yml b/.github/workflows/sub_codeql_analysis.yml new file mode 100644 index 0000000000..eb96381816 --- /dev/null +++ b/.github/workflows/sub_codeql_analysis.yml @@ -0,0 +1,41 @@ +name: CodeQL Analysis Workflow + +on: + # allows this workflow to be called from another workflow + workflow_dispatch: + workflow_call: + +jobs: + analyze: + name: CodeQL Analysis + runs-on: ubuntu-latest-4-cores + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java-kotlin' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Gradle properties + run: | + echo "kotlin.daemon.jvmargs=-Xmx2g" >> gradle.properties + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/sub_essential_tests.yml b/.github/workflows/sub_essential_tests.yml new file mode 100644 index 0000000000..8423ef8043 --- /dev/null +++ b/.github/workflows/sub_essential_tests.yml @@ -0,0 +1,109 @@ +name: Run Essential Tests Workflow + +on: + workflow_dispatch: + workflow_call: + +jobs: + essential_tests: + name: Run Essential Tests (Integration Tests, Faster End-2-End Tests, Unit Tests, and Stellar Validation Tools) + runs-on: ubuntu-latest-16-cores + # runs-on: ubuntu-latest + steps: + ############################################# + # Setup JDK 11 + # Download, and Extract java-stellar-anchor-sdk.tar + # Setup hostnames (/etc/hosts) + ############################################# + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Download java-stellar-anchor-sdk.tar + uses: actions/download-artifact@v3 + with: + name: java-stellar-anchor-sdk-tar + path: /home/runner/ + + - name: Extract java-stellar-anchor-sdk.tar + run: | + cd /home/runner + tar -xf /home/runner/java-stellar-anchor-sdk.tar + cd /home/runner/java-stellar-anchor-sdk + + - name: Set up hostnames (/etc/hosts) + run: | + sudo echo "127.0.0.1 db" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 zookeeper" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 sep24-reference-ui" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 reference-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 wallet-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 platform" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 custody-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts + + ############################################# + + + - name: Pull Stellar Validation Tests Docker Image + run: docker pull stellar/anchor-tests:v0.6.9 & + + - name: Run Zookeeper, Kafka, Postgres, and Sep24 UI with docker-compose + env: + TEST_PROFILE_NAME: default + run: docker-compose -f /home/runner/java-stellar-anchor-sdk/service-runner/src/main/resources/docker-compose-test.yaml up -d --build + + - name: Run sep server, platform server, observer, and reference servers for integration tests + env: + RUN_DOCKER: false + RUN_ALL_SERVERS: false + RUN_SEP_SERVER: true + RUN_PLATFORM_SERVER: true + RUN_OBSERVER: true + RUN_KOTLIN_REFERENCE_SERVER: true + RUN_EVENT_PROCESSING_SERVER: true + RUN_WALLET_SERVER: true + SEP1_TOML_VALUE: /home/runner/java-stellar-anchor-sdk/service-runner/src/main/resources/config/stellar.host.docker.internal.toml + SEP10_HOME_DOMAIN: host.docker.internal:8080 + run: | + cp /home/runner/java-stellar-anchor-sdk/service-runner/build/libs/anchor-platform-runner-*.jar /home/runner/anchor-platform-runner.jar + java -jar /home/runner/anchor-platform-runner.jar -t & + echo "PID=$!" >> $GITHUB_ENV + + - name: Wait for the sep server to start and get ready + uses: mydea/action-wait-for-api@v1 + with: + url: "http://localhost:8080/.well-known/stellar.toml" + expected-status: "200" + timeout: "300" + interval: "1" + + # + # Run Essential Tests + # + - name: Run Essential Tests + env: + RUN_DOCKER: false + ANCHOR_DOMAIN: http://host.docker.internal:8080 + run: | + cd /home/runner/java-stellar-anchor-sdk + ./gradlew essential-tests:test --stacktrace -x spotlessApply -x spotlessKotlinApply -x javadoc -x javadocJar -x sourcesJar -x distTar -x distZip -x shadowJar -x shadowDistZip -x shadowDistTar -x bootDistTar -x bootDistZip + + - name: Run Stellar validation tool + run: | + docker run --network host -v /home/runner/java-stellar-anchor-sdk/platform/src/test/resources://config stellar/anchor-tests:v0.6.9 --home-domain http://host.docker.internal:8080 --seps 1 6 10 12 24 31 38 --sep-config //config/stellar-anchor-tests-sep-config.json --verbose + + - name: Upload Essential Tests Report + if: always() + uses: actions/upload-artifact@v3 + with: + name: essential-tests-report + path: | + /home/runner/java-stellar-anchor-sdk/api-schema/build/reports/ + /home/runner/java-stellar-anchor-sdk/core/build/reports/ + /home/runner/java-stellar-anchor-sdk/platform/build/reports/ + /home/runner/java-stellar-anchor-sdk/essential-tests/build/reports/ + diff --git a/.github/workflows/sub_extended_tests.yml b/.github/workflows/sub_extended_tests.yml new file mode 100644 index 0000000000..5717c27e96 --- /dev/null +++ b/.github/workflows/sub_extended_tests.yml @@ -0,0 +1,65 @@ +name: Run Extended Tests Workflow + +on: + workflow_dispatch: + workflow_call: + +jobs: + extended_tests: + name: Run Extended Tests + runs-on: ubuntu-latest-8-cores + steps: + ############################################# + # Setup JDK 11 + # Download, and Extract java-stellar-anchor-sdk.tar + # Setup hostnames (/etc/hosts) + ############################################# + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Download java-stellar-anchor-sdk.tar + uses: actions/download-artifact@v3 + with: + name: java-stellar-anchor-sdk-tar + path: /home/runner/ + + - name: Extract java-stellar-anchor-sdk.tar + run: | + cd /home/runner + tar -xf /home/runner/java-stellar-anchor-sdk.tar + cd /home/runner/java-stellar-anchor-sdk + + - name: Set up hostnames (/etc/hosts) + run: | + sudo echo "127.0.0.1 db" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 zookeeper" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 sep24-reference-ui" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 reference-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 wallet-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 platform" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 custody-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts + + ############################################# + + - name: Run Extended Tests + env: + ANCHOR_DOMAIN: http://host.docker.internal:8080 + SEP1_TOML_VALUE: /home/runner/java-stellar-anchor-sdk/service-runner/src/main/resources/config/stellar.host.docker.internal.toml + SEP10_HOME_DOMAIN: host.docker.internal:8080 + run: | + cd /home/runner/java-stellar-anchor-sdk + ./gradlew extended-tests:test --stacktrace -x spotlessApply -x spotlessKotlinApply -x javadoc -x javadocJar -x sourcesJar -x distTar -x distZip -x shadowJar -x shadowDistZip -x shadowDistTar -x bootDistTar -x bootDistZip + + - name: Upload Extended Tests Report + if: always() + uses: actions/upload-artifact@v3 + with: + name: extended-tests-report + path: | + /home/runner/java-stellar-anchor-sdk/extended-tests/build/reports/ + diff --git a/.github/workflows/sub_gradle_build.yml b/.github/workflows/sub_gradle_build.yml new file mode 100644 index 0000000000..8ad00f9777 --- /dev/null +++ b/.github/workflows/sub_gradle_build.yml @@ -0,0 +1,43 @@ +name: Build Anchor Platform Runnable Jar + +on: + workflow_dispatch: + workflow_call: + +jobs: + gradle_build: + name: Gradle Build and Unit Tests + runs-on: ubuntu-latest-16-cores + permissions: + contents: read + # write to PR permission is required for jacocoTestReport Action to update comment + pull-requests: write + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + with: + show-progress: false + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Gradle Build with unit tests only + env: + run_docker: false + run: ./gradlew build jacocoTestReport -x essential-tests:test -x extended-tests:test --no-daemon --stacktrace -x spotlessApply -x spotlessKotlinApply -x javadoc -x javadocJar -x sourcesJar -x distTar -x distZip -x shadowJar -x shadowDistZip -x shadowDistTar -x bootDistTar -x bootDistZip + + - name: Archive Project Folder + run: | + cd /home/runner/work/java-stellar-anchor-sdk + tar -cf /home/runner/java-stellar-anchor-sdk.tar ./java-stellar-anchor-sdk + + - name: Upload java-stellar-anchor-sdk.tar to GitHub Artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: java-stellar-anchor-sdk-tar + path: | + /home/runner/java-stellar-anchor-sdk.tar diff --git a/.github/workflows/sub_gradle_test_and_build.yml b/.github/workflows/sub_gradle_test_and_build.yml deleted file mode 100644 index f847bb0bcc..0000000000 --- a/.github/workflows/sub_gradle_test_and_build.yml +++ /dev/null @@ -1,158 +0,0 @@ -name: Gradle Build and Test - -on: - # allows this workflow to be called from another workflow - workflow_dispatch: - workflow_call: - -jobs: - gradle_test_and_build: - name: Gradle Test and Build - runs-on: ubuntu-latest-16-cores - # write to PR permission is required for jacocoTestReport Action to update comment - permissions: - contents: read - pull-requests: write - steps: - # Checkout the code - - uses: actions/checkout@v3 - - # This is to add to DNS entries to access the services started by docker-compose. - # This should be deprecated. Refer to: https://stackoverflow.com/questions/47762339/how-to-correctly-set-up-docker-network-to-use-localhost-connection/47763442#47763442 - - name: Set up hostnames (/etc/hosts) - run: | - sudo echo "127.0.0.1 db" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 zookeeper" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 sep24-reference-ui" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 reference-server" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 wallet-server" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 platform" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 custody-server" | sudo tee -a /etc/hosts - sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts - - - name: Build and run the stack with docker compose - env: - TEST_PROFILE_NAME: default - run: docker-compose -f service-runner/src/main/resources/docker-compose-test.yaml up -d --build - - # Check the docker containers - - name: Check running containers - run: docker ps - - # Prepare Stellar Validation Tests - - name: Pull Stellar Validation Tests Docker Image - run: docker pull stellar/anchor-tests:v0.6.9 & - - # Set up JDK 11 - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'adopt' - - # Gradle test is now printing the test messages to GitHub Actions log. - # It is not necessary to print the reports. - - name: Gradle test and build. (unit tests, integration tests, end-2-end tests and build) - env: - run_docker: false - run: ./gradlew clean build jacocoTestReport --no-daemon --stacktrace -x spotlessApply -x spotlessKotlinApply -x javadoc -x javadocJar -x sourcesJar - - - name: Add coverage to PR - id: jacoco - uses: madrapps/jacoco-report@v1.6.1 - with: - paths: ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml - token: ${{ secrets.GITHUB_TOKEN }} - min-coverage-overall: 40 - min-coverage-changed-files: 60 - title: Code Coverage - update-comment: true - - - name: Stop docker containers - env: - TEST_PROFILE_NAME: default - run: docker-compose -f service-runner/src/main/resources/docker-compose-test.yaml down - - - name: Start docker containers - env: - TEST_PROFILE_NAME: default - run: docker-compose -f service-runner/src/main/resources/docker-compose-test.yaml up -d - - - name: Run sep server, observer, java reference server and kotlin reference server - env: - run_docker: false - run_all_servers: false - run_sep_server: true - run_platform_server: true - run_observer: true - run_kotlin_reference_server: true - # Running wallet server is not required for sep tests. However, running wallet server - # and kotlin reference server at the same time causes `ktor` to block the JVM. - # If we need to run both `ktor` servers, we will need to run them in separate JVMs. - run_wallet_server: false - - SEP1_TOML_VALUE: service-runner/src/main/resources/config/stellar.host.docker.internal.toml - SEP10_HOME_DOMAIN: host.docker.internal:8080 - run: | - pwd - cp service-runner/build/libs/anchor-platform-runner-*.jar service-runner/build/libs/anchor-platform-runner.jar - java -jar service-runner/build/libs/anchor-platform-runner.jar -t & - - - name: Wait for sep server to start and get ready - uses: mydea/action-wait-for-api@v1 - with: - url: "http://localhost:8080/.well-known/stellar.toml" - expected-status: "200" - timeout: "300" - interval: "1" - - - name: Run Stellar validation tool - run: | - docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.9 --home-domain http://host.docker.internal:8080 --seps 1 6 10 12 24 31 38 --sep-config //config/stellar-anchor-tests-sep-config.json --verbose - - - name: Upload Artifacts - if: always() - uses: actions/upload-artifact@v3 - with: - name: gradle-artifact - path: | - /etc/hosts - /home/runner/work/java-stellar-anchor-sdk/java-stellar-anchor-sdk/api-schema/build/reports/ - /home/runner/work/java-stellar-anchor-sdk/java-stellar-anchor-sdk/core/build/reports/ - /home/runner/work/java-stellar-anchor-sdk/java-stellar-anchor-sdk/platform/build/reports/ - /home/runner/work/java-stellar-anchor-sdk/java-stellar-anchor-sdk/integration-tests/build/reports/ - - analyze: - name: CodeQL Analysis - runs-on: ubuntu-22.04 - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java', 'python' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Gradle properties - run: | - echo "kotlin.daemon.jvmargs=-Xmx2g" >> gradle.properties - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/sub_jacoco_report.yml b/.github/workflows/sub_jacoco_report.yml new file mode 100644 index 0000000000..4ba3982db2 --- /dev/null +++ b/.github/workflows/sub_jacoco_report.yml @@ -0,0 +1,70 @@ +name: Generate Jacoco Coverage Report + +on: + workflow_dispatch: + workflow_call: + inputs: + forceRebuild: + type: boolean + description: 'Force Gradle rebuild (true/false)' + required: false + default: false + +jobs: + jacoco-report: + name: Generate Jacoco Coverage Report + runs-on: ubuntu-latest + steps: + ############################################# + # Setup JDK 11 + # Download, and Extract java-stellar-anchor-sdk.tar + # Setup hostnames (/etc/hosts) + ############################################# + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Download java-stellar-anchor-sdk.tar + uses: actions/download-artifact@v3 + with: + name: java-stellar-anchor-sdk-tar + path: /home/runner/ + + - name: Extract java-stellar-anchor-sdk.tar + run: | + cd /home/runner + tar -xf /home/runner/java-stellar-anchor-sdk.tar + cd /home/runner/java-stellar-anchor-sdk + + - name: Set up hostnames (/etc/hosts) + run: | + sudo echo "127.0.0.1 db" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 zookeeper" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 sep24-reference-ui" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 reference-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 wallet-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 platform" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 custody-server" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts + + ############################################# + - name: Gradle Build with unit tests only + if : ${{ inputs.forceRebuild }} + run: | + cd /home/runner/java-stellar-anchor-sdk + ./gradlew clean build jacocoTestReport -x essential-tests:test -x extended-tests:test + + - name: Add coverage to the Pull Request + id: jacoco + uses: madrapps/jacoco-report@v1.6.1 + with: + # The path to the jacocoTestReport.xml files which were generated in the previous workflow + paths: /home/runner/java-stellar-sdk/**/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 40 + min-coverage-changed-files: 60 + title: Code Coverage + update-comment: true \ No newline at end of file diff --git a/.github/workflows/wf_pull_request.yml b/.github/workflows/wf_pull_request.yml deleted file mode 100644 index c5bd6e8700..0000000000 --- a/.github/workflows/wf_pull_request.yml +++ /dev/null @@ -1,21 +0,0 @@ -# This workflow will build a Java project with Gradle. -# This workflow is triggered: -# On all pull request events -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -name: Pull Request Workflow - -on: - pull_request: - -jobs: - gradle_test_and_build: - uses: ./.github/workflows/sub_gradle_test_and_build.yml - - complete: - if: always() - needs: [ gradle_test_and_build ] - runs-on: ubuntu-22.04 - steps: - - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - run: exit 1 diff --git a/.run/Test - End2End Test - no fullstack.run.xml b/.run/Test - End2End Test - no fullstack.run.xml index 3f8fff80d4..eae5b2b32d 100644 --- a/.run/Test - End2End Test - no fullstack.run.xml +++ b/.run/Test - End2End Test - no fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - End2End Test - with fullstack.run.xml b/.run/Test - End2End Test - with fullstack.run.xml index 853793a7fa..05d1f83a21 100644 --- a/.run/Test - End2End Test - with fullstack.run.xml +++ b/.run/Test - End2End Test - with fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - End2End with RPC Test - no fullstack.run.xml b/.run/Test - End2End with RPC Test - no fullstack.run.xml index 8cf28051f4..43c5d1ab4b 100644 --- a/.run/Test - End2End with RPC Test - no fullstack.run.xml +++ b/.run/Test - End2End with RPC Test - no fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - End2End with RPC Test - with fullstack.run.xml b/.run/Test - End2End with RPC Test - with fullstack.run.xml index 19e1f0439a..df0f286b25 100644 --- a/.run/Test - End2End with RPC Test - with fullstack.run.xml +++ b/.run/Test - End2End with RPC Test - with fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - Fireblocks End2End Test - no fullstack.run.xml b/.run/Test - Fireblocks End2End Test - no fullstack.run.xml index 760a18b58c..39dd22fc03 100644 --- a/.run/Test - Fireblocks End2End Test - no fullstack.run.xml +++ b/.run/Test - Fireblocks End2End Test - no fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - Fireblocks End2End Test - with fullstack.run.xml b/.run/Test - Fireblocks End2End Test - with fullstack.run.xml index 84c1ccd1ca..9e52d9fa91 100644 --- a/.run/Test - Fireblocks End2End Test - with fullstack.run.xml +++ b/.run/Test - Fireblocks End2End Test - with fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - Fireblocks End2End with RPC Test - no fullstack.run.xml b/.run/Test - Fireblocks End2End with RPC Test - no fullstack.run.xml deleted file mode 100644 index 8e25ebe7a9..0000000000 --- a/.run/Test - Fireblocks End2End with RPC Test - no fullstack.run.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.run/Test - Fireblocks End2End with RPC Test - with fullstack.run.xml b/.run/Test - Fireblocks End2End with RPC Test - with fullstack.run.xml deleted file mode 100644 index 0657614678..0000000000 --- a/.run/Test - Fireblocks End2End with RPC Test - with fullstack.run.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.run/Test - Integration Test - no fullstack.run.xml b/.run/Test - Integration Test - no fullstack.run.xml index 379d3a00bc..6d54c44ff6 100644 --- a/.run/Test - Integration Test - no fullstack.run.xml +++ b/.run/Test - Integration Test - no fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - Integration Test - with fullstack.run.xml b/.run/Test - Integration Test - with fullstack.run.xml index 8871698d51..b5f268afdc 100644 --- a/.run/Test - Integration Test - with fullstack.run.xml +++ b/.run/Test - Integration Test - with fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/.run/Test - Kotlin Reference Server - no fullstack.run.xml b/.run/Test - Kotlin Reference Server - no fullstack.run.xml index e790846d57..2a3da8ed50 100644 --- a/.run/Test - Kotlin Reference Server - no fullstack.run.xml +++ b/.run/Test - Kotlin Reference Server - no fullstack.run.xml @@ -1,6 +1,6 @@ - + diff --git a/Dockerfile b/Dockerfile index 055dfcd089..292a2d4865 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ COPY --chown=gradle:gradle . . RUN gradle clean bootJar --stacktrace -x test -FROM ubuntu:20.04 +FROM ubuntu:22.04 RUN apt-get update && \ apt-get install -y --no-install-recommends openjdk-11-jre diff --git a/Makefile b/Makefile deleted file mode 100644 index 29fa0f2da9..0000000000 --- a/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -# Check if we need to prepend docker commands with sudo -SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") - -# If LABEL is not provided set default value -LABEL ?= $(shell git rev-parse --short HEAD)$(and $(shell git status -s),-dirty-$(shell id -u -n)) -# If TAG is not provided set default value -TAG ?= stellar/anchor-platform:$(LABEL) -E2E_TAG ?= stellar/anchor-platform-e2e-test:$(LABEL) -# https://github.com/opencontainers/image-spec/blob/master/annotations.md -BUILD_DATE := $(shell date -u +%FT%TZ) - -docker-build: - $(SUDO) docker build -f Dockerfile --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ - -t $(TAG) . - -docker-push: - $(SUDO) docker push $(TAG) - -docker-build-e2e-test: - $(SUDO) docker build -f end-to-end-tests/Dockerfile --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ - -t $(E2E_TAG) . - -docker-push-e2e-test: - $(SUDO) docker push $(E2E_TAG) - -build-docker-compose-tests: - docker-compose -f integration-tests/docker-compose-configs/docker-compose.base.yaml build --no-cache - -run-e2e-test-all: - make run-e2e-test-default-config - make run-e2e-test-allowlist - make run-e2e-test-unique-address - make run-e2e-test-sep24-withdrawal - -define run_tests - $(SUDO) docker-compose --env-file integration-tests/docker-compose-configs/.env \ - -f integration-tests/docker-compose-configs/docker-compose.base.yaml \ - -f integration-tests/docker-compose-configs/$(1)/docker-compose-config.override.yaml rm -f - - $(SUDO) docker-compose --env-file integration-tests/docker-compose-configs/.env \ - -f integration-tests/docker-compose-configs/docker-compose.base.yaml \ - -f integration-tests/docker-compose-configs/$(1)/docker-compose-config.override.yaml \ - up --exit-code-from end-to-end-tests || (echo "E2E Test Failed: $(1)" && exit 1) -endef - -run-e2e-test-default-config: - $(call run_tests,anchor-platform-default-configs) - -run-e2e-test-allowlist: - $(call run_tests,anchor-platform-allowlist) - -run-e2e-test-unique-address: - $(call run_tests,anchor-platform-unique-address) - -run-e2e-test-sep24-withdrawal: - $(call run_tests,anchor-platform-sep24-withdrawal) diff --git a/api-schema/src/main/java/org/stellar/anchor/api/platform/PlatformTransactionData.java b/api-schema/src/main/java/org/stellar/anchor/api/platform/PlatformTransactionData.java index 831161534b..40a26b7ff3 100644 --- a/api-schema/src/main/java/org/stellar/anchor/api/platform/PlatformTransactionData.java +++ b/api-schema/src/main/java/org/stellar/anchor/api/platform/PlatformTransactionData.java @@ -89,6 +89,12 @@ public class PlatformTransactionData { @SerializedName("withdraw_anchor_account") String withdrawAnchorAccount; + @SerializedName("client_domain") + String clientDomain; + + @SerializedName("client_name") + String clientName; + Customers customers; StellarId creator; diff --git a/api-schema/src/main/java/org/stellar/anchor/api/sep/sep38/Sep38Context.java b/api-schema/src/main/java/org/stellar/anchor/api/sep/sep38/Sep38Context.java index 586dadc419..4b6e549248 100644 --- a/api-schema/src/main/java/org/stellar/anchor/api/sep/sep38/Sep38Context.java +++ b/api-schema/src/main/java/org/stellar/anchor/api/sep/sep38/Sep38Context.java @@ -6,6 +6,9 @@ public enum Sep38Context { @SerializedName("sep6") SEP6("sep6"), + @SerializedName("sep24") + SEP24("sep24"), + @SerializedName("sep31") SEP31("sep31"); diff --git a/api-schema/src/main/java/org/stellar/anchor/util/GsonUtils.java b/api-schema/src/main/java/org/stellar/anchor/util/GsonUtils.java index 0eda2e26e6..780d14e4a9 100644 --- a/api-schema/src/main/java/org/stellar/anchor/util/GsonUtils.java +++ b/api-schema/src/main/java/org/stellar/anchor/util/GsonUtils.java @@ -1,11 +1,14 @@ package org.stellar.anchor.util; import com.google.gson.*; +import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; public class GsonUtils { private static Gson instance = null; @@ -21,6 +24,10 @@ public static Gson getInstance() { if (instance == null) instance = builder().create(); return instance; } + + public static Map fromJsonToMap(String json) { + return getInstance().fromJson(json, new TypeToken>() {}.getType()); + } } class DurationConverter implements JsonSerializer, JsonDeserializer { diff --git a/build.gradle.kts b/build.gradle.kts index e6bff56ab6..1f1790827c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,29 +4,44 @@ import org.apache.tools.ant.taskdefs.condition.Os @Suppress("DSL_SCOPE_VIOLATION") plugins { java + `java-test-fixtures` alias(libs.plugins.spotless) alias(libs.plugins.kotlin.jvm) apply false jacoco } -tasks { - register("updateGitHook") { - from("scripts/pre-commit.sh") { rename { it.removeSuffix(".sh") } } - into(".git/hooks") - doLast { - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - project.exec { commandLine("chmod", "+x", ".git/hooks/pre-commit") } - } +// ******************************************************************************* +// Task registration and configuration +// ******************************************************************************* + +// The printVersionName task is used to print the version name of the project. This +// is useful for CI/CD pipelines to get the version string of the project. +tasks.register("printVersionName") { println(rootProject.version.toString()) } + +// The updateGitHook task is used to copy the pre-commit.sh file to the .git/hooks +// directory. This is part of the efforts to force the Java/Kotlin code to be formatted +// before committing the code. +tasks.register("updateGitHook") { + from("scripts/pre-commit.sh") { rename { it.removeSuffix(".sh") } } + into(".git/hooks") + doLast { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { commandLine("chmod", "+x", ".git/hooks/pre-commit") } } } - - "build" { dependsOn("updateGitHook") } } +tasks { build { dependsOn("updateGitHook") } } + +// ******************************************************************************* +// Common configurations +// ******************************************************************************* + subprojects { apply(plugin = "java") apply(plugin = "com.diffplug.spotless") apply(plugin = "jacoco") + apply(plugin = "java-test-fixtures") repositories { mavenLocal() @@ -70,7 +85,7 @@ subprojects { reports { xml.required.set(true) csv.required.set(false) - html.required.set(true) + html.required.set(false) } } } @@ -102,6 +117,9 @@ subprojects { testImplementation(rootProject.libs.bundles.junit) testImplementation(rootProject.libs.jsonassert) + testFixturesImplementation(rootProject.libs.bundles.junit) + testFixturesImplementation(rootProject.libs.jsonassert) + testAnnotationProcessor(rootProject.libs.lombok) } @@ -139,20 +157,6 @@ subprojects { } } - register("testFireblocksE2E") { - useJUnitPlatform() - - include("**/AnchorPlatformCustodyEnd2EndTest**") - include("**/AnchorPlatformCustodyApiRpcEnd2EndTest**") - - testLogging { - events("SKIPPED", "FAILED") - showExceptions = true - showStandardStreams = true - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - } - } - processResources { doFirst { val existingFile = file("$buildDir/resources/main/metadata.properties") @@ -174,7 +178,7 @@ subprojects { allprojects { group = "org.stellar.anchor-sdk" - version = "2.4.0" + version = "2.5.0" tasks.jar { manifest { @@ -185,4 +189,3 @@ allprojects { } } -tasks.register("printVersionName") { println(rootProject.version.toString()) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1b360130a9..d96085596f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -30,7 +30,6 @@ dependencies { implementation(libs.javax.transaction.api) implementation(libs.commons.beanutils) - implementation(libs.commons.text) implementation(libs.commons.io) implementation(libs.apache.commons.lang3) implementation(libs.log4j2.core) @@ -45,7 +44,7 @@ dependencies { implementation(variantOf(libs.java.stellar.sdk) { classifier("uber") }) implementation(project(":api-schema")) - implementation(project(":test-lib")) + implementation(project(":lib-util")) testImplementation(libs.okhttp3.mockserver) testImplementation(libs.servlet.api) diff --git a/core/src/main/java/org/stellar/anchor/asset/AssetService.java b/core/src/main/java/org/stellar/anchor/asset/AssetService.java index a8a0fc365a..95c0375f95 100644 --- a/core/src/main/java/org/stellar/anchor/asset/AssetService.java +++ b/core/src/main/java/org/stellar/anchor/asset/AssetService.java @@ -36,4 +36,11 @@ public interface AssetService { * @return an asset with the given SEP-38 asset identifier. */ AssetInfo getAssetByName(String asset); + + /** + * Returns all stellar assets supported by the anchor. + * + * @return a list of assets with stellar schema. + */ + List listStellarAssets(); } diff --git a/core/src/main/java/org/stellar/anchor/asset/DefaultAssetService.java b/core/src/main/java/org/stellar/anchor/asset/DefaultAssetService.java index 3939509b84..592f265de2 100644 --- a/core/src/main/java/org/stellar/anchor/asset/DefaultAssetService.java +++ b/core/src/main/java/org/stellar/anchor/asset/DefaultAssetService.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; import lombok.NoArgsConstructor; import org.apache.commons.io.FilenameUtils; import org.stellar.anchor.api.exception.InvalidConfigException; @@ -117,4 +118,11 @@ public AssetInfo getAssetByName(String name) { } return null; } + + @Override + public List listStellarAssets() { + return listAllAssets().stream() + .filter(asset -> asset.getSchema().equals(AssetInfo.Schema.stellar)) + .collect(Collectors.toList()); + } } diff --git a/core/src/main/java/org/stellar/anchor/client/ClientFinder.java b/core/src/main/java/org/stellar/anchor/client/ClientFinder.java index 8ade86f931..c1a2baf445 100644 --- a/core/src/main/java/org/stellar/anchor/client/ClientFinder.java +++ b/core/src/main/java/org/stellar/anchor/client/ClientFinder.java @@ -9,14 +9,14 @@ import org.stellar.anchor.config.ClientsConfig.ClientConfig; import org.stellar.anchor.config.Sep10Config; -/** Finds the client ID for a SEP-10 JWT. */ +/** Finds the client name for a SEP-10 JWT. */ @RequiredArgsConstructor public class ClientFinder { @NonNull private final Sep10Config sep10Config; @NonNull private final ClientsConfig clientsConfig; /** - * Returns the client ID for a SEP-10 JWT. If the client attribution is not required, the client + * Returns the client name for a SEP-10 JWT. If the client attribution is not required, the client * ID is returned if the client is found. If the client attribution is required, the client ID is * returned if the client is found and the client domain and name are allowed. * @@ -25,7 +25,7 @@ public class ClientFinder { * @throws BadRequestException if the client is not found or the client domain or name is not */ @Nullable - public String getClientId(Sep10Jwt token) throws BadRequestException { + public String getClientName(Sep10Jwt token) throws BadRequestException { ClientsConfig.ClientConfig client = getClient(token); // If client attribution is not required, return the client name diff --git a/core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java b/core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java index dff3ca5766..62366dbc14 100644 --- a/core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java +++ b/core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java @@ -7,7 +7,7 @@ import static org.stellar.anchor.api.sep.sep24.InfoResponse.FeeResponse; import static org.stellar.anchor.event.EventService.EventQueue.TRANSACTION; import static org.stellar.anchor.sep24.Sep24Helper.fromTxn; -import static org.stellar.anchor.sep24.Sep24Transaction.Kind.WITHDRAWAL; +import static org.stellar.anchor.sep24.Sep24Transaction.Kind.*; import static org.stellar.anchor.util.Log.debug; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.info; @@ -27,18 +27,9 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; +import java.util.*; import org.stellar.anchor.api.event.AnchorEvent; -import org.stellar.anchor.api.exception.AnchorException; -import org.stellar.anchor.api.exception.SepException; -import org.stellar.anchor.api.exception.SepNotAuthorizedException; -import org.stellar.anchor.api.exception.SepNotFoundException; -import org.stellar.anchor.api.exception.SepValidationException; +import org.stellar.anchor.api.exception.*; import org.stellar.anchor.api.sep.AssetInfo; import org.stellar.anchor.api.sep.sep24.GetTransactionRequest; import org.stellar.anchor.api.sep.sep24.GetTransactionsRequest; @@ -50,11 +41,14 @@ import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.auth.Sep10Jwt; +import org.stellar.anchor.client.ClientFinder; import org.stellar.anchor.config.AppConfig; import org.stellar.anchor.config.ClientsConfig; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.Sep24Config; import org.stellar.anchor.event.EventService; +import org.stellar.anchor.sep38.Sep38Quote; +import org.stellar.anchor.sep6.ExchangeAmountsCalculator; import org.stellar.anchor.util.ConfigHelper; import org.stellar.anchor.util.CustodyUtils; import org.stellar.anchor.util.MetricConstants; @@ -69,11 +63,13 @@ public class Sep24Service { final ClientsConfig clientsConfig; final AssetService assetService; final JwtService jwtService; + final ClientFinder clientFinder; final Sep24TransactionStore txnStore; final EventService.Session eventSession; final InteractiveUrlConstructor interactiveUrlConstructor; final MoreInfoUrlConstructor moreInfoUrlConstructor; final CustodyConfig custodyConfig; + final ExchangeAmountsCalculator exchangeAmountsCalculator; final Counter sep24TransactionRequestedCounter = counter(MetricConstants.SEP24_TRANSACTION_REQUESTED); @@ -99,11 +95,13 @@ public Sep24Service( ClientsConfig clientsConfig, AssetService assetService, JwtService jwtService, + ClientFinder clientFinder, Sep24TransactionStore txnStore, EventService eventService, InteractiveUrlConstructor interactiveUrlConstructor, MoreInfoUrlConstructor moreInfoUrlConstructor, - CustodyConfig custodyConfig) { + CustodyConfig custodyConfig, + ExchangeAmountsCalculator exchangeAmountsCalculator) { debug("appConfig:", appConfig); debug("sep24Config:", sep24Config); this.appConfig = appConfig; @@ -111,11 +109,13 @@ public Sep24Service( this.clientsConfig = clientsConfig; this.assetService = assetService; this.jwtService = jwtService; + this.clientFinder = clientFinder; this.txnStore = txnStore; this.eventSession = eventService.createSession(this.getClass().getName(), TRANSACTION); this.interactiveUrlConstructor = interactiveUrlConstructor; this.moreInfoUrlConstructor = moreInfoUrlConstructor; this.custodyConfig = custodyConfig; + this.exchangeAmountsCalculator = exchangeAmountsCalculator; info("Sep24Service initialized."); } @@ -212,10 +212,13 @@ public InteractiveTransactionResponse withdraw( .sep10Account(token.getAccount()) .sep10AccountMemo(token.getAccountMemo()) .fromAccount(sourceAccount) - // TODO - jamie to add unique address generator - .withdrawAnchorAccount(asset.getDistributionAccount()) .toAccount(asset.getDistributionAccount()) - .clientDomain(token.getClientDomain()); + .clientDomain(token.getClientDomain()) + .clientName(clientFinder.getClientName(token)); + + if (!isEmpty(asset.getDistributionAccount())) { + builder.withdrawAnchorAccount(asset.getDistributionAccount()); + } // TODO - jamie to look into memo vs withdrawal_memo if (memo != null) { @@ -248,6 +251,14 @@ public InteractiveTransactionResponse withdraw( builder.refundMemoType(memoTypeString(memoType(refundMemo))); } + String quoteId = withdrawRequest.get("quote_id"); + if (quoteId != null) { + System.out.println(asset.getSep38AssetName() + 1231231); + AssetInfo buyAsset = assetService.getAssetByName(withdrawRequest.get("destination_asset")); + this.validatedAndPopulateQuote( + quoteId, asset, buyAsset, strAmount, builder, WITHDRAWAL.toString(), txnId); + } + Sep24Transaction txn = builder.build(); txnStore.save(txn); @@ -338,7 +349,7 @@ public InteractiveTransactionResponse deposit(Sep10Jwt token, Map responses = transactions.stream().map(Sep6TransactionUtils::fromTxn).collect(Collectors.toList()); + sep6TransactionQueriedCounter.increment(); return new GetTransactionsResponse(responses); } @@ -433,6 +471,7 @@ public GetTransactionResponse findTransaction(Sep10Jwt token, GetTransactionRequ throw new NotFoundException("account memo does not match token"); } + sep6TransactionQueriedCounter.increment(); return new GetTransactionResponse(Sep6TransactionUtils.fromTxn(txn)); } diff --git a/core/src/main/java/org/stellar/anchor/sep6/Sep6Transaction.java b/core/src/main/java/org/stellar/anchor/sep6/Sep6Transaction.java index 0d5d97091e..dbfaa4638f 100644 --- a/core/src/main/java/org/stellar/anchor/sep6/Sep6Transaction.java +++ b/core/src/main/java/org/stellar/anchor/sep6/Sep6Transaction.java @@ -276,6 +276,24 @@ public interface Sep6Transaction extends SepTransaction { void setMemoType(String memoType); + /** + * The client domain of the wallet that initiated the transaction. + * + * @return the client domain. + */ + String getClientDomain(); + + void setClientDomain(String clientDomain); + + /** + * The name of the client that initiated the transaction. + * + * @return the client name. + */ + String getClientName(); + + void setClientName(String clientName); + /** * The ID returned from a SEP-38 quote response. IF this is set, the user must deliver the deposit * funds to the anchor before the quote expires, otherwise the anchor may not honor the quote. diff --git a/core/src/main/java/org/stellar/anchor/sep6/Sep6TransactionBuilder.java b/core/src/main/java/org/stellar/anchor/sep6/Sep6TransactionBuilder.java index 2a06129b65..e6b491f2ad 100644 --- a/core/src/main/java/org/stellar/anchor/sep6/Sep6TransactionBuilder.java +++ b/core/src/main/java/org/stellar/anchor/sep6/Sep6TransactionBuilder.java @@ -148,6 +148,16 @@ public Sep6TransactionBuilder memoType(String memoType) { return this; } + public Sep6TransactionBuilder clientDomain(String clientDomain) { + txn.setClientDomain(clientDomain); + return this; + } + + public Sep6TransactionBuilder clientName(String clientName) { + txn.setClientName(clientName); + return this; + } + public Sep6TransactionBuilder quoteId(String quoteId) { txn.setQuoteId(quoteId); return this; diff --git a/core/src/main/java/org/stellar/anchor/util/MetricConstants.java b/core/src/main/java/org/stellar/anchor/util/MetricConstants.java index e16cc38d51..567afe4e19 100644 --- a/core/src/main/java/org/stellar/anchor/util/MetricConstants.java +++ b/core/src/main/java/org/stellar/anchor/util/MetricConstants.java @@ -10,9 +10,13 @@ public class MetricConstants { public static final String SEP1_TOML_ACCESSED = "sep1.toml_accessed"; // SEP-6 metrics - public static final String SEP6_TRANSACTION = "sep6.transaction"; - public static final String TV_SEP6_DEPOSIT = "deposit"; + public static final String SEP6_TRANSACTION_REQUESTED = "sep6.transaction.requested"; + public static final String SEP6_TRANSACTION_CREATED = "sep6.transaction.created"; + public static final String SEP6_TRANSACTION_QUERIED = "sep6.transaction.queried"; public static final String TV_SEP6_WITHDRAWAL = "withdrawal"; + public static final String TV_SEP6_WITHDRAWAL_EXCHANGE = "withdrawal_exchange"; + public static final String TV_SEP6_DEPOSIT = "deposit"; + public static final String TV_SEP6_DEPOSIT_EXCHANGE = "deposit_exchange"; // SEP-10 metrics public static final String SEP10_CHALLENGE_CREATED = "sep10.transaction"; diff --git a/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java b/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java index 4ae9e63399..dd790a150c 100644 --- a/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java +++ b/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java @@ -108,6 +108,8 @@ public static GetTransactionResponse toGetTransactionResponse(Sep31Transaction t .refunds(refunds) .stellarTransactions(txn.getStellarTransactions()) .externalTransactionId(txn.getExternalTransactionId()) + .clientDomain(txn.getClientDomain()) + .clientName(txn.getClientName()) .customers(txn.getCustomers()) .creator(txn.getCreator()) .build(); @@ -145,6 +147,8 @@ public static GetTransactionResponse toGetTransactionResponse( .externalTransactionId(txn.getExternalTransactionId()) .memo(txn.getMemo()) .memoType(txn.getMemoType()) + .clientDomain(txn.getClientDomain()) + .clientName(txn.getClientName()) .refundMemo(txn.getRefundMemo()) .refundMemoType(txn.getRefundMemoType()) .requiredInfoMessage(txn.getRequiredInfoMessage()) @@ -190,8 +194,11 @@ public static GetTransactionResponse toGetTransactionResponse( .externalTransactionId(txn.getExternalTransactionId()) .memo(txn.getMemo()) .memoType(txn.getMemoType()) + .clientDomain(txn.getClientDomain()) + .clientName(txn.getClientName()) .refundMemo(txn.getRefundMemo()) .refundMemoType(txn.getRefundMemoType()) + .quoteId(txn.getQuoteId()) .build(); } diff --git a/core/src/test/java/org/stellar/anchor/sep24/PojoSep24Transaction.java b/core/src/test/java/org/stellar/anchor/sep24/PojoSep24Transaction.java index efb084c76d..c4f727790b 100644 --- a/core/src/test/java/org/stellar/anchor/sep24/PojoSep24Transaction.java +++ b/core/src/test/java/org/stellar/anchor/sep24/PojoSep24Transaction.java @@ -28,6 +28,7 @@ public class PojoSep24Transaction implements Sep24Transaction { String memo; String protocol; String clientDomain; + String clientName; Boolean claimableBalanceSupported; String amountIn; String amountOut; @@ -42,4 +43,7 @@ public class PojoSep24Transaction implements Sep24Transaction { String message; String refundMemo; String refundMemoType; + String quoteId; + String sourceAsset; + String destinationAsset; } diff --git a/core/src/test/java/org/stellar/anchor/sep31/PojoSep31Transaction.java b/core/src/test/java/org/stellar/anchor/sep31/PojoSep31Transaction.java index be3a3c43ac..9b4c625c03 100644 --- a/core/src/test/java/org/stellar/anchor/sep31/PojoSep31Transaction.java +++ b/core/src/test/java/org/stellar/anchor/sep31/PojoSep31Transaction.java @@ -30,6 +30,7 @@ public class PojoSep31Transaction implements Sep31Transaction { String requiredInfoMessage; String quoteId; String clientDomain; + String clientName; Sep31Operation.Fields requiredInfoUpdates; Map fields; Boolean refunded; diff --git a/core/src/test/java/org/stellar/anchor/sep6/PojoSep6Transaction.java b/core/src/test/java/org/stellar/anchor/sep6/PojoSep6Transaction.java index 191dd09a1c..91fbb5b468 100644 --- a/core/src/test/java/org/stellar/anchor/sep6/PojoSep6Transaction.java +++ b/core/src/test/java/org/stellar/anchor/sep6/PojoSep6Transaction.java @@ -40,6 +40,8 @@ public class PojoSep6Transaction implements Sep6Transaction { String toAccount; String memo; String memoType; + String clientDomain; + String clientName; String quoteId; String message; Refunds refunds; diff --git a/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt index a5c4ccb83a..a864a5941c 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt @@ -26,6 +26,7 @@ import org.stellar.anchor.TestConstants.Companion.TEST_TRANSACTION_ID_0 import org.stellar.anchor.TestConstants.Companion.TEST_TRANSACTION_ID_1 import org.stellar.anchor.TestHelper import org.stellar.anchor.api.callback.FeeIntegration +import org.stellar.anchor.api.exception.BadRequestException import org.stellar.anchor.api.exception.SepException import org.stellar.anchor.api.exception.SepNotAuthorizedException import org.stellar.anchor.api.exception.SepNotFoundException @@ -38,8 +39,12 @@ import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.JwtService.CLIENT_DOMAIN import org.stellar.anchor.auth.Sep10Jwt import org.stellar.anchor.auth.Sep24InteractiveUrlJwt +import org.stellar.anchor.client.ClientFinder import org.stellar.anchor.config.* import org.stellar.anchor.event.EventService +import org.stellar.anchor.sep38.PojoSep38Quote +import org.stellar.anchor.sep38.Sep38QuoteStore +import org.stellar.anchor.sep6.ExchangeAmountsCalculator import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.MemoHelper.makeMemo import org.stellar.sdk.MemoHash @@ -53,6 +58,42 @@ internal class Sep24ServiceTest { const val TEST_SEP24_MORE_INFO_URL = "https://test-anchor.stellar.org/more_info_url" val TEST_STARTED_AT: Instant = Instant.now() val TEST_COMPLETED_AT: Instant = Instant.now().plusSeconds(100) + val DEPOSIT_QUOTE_JSON = + """ + { + "id": "test-deposit-quote-id", + "expires_at": "2021-04-30T07:42:23", + "total_price": "5.42", + "price": "5.00", + "sell_asset": "iso4217:BRL", + "sell_amount": "542", + "buy_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", + "buy_amount": "100", + "fee": { + "total": "42.00", + "asset": "iso4217:BRL" + } + } + """ + .trimIndent() + val WITHDRAW_QUOTE_JSON = + """ + { + "id": "test-withdraw-quote-id", + "expires_at": "2021-04-30T07:42:23", + "total_price": "0.542", + "price": "0.5", + "sell_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", + "sell_amount": "542", + "buy_asset": "iso4217:BRL", + "buy_amount": "1000", + "fee": { + "total": "42", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + } + } + """ + .trimIndent() } @MockK(relaxed = true) lateinit var appConfig: AppConfig @@ -66,6 +107,8 @@ internal class Sep24ServiceTest { @MockK(relaxed = true) lateinit var feeIntegration: FeeIntegration + @MockK(relaxed = true) lateinit var clientFinder: ClientFinder + @MockK(relaxed = true) lateinit var txnStore: Sep24TransactionStore @MockK(relaxed = true) lateinit var interactiveUrlConstructor: InteractiveUrlConstructor @@ -78,11 +121,16 @@ internal class Sep24ServiceTest { @MockK(relaxed = true) lateinit var clientConfig: ClientsConfig.ClientConfig + @MockK(relaxed = true) lateinit var sep38QuoteStore: Sep38QuoteStore + private val assetService: AssetService = DefaultAssetService.fromJsonResource("test_assets.json") private lateinit var jwtService: JwtService private lateinit var sep24Service: Sep24Service private lateinit var testInteractiveUrlJwt: Sep24InteractiveUrlJwt + private lateinit var depositQuote: PojoSep38Quote + private lateinit var withdrawQuote: PojoSep38Quote + private lateinit var calculator: ExchangeAmountsCalculator private val gson = GsonUtils.getInstance() @@ -103,6 +151,8 @@ internal class Sep24ServiceTest { every { moreInfoUrlConstructor.construct(any()) } returns "${TEST_SEP24_MORE_INFO_URL}?lang=en&token=$strToken" every { clientsConfig.getClientConfigByDomain(any()) } returns clientConfig + every { clientFinder.getClientName(any()) } returns TEST_CLIENT_NAME + calculator = ExchangeAmountsCalculator(sep38QuoteStore) sep24Service = Sep24Service( @@ -111,12 +161,16 @@ internal class Sep24ServiceTest { clientsConfig, assetService, jwtService, + clientFinder, txnStore, eventService, interactiveUrlConstructor, moreInfoUrlConstructor, - custodyConfig + custodyConfig, + calculator ) + depositQuote = gson.fromJson(DEPOSIT_QUOTE_JSON, PojoSep38Quote::class.java) + withdrawQuote = gson.fromJson(WITHDRAW_QUOTE_JSON, PojoSep38Quote::class.java) } @Test @@ -146,6 +200,7 @@ internal class Sep24ServiceTest { ) assertEquals(TEST_ACCOUNT, slotTxn.captured.fromAccount) assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain) + assertEquals(TEST_CLIENT_NAME, slotTxn.captured.clientName) val params = URLEncodedUtils.parse(URI(response.url), Charset.forName("UTF-8")) val tokenStrings = params.filter { pair -> pair.name.equals("token") } @@ -195,6 +250,20 @@ internal class Sep24ServiceTest { assertEquals(TEST_MEMO, slotTxn.captured.sep10AccountMemo) assertEquals(TEST_ACCOUNT, slotTxn.captured.fromAccount) assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain) + assertEquals(TEST_CLIENT_NAME, slotTxn.captured.clientName) + } + + @Test + fun `test withdraw with quote_id`() { + val slotTxn = slot() + every { txnStore.save(capture(slotTxn)) } returns null + every { sep38QuoteStore.findByQuoteId(any()) } returns withdrawQuote + sep24Service.withdraw( + createTestSep10JwtWithMemo(), + createTestTransactionRequest(withdrawQuote.id) + ) + assertEquals(withdrawQuote.id, slotTxn.captured.quoteId) + assertEquals(withdrawQuote.buyAsset, slotTxn.captured.destinationAsset) } @Test @@ -245,6 +314,13 @@ internal class Sep24ServiceTest { token.sub = "G1234" sep24Service.withdraw(token, request) } + + assertThrows { + val request = createTestTransactionRequest("bad-quote-id") + val token = createTestSep10JwtToken() + every { sep38QuoteStore.findByQuoteId(any()) } returns null + sep24Service.withdraw(token, request) + } } @ParameterizedTest @@ -274,6 +350,7 @@ internal class Sep24ServiceTest { assertEquals(TEST_ASSET_ISSUER_ACCOUNT_ID, slotTxn.captured.requestAssetIssuer) assertEquals(TEST_ACCOUNT, slotTxn.captured.toAccount) assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain) + assertEquals(TEST_CLIENT_NAME, slotTxn.captured.clientName) } @Test @@ -308,6 +385,20 @@ internal class Sep24ServiceTest { assertEquals(TEST_MEMO, slotTxn.captured.sep10AccountMemo) assertEquals(TEST_ACCOUNT, slotTxn.captured.fromAccount) assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain) + assertEquals(TEST_CLIENT_NAME, slotTxn.captured.clientName) + } + + @Test + fun `test deposit with quote_id`() { + val slotTxn = slot() + every { txnStore.save(capture(slotTxn)) } returns null + every { sep38QuoteStore.findByQuoteId(any()) } returns depositQuote + sep24Service.deposit( + createTestSep10JwtWithMemo(), + createTestTransactionRequest(depositQuote.id) + ) + assertEquals(depositQuote.id, slotTxn.captured.quoteId) + assertEquals(depositQuote.sellAsset, slotTxn.captured.sourceAsset) } @Test @@ -360,6 +451,7 @@ internal class Sep24ServiceTest { assertEquals(TEST_ASSET_ISSUER_ACCOUNT_ID, slotTxn.captured.requestAssetIssuer) assertEquals(whitelistedAccount, slotTxn.captured.toAccount) assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain) + assertEquals(TEST_CLIENT_NAME, slotTxn.captured.clientName) } @Test @@ -401,6 +493,13 @@ internal class Sep24ServiceTest { token.sub = "G1234" sep24Service.deposit(token, request) } + + assertThrows { + val request = createTestTransactionRequest("bad-quote-id") + val token = createTestSep10JwtToken() + every { sep38QuoteStore.findByQuoteId(any()) } returns null + sep24Service.deposit(token, request) + } } @ParameterizedTest diff --git a/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTestHelper.kt b/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTestHelper.kt index fef1b9aa24..97ee312f13 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTestHelper.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTestHelper.kt @@ -2,17 +2,24 @@ package org.stellar.anchor.sep24 import org.stellar.anchor.TestConstants -fun createTestTransactionRequest(): MutableMap { - return mutableMapOf( - "lang" to "en", - "asset_code" to TestConstants.TEST_ASSET, - "asset_issuer" to TestConstants.TEST_ASSET_ISSUER_ACCOUNT_ID, - "account" to TestConstants.TEST_ACCOUNT, - "amount" to "123.4", - "email_address" to "jamie@stellar.org", - "first_name" to "Jamie", - "last_name" to "Li" - ) +fun createTestTransactionRequest( + quoteID: String? = null, +): MutableMap { + val request = + mutableMapOf( + "lang" to "en", + "asset_code" to TestConstants.TEST_ASSET, + "asset_issuer" to TestConstants.TEST_ASSET_ISSUER_ACCOUNT_ID, + "account" to TestConstants.TEST_ACCOUNT, + "amount" to "542", + "email_address" to "jamie@stellar.org", + "first_name" to "Jamie", + "last_name" to "Li", + ) + if (quoteID != null) { + request["quote_id"] = quoteID + } + return request } fun createTestTransaction(kind: String): Sep24Transaction { diff --git a/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt index b8656e2385..33cf9418f0 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt @@ -784,6 +784,14 @@ class Sep31ServiceTest { SepDepositInfo(tx.stellarAccountId, memo, "hash") } + // mock client config + every { sep10Config.allowedClientDomains } returns listOf("vibrant.stellar.org") + every { clientsConfig.getClientConfigBySigningKey(any()) } returns + ClientsConfig.ClientConfig().apply { + domain = "vibrant.stellar.org" + name = "vibrant" + } + // mock transaction save val slotTxn = slot() every { txnStore.save(capture(slotTxn)) } answers @@ -824,6 +832,7 @@ class Sep31ServiceTest { "updatedAt": "$txStartedAt", "quoteId": "my_quote_id", "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "fields": { "receiver_account_number": "1", "type": "1", diff --git a/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt index 47ceba852a..0ce4d2a284 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt @@ -819,7 +819,7 @@ class Sep38ServiceTest { ) } assertInstanceOf(BadRequestException::class.java, ex) - assertEquals("Unsupported context. Should be one of [sep6, sep31].", ex.message) + assertEquals("Unsupported context. Should be one of [sep6, sep24, sep31].", ex.message) // sell_amount should be within limit ex = assertThrows { diff --git a/core/src/test/kotlin/org/stellar/anchor/sep6/ExchangeAmountsCalculatorTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep6/ExchangeAmountsCalculatorTest.kt index 151b4bc285..e887d1543d 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep6/ExchangeAmountsCalculatorTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep6/ExchangeAmountsCalculatorTest.kt @@ -103,4 +103,18 @@ class ExchangeAmountsCalculatorTest { calculator.calculateFromQuote(quoteId, assetService.getAsset("USDC"), "100") } } + + @Test + fun `test validateQuoteAgainstRequestInfo with mismatched buy asset`() { + val quoteId = "id" + every { sep38QuoteStore.findByQuoteId(quoteId) } returns usdcQuote + assertThrows { + calculator.validateQuoteAgainstRequestInfo( + quoteId, + assetService.getAsset("USDC"), + assetService.getAsset("JPYC"), + "100" + ) + } + } } diff --git a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt index 4eec4f174d..dae20c04fe 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt @@ -28,6 +28,7 @@ import org.stellar.anchor.api.shared.RefundPayment import org.stellar.anchor.api.shared.Refunds import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService +import org.stellar.anchor.client.ClientFinder import org.stellar.anchor.config.Sep6Config import org.stellar.anchor.event.EventService import org.stellar.anchor.sep6.ExchangeAmountsCalculator.Amounts @@ -43,6 +44,7 @@ class Sep6ServiceTest { @MockK(relaxed = true) lateinit var sep6Config: Sep6Config @MockK(relaxed = true) lateinit var requestValidator: RequestValidator + @MockK(relaxed = true) lateinit var clientFinder: ClientFinder @MockK(relaxed = true) lateinit var txnStore: Sep6TransactionStore @MockK(relaxed = true) lateinit var exchangeAmountsCalculator: ExchangeAmountsCalculator @MockK(relaxed = true) lateinit var eventService: EventService @@ -55,6 +57,7 @@ class Sep6ServiceTest { MockKAnnotations.init(this, relaxUnitFun = true) every { sep6Config.features.isAccountCreation } returns false every { sep6Config.features.isClaimableBalances } returns false + every { clientFinder.getClientName(token) } returns "vibrant" every { txnStore.newInstance() } returns PojoSep6Transaction() every { eventService.createSession(any(), any()) } returns eventSession every { requestValidator.getDepositAsset(TEST_ASSET) } returns asset @@ -64,6 +67,7 @@ class Sep6ServiceTest { sep6Config, assetService, requestValidator, + clientFinder, txnStore, exchangeAmountsCalculator, eventService diff --git a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt index 3d02534883..20561c45f3 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt @@ -166,14 +166,12 @@ class Sep6ServiceTestData { "type": "bank_account", "requestAssetCode": "USDC", "requestAssetIssuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountOut": "100", - "amountOutAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountFee": "0", - "amountFeeAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO" + "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant" } """ .trimIndent() @@ -191,15 +189,9 @@ class Sep6ServiceTestData { "amount": "100", "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, - "amount_out": { - "amount": "100", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, - "amount_fee": { - "amount": "0", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, "destination_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -222,12 +214,11 @@ class Sep6ServiceTestData { "kind": "deposit", "requestAssetCode": "USDC", "requestAssetIssuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountOutAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountFee": "0", - "amountFeeAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO" + "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant" } """ @@ -245,11 +236,9 @@ class Sep6ServiceTestData { "amount_expected": { "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, - "amount_fee": { - "amount": "0", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, "destination_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -283,6 +272,8 @@ class Sep6ServiceTestData { "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "quoteId": "test-quote-id" } """ @@ -312,6 +303,8 @@ class Sep6ServiceTestData { }, "quote_id": "test-quote-id", "destination_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -344,7 +337,9 @@ class Sep6ServiceTestData { "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO" + "toAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant" } """ .trimIndent() @@ -370,6 +365,8 @@ class Sep6ServiceTestData { }, "amount_fee": { "amount": "0", "asset": "iso4217:USD" }, "destination_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -402,14 +399,12 @@ class Sep6ServiceTestData { "requestAssetIssuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "amountIn": "100", "amountInAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountOut": "100", - "amountOutAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountFee": "0", - "amountFeeAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "refundMemo": "some text", "refundMemoType": "text" } @@ -433,17 +428,11 @@ class Sep6ServiceTestData { "amount": "100", "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, - "amount_out": { - "amount": "100", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, - "amount_fee": { - "amount": "0", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "refund_memo": "some text", "refund_memo_type": "text", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -467,12 +456,11 @@ class Sep6ServiceTestData { "requestAssetCode": "USDC", "requestAssetIssuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "amountInAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountOutAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "amountFee": "0", - "amountFeeAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "refundMemo": "some text", "refundMemoType": "text" } @@ -491,13 +479,11 @@ class Sep6ServiceTestData { "amount_expected": { "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, - "amount_fee": { - "amount": "0", - "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - }, "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "refund_memo": "some text", "refund_memo_type": "text", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -531,6 +517,8 @@ class Sep6ServiceTestData { "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "quoteId": "test-quote-id", "refundMemo": "some text", "refundMemoType": "text" @@ -561,6 +549,8 @@ class Sep6ServiceTestData { "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "refund_memo": "some text", "refund_memo_type": "text", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", @@ -594,6 +584,8 @@ class Sep6ServiceTestData { "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "clientDomain": "vibrant.stellar.org", + "clientName": "vibrant", "refundMemo": "some text", "refundMemoType": "text" } @@ -626,6 +618,8 @@ class Sep6ServiceTestData { "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "refund_memo": "some text", "refund_memo_type": "text", + "client_domain": "vibrant.stellar.org", + "client_name": "vibrant", "customers": { "sender": { "account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", diff --git a/core/src/test/kotlin/org/stellar/anchor/util/ClientFinderTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/ClientFinderTest.kt index d97161d6f9..7760ca526b 100644 --- a/core/src/test/kotlin/org/stellar/anchor/util/ClientFinderTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/util/ClientFinderTest.kt @@ -54,86 +54,86 @@ class ClientFinderTest { } @Test - fun `test getClientId with client found by domain`() { + fun `test getClientName with client found by domain`() { every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns clientConfig - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } @Test - fun `test getClientId with client found by signing key`() { + fun `test getClientName with client found by signing key`() { every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns null - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } @Test - fun `test getClientId with client not found`() { + fun `test getClientName with client not found`() { every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns null every { clientsConfig.getClientConfigBySigningKey(token.account) } returns null - assertThrows { clientFinder.getClientId(token) } + assertThrows { clientFinder.getClientName(token) } } @Test - fun `test getClientId with client not found by domain`() { + fun `test getClientName with client not found by domain`() { every { sep10Config.allowedClientDomains } returns listOf("nothing") - assertThrows { clientFinder.getClientId(token) } + assertThrows { clientFinder.getClientName(token) } } @Test - fun `test getClientId with client not found by name`() { + fun `test getClientName with client not found by name`() { every { sep10Config.allowedClientNames } returns listOf("nothing") - assertThrows { clientFinder.getClientId(token) } + assertThrows { clientFinder.getClientName(token) } } @Test - fun `test getClientId with all domains allowed`() { + fun `test getClientName with all domains allowed`() { every { sep10Config.allowedClientDomains } returns emptyList() - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } @Test - fun `test getClientId with all names allowed`() { + fun `test getClientName with all names allowed`() { every { sep10Config.allowedClientNames } returns emptyList() - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } @Test - fun `test getClientId with client attribution disabled and missing client`() { + fun `test getClientName with client attribution disabled and missing client`() { every { sep10Config.isClientAttributionRequired } returns false every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns null every { clientsConfig.getClientConfigBySigningKey(token.account) } returns null - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertNull(clientId) } @Test - fun `test getClientId with client attribution disabled and client found by signing key`() { + fun `test getClientName with client attribution disabled and client found by signing key`() { every { sep10Config.isClientAttributionRequired } returns false every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns null every { clientsConfig.getClientConfigBySigningKey(token.account) } returns clientConfig - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } @Test - fun `test getClientId with client attribution disabled and client found by domain`() { + fun `test getClientName with client attribution disabled and client found by domain`() { every { sep10Config.isClientAttributionRequired } returns false every { clientsConfig.getClientConfigByDomain(token.clientDomain) } returns clientConfig every { clientsConfig.getClientConfigBySigningKey(token.account) } returns null - val clientId = clientFinder.getClientId(token) + val clientId = clientFinder.getClientName(token) assertEquals(clientConfig.name, clientId) } } diff --git a/docs/01 - Contributing/A - Development Environment.md b/docs/01 - Contributing/A - Development Environment.md index 90763760bb..58013d1754 100644 --- a/docs/01 - Contributing/A - Development Environment.md +++ b/docs/01 - Contributing/A - Development Environment.md @@ -103,6 +103,12 @@ Run all tests: `./gradlew test` Run subproject tests: `./gradlew :[subproject]:test` +### Running `docker-compose` up for development +`./gradlew dockerComposeUp` + +### Starting all servers +`./gradlew startAllServers` + ## Set up the Git Hooks In order to have consistent code style, we use [Google Java Format](https://github.com/google/google-java-format) to diff --git a/docs/01 - Contributing/B - Git Guidelines.md b/docs/01 - Contributing/B - Git Guidelines.md index a98bae84ec..d648554856 100644 --- a/docs/01 - Contributing/B - Git Guidelines.md +++ b/docs/01 - Contributing/B - Git Guidelines.md @@ -53,7 +53,7 @@ The following artifacts are available for each workflow run: - `/java-stellar-anchor-sdk/api-schema/build/libs`: The build artifact of the `api-schema` subproject. - `java-stellar-anchor-sdk/core/build/reports`: The test reports of the `core` subproject. - `java-stellar-anchor-sdk/platform/build/reports`: The test reports of the `platform` subproject. -- `java-stellar-anchor-sdk/integration-tests/build/reports`: The test reports of the `integration-tests` subproject. +- `java-stellar-anchor-sdk/essential-tests/build/reports`: The test reports of the `essential-tests` subproject. To access the artifacts, follow these steps: 1. Go to the `Actions` tab of the repository. diff --git a/docs/01 - Contributing/E - Github Workflows.md b/docs/01 - Contributing/E - Github Workflows.md new file mode 100644 index 0000000000..4d8e183e82 --- /dev/null +++ b/docs/01 - Contributing/E - Github Workflows.md @@ -0,0 +1,33 @@ +# Github Workflows + +## Workflows Triggered by Github Events + +The following workflows are triggered according to the Github events: + +- `on_pull_request.yml`: triggered when a pull request is created or updated +- `on_pull_request_comments.yml`: triggered callable workflows when a pull request is commented +- `on_push_to_develop.yml`: triggered when a pull request is merged or a commit is pushed. +- `on_release_created_or_updated.yml`: triggered when a release is created or updated. + +## Callable Workflows + +Here are the callable workflows: + +- `sub_gradle_build.yml`: Run Gradle build and unit tests. +- `sub_essential_tests.yml`: Run essential tests. +- `sub_extended_tests.yml`: Run extended tests. +- `sub_codeql_analysis.yml`: Run the CodeQL. +- `sub_jacoco_report.yml`: Generate the Jacoco reports. + +## How to run the workflows from the pull request comments + +The following callable workflows can be called by typing the following commands in the pull request comments: + +- `/run-extended-tests`: Run the extended tests +- `/run-essential-tests`: Run the essential tests +- `/run-codeql-analysis`: Run the CodeQL analysis +- `/run-jacoco-report`: Generate the Jacoco reports + +Please note that when triggered from comments, these callable workflow are running from the `develop` branch instead of +the pull request branch. + diff --git a/docs/01 - Contributing/E - Publishing the SDK.md b/docs/01 - Contributing/F - Publishing the SDK.md similarity index 100% rename from docs/01 - Contributing/E - Publishing the SDK.md rename to docs/01 - Contributing/F - Publishing the SDK.md diff --git a/docs/01 - Contributing/F - Testing with AWS Services.md b/docs/01 - Contributing/G - Testing with AWS Services.md similarity index 100% rename from docs/01 - Contributing/F - Testing with AWS Services.md rename to docs/01 - Contributing/G - Testing with AWS Services.md diff --git a/docs/01 - Contributing/README.md b/docs/01 - Contributing/README.md index b83e0176bf..ba6b4eb761 100644 --- a/docs/01 - Contributing/README.md +++ b/docs/01 - Contributing/README.md @@ -1,13 +1,15 @@ # How to contribute + * [How to contribute](#how-to-contribute) - * [Set up the Development Environment](#set-up-the-development-environment) - * [Git Guidelines](#git-guidelines) - * [Logging Guidelines](#logging-guidelines) - * [Database Migration](#database-migration) - * [Publishing the SDK library to Maven Central](#publishing-the-sdk-library-to-maven-central) - * [Testing with AWS Services](#testing-with-aws-services) + * [Set up the Development Environment](#set-up-the-development-environment) + * [Git Guidelines](#git-guidelines) + * [Logging Guidelines](#logging-guidelines) + * [Database Migration](#database-migration) + * [Publishing the SDK library to Maven Central](#publishing-the-sdk-library-to-maven-central) + * [Testing with AWS Services](#testing-with-aws-services) + 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 @@ -36,12 +38,16 @@ Please follow the [Logging Guidelines](C%20-%20Logging%20Guidelines.md). Please refer to [Database Migration](D%20-%20Database%20Migration.md) for information on this topic. +## Github Workflows + +Please refer to [Github Workflows](E%20-%20Github%20Workflows.md) for information on how the Github workflows are setup. + ## Publishing the SDK library to Maven Central -Please refer to [Publishing the SDK](E%20-%20Publishing%20the%20SDK.md) for information on +Please refer to [Publishing the SDK](F%20-%20Publishing%20the%20SDK.md) for information on this topic. ## Testing with AWS Services -Please refer to [Testing with AWS Services](F%20-%20Testing%20with%20AWS%20Services.md) for +Please refer to [Testing with AWS Services](G%20-%20Testing%20with%20AWS%20Services.md) for information on this topic. diff --git a/docs/README.md b/docs/README.md index 24e139c2a6..9d4a238d73 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ [![License](https://badgen.net/badge/license/Apache%202/blue?icon=github&label=License)](https://github.com/stellar/java-stellar-anchor-sdk/blob/develop/LICENSE) [![GitHub Version](https://badgen.net/github/release/stellar/java-stellar-anchor-sdk?icon=github&label=Latest%20release)](https://github.com/stellar/java-stellar-anchor-sdk/releases) -[![Docker](https://badgen.net/badge/Latest%20Release/v2.4.0/blue?icon=docker)](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=2.4.0) +[![Docker](https://badgen.net/badge/Latest%20Release/v2.5.0/blue?icon=docker)](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=2.5.0) ![Develop Branch](https://github.com/stellar/java-stellar-anchor-sdk/actions/workflows/wk_push_to_develop.yml/badge.svg?branch=develop)
@@ -45,7 +45,8 @@ contribute to this project. - __wallet_reference_server__: Contains the wallet's reference server implementation in Kotlin. - __service_runner__: Contains the service runner implementation that runs services, such as SEP, platform, payment observer, and reference servers, etc. It also contains the main entry point of the Anchor Platform. -- __integration-tests__: Contains the integration tests and end-2-end tests for the Anchor Platform. +- __essential-tests__: Contains the essential integration tests and end-2-end tests for the Anchor Platform. +- __extended-tests__: Contains the extended integration tests and end-2-end tests for the Anchor Platform. ## References [SEP-1](https://stellar.org/protocol/sep-6): Stellar Info File diff --git a/docs/resources/deployment-examples/example-fargate/Dockerfile b/docs/resources/deployment-examples/example-fargate/Dockerfile index 898275c280..eb73d28fb6 100644 --- a/docs/resources/deployment-examples/example-fargate/Dockerfile +++ b/docs/resources/deployment-examples/example-fargate/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 RUN mkdir /config_files ENV STELLAR_ANCHOR_CONFIG=file:/config/anchor-config.yaml ENV REFERENCE_SERVER_CONFIG_ENV=file:/config/reference-config.yaml diff --git a/end-to-end-tests/Dockerfile b/end-to-end-tests/Dockerfile deleted file mode 100644 index d1a8b3dbaf..0000000000 --- a/end-to-end-tests/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM python:3.10-alpine - -WORKDIR /app - -COPY end-to-end-tests/requirements.txt requirements.txt -RUN pip3 install -r requirements.txt - -COPY end-to-end-tests/. . - -ENTRYPOINT [ "python3", "end_to_end_tests.py", "-d", "host.docker.internal:8080"] \ No newline at end of file diff --git a/end-to-end-tests/README.md b/end-to-end-tests/README.md deleted file mode 100644 index e76e68d2dd..0000000000 --- a/end-to-end-tests/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Anchor Platform End-to-End Tests - -end_to_end_tests.py is a Python CLI tool used to test end-to-end Anchor Platform + Anchor Server data flows - -## Available Tests - -### sep31_flow -1) (End-to-End Test) Get the SEP endpoints/keys from the Platform Server TOML file (*/.well-known/stellar.toml*) -2) (End-to-End Test) Get a token from the *GET /* endpoint to authenticate all further requests -3) (End-to-End Test) Make a *PUT //customer* request to create a customer -4) (End-to-End Test) Create a transaction using the *POST /transactions* endpoint -5) (End-to-End Test) Send the Asset on the Stellar Network to the *stellar_account_id* address that was returned in step 4 -6) The Platform Server's Payment Observer detects the payment on the Stellar Network and publishes a message to the event queue -7) The Anchor Reference Server detects the published event, "processes" the event and makes a PATCH request back to the Platform Server to update the transaction's status as "complete" -8) (End-to-End Test) Polls the Platform Server for the transaction to be marked as "complete" - -### sep31_flow_with_sep38 -Same as **sep31_flow** but a SEP38 quote is requested before step 4 and the "quote_id" is passed into the transaction creation step. - -### sep38_create_quote -Request a SEP38 quote from the Anchor Platform - -### omnibus_allowlist -Test the omnibus allowlist feature in SEP-10. If the supplied account is not in the `omnibusAllowList`, the request -should be rejected with a 403 error. - -### all -Run all the available tests - -## How To Run -```shell -python anchor_platform_functional_test.py --domain --tests --secret -``` -## Examples: -```shell -python anchor_platform_functional_test.py --domain localhost:8080 --tests sep31_flow_with_sep38 --secret -``` -```shell -python anchor_platform_functional_test.py --domain anchor-sep-server-dev.stellar.org --tests sep31_flow --secret -``` - - -Note: The Stellar account derived from should have own assets used in the tests (USDC, JPYC) \ No newline at end of file diff --git a/end-to-end-tests/end_to_end_tests.py b/end-to-end-tests/end_to_end_tests.py deleted file mode 100644 index 3dc3ec5ac9..0000000000 --- a/end-to-end-tests/end_to_end_tests.py +++ /dev/null @@ -1,433 +0,0 @@ -import argparse -import os -import requests -import json -import base64 -import time -from datetime import datetime, timedelta -from pprint import pprint - -from stellar_sdk import ( - Asset, - Keypair, - Network, - TransactionEnvelope, - TransactionBuilder, - Server, -) -from stellar_sdk.sep.federation import fetch_stellar_toml - -STELLAR_TESTNET_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" -HORIZON_URI = "https://horizon-testnet.stellar.org" -FRIENDBOT_URI = "https://friendbot.stellar.org" - -TRANSACTION_STATUS_COMPLETE = "completed" -TRANSACTION_PENDING_USER_TRANSFER_START = "pending_user_transfer_start" - -class Endpoints: - def __init__(self, domain, use_http=True): - toml = fetch_stellar_toml(domain, use_http=use_http) - self.ANCHOR_PLATFORM_AUTH_ENDPOINT = toml.get("WEB_AUTH_ENDPOINT") - self.ANCHOR_PLATFORM_SEP12_CUSTOMER_ENDPOINT = toml.get("KYC_SERVER") + "/customer" - self.ANCHOR_PLATFORM_SEP38_QUOTE_ENDPOINT = toml.get("ANCHOR_QUOTE_SERVER") + "/quote" - self.ANCHOR_PLATFORM_SEP31_TRANSACTIONS_ENDPOINT = toml.get("DIRECT_PAYMENT_SERVER") + "/transactions" - self.ANCHOR_PLATFORM_SEP24_TRANSACTIONS_ENDPOINT = toml.get("TRANSFER_SERVER_SEP0024") - - -def get_anchor_platform_token(endpoints, public_key, secret_key): - print("============= Getting Token from Anchor Platform ========================") - response = requests.get(endpoints.ANCHOR_PLATFORM_AUTH_ENDPOINT, {"account": public_key}) - content = response.json() - - envelope_xdr = content["transaction"] - envelope = TransactionEnvelope.from_xdr( - envelope_xdr, network_passphrase=STELLAR_TESTNET_NETWORK_PASSPHRASE - ) - envelope.sign(secret_key) - response = requests.post( - endpoints.ANCHOR_PLATFORM_AUTH_ENDPOINT, - data={"transaction": envelope.to_xdr()}, - ) - print(response) - content = json.loads(response.content) - return content["token"] - - -def create_anchor_test_quote(endpoints, headers, payload): - print("===================== Creating SEP-38 Quote ====================================") - print("Request Payload:") - pprint(payload) - expire = (datetime.utcnow() + timedelta(days=2)).strftime('%Y-%m-%dT%H:%M:%S.%fZ') - payload["expire_after"] = expire - create_quote = requests.post(endpoints.ANCHOR_PLATFORM_SEP38_QUOTE_ENDPOINT, data=json.dumps(payload), - headers=headers) - print("Quote:") - quote = json.loads(create_quote.content) - pprint(quote) - return quote - - -def get_transaction(endpoints, headers, transaction_id, transaction_type): - transaction = None - if transaction_type == "sep31": - transaction = requests.get(endpoints.ANCHOR_PLATFORM_SEP31_TRANSACTIONS_ENDPOINT + "/" + transaction_id, - headers=headers) - elif transaction_type == "sep24": - params = { - "id": transaction_id - } - transaction = requests.get(endpoints.ANCHOR_PLATFORM_SEP24_TRANSACTIONS_ENDPOINT + "/transaction", - params=params, headers=headers) - else: - print("error") - - res = json.loads(transaction.content) - return res - - -SENDING_CLIENT_PAYLOAD = { - "first_name": "Allie", - "last_name": "Grater", - "email_address": "allie@email.com" -} - -RECEIVING_CLIENT_PAYLOAD = { - "first_name": "John", - "last_name": "Doe", - "address": "123 Washington Street", - "city": "San Francisco", - "state_or_province": "CA", - "address_country_code": "US", - "clabe_number": "1234", - "bank_number": "abcd", - "bank_account_number": "1234", - "bank_account_type": "checking" -} - - -def create_anchor_test_customer(endpoints, headers, payload): - print("============= Creating Customer in Anchor Platform ======================") - create_customer = requests.put(endpoints.ANCHOR_PLATFORM_SEP12_CUSTOMER_ENDPOINT, - data=json.dumps(payload), headers=headers) - res = json.loads(create_customer.content) - return res["id"] - - -def create_anchor_test_transaction(endpoints, headers, payload, quote_id=None): - print("============= Creating Transaction in Anchor Platform ===================") - - if quote_id: - payload["quote_id"] = quote_id - create_transaction = requests.post(endpoints.ANCHOR_PLATFORM_SEP31_TRANSACTIONS_ENDPOINT, - data=json.dumps(payload), headers=headers) - res = json.loads(create_transaction.content) - pprint(res) - return res - - -def send_asset(asset, amount, source_secret_key, receiver_public_key, memo_hash): - print("============= Send Asset on Stellar Network =============================") - - source_keypair = Keypair.from_secret(source_secret_key) - source_public_key = source_keypair.public_key - server = Server(horizon_url="https://horizon-testnet.stellar.org") - - source_account = server.load_account(source_public_key) - - base_fee = 100 - - transaction = ( - TransactionBuilder( - source_account=source_account, - network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, - base_fee=base_fee, - ) - .add_hash_memo(memo_hash) - .append_payment_op(receiver_public_key, asset, str(amount)) - .set_timeout(30) - .build() - ) - - transaction.sign(source_keypair) - - print(transaction.to_xdr()) - - response = server.submit_transaction(transaction) - - -def poll_transaction_status(endpoints, headers, transaction_id, transaction_type, status=TRANSACTION_STATUS_COMPLETE, - timeout=120, poll_interval=2): - """ - :param transaction_type: type of transaction (SEP24 or SEP31) - :param endpoints: Anchor Platform endpoints - :param headers: Request headers - :param transaction_id: Transaction ID to poll for completion - :param status: The desired status to poll for - :param timeout: Time (in seconds) to poll for the desired status - :param poll_interval: Time (in seconds) between each poll attempt - :return: - """ - attempt = 1 - print(f"============= Polling Transaction For '{status}' Status From Anchor Platform ===========") - while True and attempt * poll_interval <= timeout: - transaction_status = get_transaction(endpoints, headers, transaction_id, - transaction_type)["transaction"]["status"] - print(f"attempt #{attempt} - transaction - {transaction_id} status is {transaction_status}") - if transaction_status == status: - break - attempt += 1 - time.sleep(poll_interval) - else: - raise Exception("error: timed out while polling transaction status") - print("=========================================================================") - - -def test_sep_31_flow(endpoints, keypair, transaction_payload, sep38_payload=None): - """ - :param endpoints: Anchor Platform endpoints - :param keypair: Stellar Account keypair - :param transaction_payload: Payload to create a transaction in the Anchor Platform - :param sep38_payload: Payload to request a quote from the Anchor Platform and use that quote in the sep31 request - :return: - """ - public_key = keypair.public_key - secret_key = keypair.secret - - token = get_anchor_platform_token(endpoints, public_key, secret_key) - - headers = {"Authorization": f"Bearer {token}", 'Content-Type': 'application/json'} - - sender_id = create_anchor_test_customer(endpoints, headers, SENDING_CLIENT_PAYLOAD) - transaction_payload["sender_id"] = sender_id - receiver_id = create_anchor_test_customer(endpoints, headers, RECEIVING_CLIENT_PAYLOAD) - transaction_payload["receiver_id"] = receiver_id - - if sep38_payload: - quote = create_anchor_test_quote(endpoints, headers, sep38_payload) - transaction = create_anchor_test_transaction(endpoints, headers, transaction_payload, quote["id"]) - else: - transaction = create_anchor_test_transaction(endpoints, headers, transaction_payload) - - memo_hash = transaction["stellar_memo"] - memo_hash = base64.b64decode(memo_hash) - amount = 10 - asset = Asset(transaction_payload["asset_code"], transaction_payload["asset_issuer"]) - send_asset(asset, amount, secret_key, transaction["stellar_account_id"], memo_hash) - - try: - poll_transaction_status(endpoints, headers, transaction["id"], "sep31") - except Exception as e: - print(e) - exit(1) - - -def test_sep38_create_quote(endpoints, keypair, payload): - token = get_anchor_platform_token(endpoints, keypair.public_key, keypair.secret) - - headers = {"Authorization": f"Bearer {token}", 'Content-Type': 'application/json'} - # res = requests.get("http://localhost:8080/sep38/prices?sell_asset=iso4217:USD&sell_amount=10", headers=headers) - # print(res.content) - quote = create_anchor_test_quote(endpoints, headers, payload) - return quote - - -def wait_for_anchor_platform_ready(domain, poll_interval=3, timeout=180): - print(f"polling {domain}/.well-known/stellar.toml to check for readiness") - attempt = 1 - while attempt * poll_interval <= timeout: - try: - toml = fetch_stellar_toml(domain, use_http=True) - print("anchor platform is ready") - # time.sleep(30) - return - except Exception as e: - print(e) - attempt += 1 - time.sleep(poll_interval) - else: - print("error: timed out while polling for readiness") - exit(1) - - -def test_sep24_withdrawal(endpoints, keypair, sep24_transaction_payload): - print("####################### Testing SEP-24 Send Flow #######################") - - public_key = keypair.public_key - secret_key = keypair.secret - - # 1) get anchor platform token to be used for requests - token = get_anchor_platform_token(endpoints, public_key, secret_key) - headers = {"Authorization": f"Bearer {token}", 'Content-Type': 'application/json'} - - # 2) create SEP-24 withdrawal request - print("####################### Creating SEP-24 Withdrawal Transaction #######################") - create_sep24_withdrawal_transaction = requests.post( - endpoints.ANCHOR_PLATFORM_SEP24_TRANSACTIONS_ENDPOINT + "/transactions/withdraw/interactive", - data=json.dumps(sep24_transaction_payload), headers=headers) - - res = json.loads(create_sep24_withdrawal_transaction.content) - print("transaction created:") - pprint(res) - - # TODO - check if interactive_url returns 200 - - txn_id = res["id"] - # 3) poll anchor platform and wait for transaction to be set to pending_user_transfer_start by the business server - try: - poll_transaction_status(endpoints, headers, txn_id, "sep24", status=TRANSACTION_PENDING_USER_TRANSFER_START) - except Exception as e: - print(e) - exit(1) - - # 4) get transaction with the updated details (ex: withdrawal_memo) - txn = get_transaction(endpoints, headers, txn_id, "sep24")["transaction"] - - memo_hash = txn["withdraw_memo"] - memo_hash = base64.b64decode(memo_hash) - - # TODO get asset_issuer details from sep24.info instead of hardcoding - asset = Asset("USDC", "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") - - # TODO - retrieve this from the transaction object - # 5) send the asset to the anchor distribution account - anchor_distribution_account = "GBN4NNCDGJO4XW4KQU3CBIESUJWFVBUZPOKUZHT7W7WRB7CWOA7BXVQF" - send_asset(asset, float(sep24_transaction_payload["amount"]), secret_key, anchor_distribution_account, memo_hash) - - # poll anchor platform for transaction to be updated to `complete` status by the business server - try: - poll_transaction_status(endpoints, headers, txn_id, "sep24", status=TRANSACTION_STATUS_COMPLETE) - except Exception as e: - print(e) - exit(1) - - -if __name__ == "__main__": - TESTS = [ - "sep24_withdrawal_flow", - "sep31_flow", - "sep31_flow_with_sep38", - "sep38_create_quote", - "omnibus_allowlist" - ] - - parser = argparse.ArgumentParser() - # parser.add_argument('--verbose', '-v', help="verbose mode", type=bool, default=False) TODO - # parser.add_argument('--load-size', "-ls", help="number of tests to execute (multithreaded)", type=int, default=1) - parser.add_argument('--tests', "-t", nargs="*", help=f"names of tests to execute: {TESTS}", default=TESTS) - parser.add_argument('--domain', "-d", help="The Anchor Platform endpoint", default="http://localhost:8000") - parser.add_argument('--secret', "-s", help="The secret key used for transactions", - default=os.environ.get('E2E_SECRET')) - parser.add_argument('--delay', help="Seconds to delay before running the tests", default=0) - - args = parser.parse_args() - - if args.delay: - print(f"delaying start by {args.delay} seconds") - time.sleep(int(args.delay)) - - domain = args.domain - - wait_for_anchor_platform_ready(domain) - - endpoints = Endpoints(domain) - keypair = Keypair.from_secret(args.secret) - - tests = TESTS if args.tests[0] == "all" else args.tests - - # TODO - refactor tests to multiple files - for test in tests: - if test == "sep31_flow": - print("####################### Testing SEP-31 Send Flow #######################") - TRANSACTION_PAYLOAD = { - "amount": "10.0", - "asset_code": "USDC", - "asset_issuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "fields": { - "transaction": { - "receiver_routing_number": "r0123", - "receiver_account_number": "a0456", - "type": "SWIFT" - } - } - } - test_sep_31_flow(endpoints, keypair, TRANSACTION_PAYLOAD) - elif test == "sep31_flow_with_sep38": - QUOTE_PAYLOAD_USDC_TO_JPYC = { - "sell_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "sell_amount": "10", - "buy_asset": "stellar:JPYC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "context": "sep31" - } - - TRANSACTION_PAYLOAD_USDC_TO_JPYC = { - "amount": "10.0", - "asset_code": "USDC", - "asset_issuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "fields": { - "transaction": { - "receiver_routing_number": "r0123", - "receiver_account_number": "a0456", - "type": "SWIFT" - } - } - } - - QUOTE_PAYLOAD_JPYC_TO_USD = { - "sell_asset": "stellar:JPYC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "sell_amount": "10", - "buy_asset": "iso4217:USD", - "context": "sep31" - } - - TRANSACTION_PAYLOAD_JPYC_TO_USD = { - "amount": "10.0", - "asset_code": "JPYC", - "asset_issuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "fields": { - "transaction": { - "receiver_routing_number": "r0123", - "receiver_account_number": "a0456", - "type": "SWIFT" - } - } - } - print("####################### Testing SEP-31/38 USDC to JPYC Flow #######################") - test_sep_31_flow(endpoints, keypair, TRANSACTION_PAYLOAD_USDC_TO_JPYC, - sep38_payload=QUOTE_PAYLOAD_USDC_TO_JPYC) - print() - print() - print("####################### Testing SEP-31/38 JPYC to USD Flow #######################") - test_sep_31_flow(endpoints, keypair, TRANSACTION_PAYLOAD_JPYC_TO_USD, - sep38_payload=QUOTE_PAYLOAD_JPYC_TO_USD) - elif test == "sep38_create_quote": - print("####################### Testing POST Quote #######################") - QUOTE_PAYLOAD_USDC_TO_JPYC = { - "sell_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "sell_amount": "10", - "buy_asset": "stellar:JPYC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "context": "sep31" - } - test_sep38_create_quote(endpoints, keypair, QUOTE_PAYLOAD_USDC_TO_JPYC) - elif test == "omnibus_allowlist": - print("####################### Testing Omnibus Allowlist #######################") - print(f"Omnibus Allowlist - testing with allowed key: {keypair.public_key}") - response = requests.get(endpoints.ANCHOR_PLATFORM_AUTH_ENDPOINT, {"account": keypair.public_key}) - assert response.status_code == 200, f"return code should be 200, got: {response.status_code}" - print(f"Omnibus Allowlist - testing with allowed key: {keypair.public_key} - success") - - random_kp = Keypair.random() - print(f"Omnibus Allowlist - testing with disallowed (random) key: {random_kp.public_key}") - response = requests.get(endpoints.ANCHOR_PLATFORM_AUTH_ENDPOINT, {"account": random_kp.public_key}) - assert response.status_code == 403, f"return code should be 403, got: {response.status_code}" - print(f"Omnibus Allowlist - testing with disallowed (random) key: {random_kp.public_key} expecting 403 error - success") - elif test == "sep24_withdrawal_flow": - SEP24_WITHDRAWAL_TRANSACTION_PAYLOAD = { - "asset_code": "USDC", - "amount": 10, - "asset_issuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "account": keypair.public_key, - "lang": "en" - } - test_sep24_withdrawal(endpoints, keypair, SEP24_WITHDRAWAL_TRANSACTION_PAYLOAD) - else: - exit(f"Error: unknown test {test}") diff --git a/end-to-end-tests/requirements.txt b/end-to-end-tests/requirements.txt deleted file mode 100644 index e716c45b8d..0000000000 --- a/end-to-end-tests/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -stellar-sdk \ No newline at end of file diff --git a/essential-tests/build.gradle.kts b/essential-tests/build.gradle.kts new file mode 100644 index 0000000000..120ec414aa --- /dev/null +++ b/essential-tests/build.gradle.kts @@ -0,0 +1,50 @@ +// The alias call in plugins scope produces IntelliJ false error which is suppressed here. +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + `java-library` + `java-test-fixtures` + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.kotlin.jvm) +} + +repositories { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } + +dependencies { + testFixturesImplementation(libs.assertj.core) + testFixturesImplementation(libs.httpclient) + testFixturesImplementation(libs.kotlin.serialization.json) + + // Stellar dependencies + testFixturesImplementation(libs.stellar.wallet.sdk) + testFixturesImplementation(variantOf(libs.java.stellar.sdk) { classifier("uber") }) + + // Spring dependencies + testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jpa") + testFixturesImplementation("org.springframework.boot:spring-boot-starter-web") + + // project dependencies + testFixturesImplementation(project(":api-schema")) + testFixturesImplementation(project(":core")) + testFixturesImplementation(project(":platform")) + testFixturesImplementation(project(":kotlin-reference-server")) + testFixturesImplementation(project(":wallet-reference-server")) + testFixturesImplementation(project(":service-runner")) + testFixturesImplementation(project(":lib-util")) + + testImplementation(libs.stellar.wallet.sdk) +} + +tasks { bootJar { enabled = false } } + +// The following is to enable test concurrency +apply(from = "$rootDir/scripts.gradle.kts") +@Suppress("UNCHECKED_CAST") +val enableTestConcurrency = extra["enableTestConcurrency"] as (Test) -> Unit + +tasks.test { + enableTestConcurrency(this) + exclude("**/org/stellar/anchor/platform/*Test.class") + exclude("**/org/stellar/anchor/platform/integrationtest/**") + exclude("**/org/stellar/anchor/platform/e2etest/**") +} diff --git a/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/End2EndTests.kt b/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/End2EndTests.kt new file mode 100644 index 0000000000..b7176a5158 --- /dev/null +++ b/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/End2EndTests.kt @@ -0,0 +1,6 @@ +package org.stellar.anchor.platform.suite + +import org.junit.platform.suite.api.SelectPackages +import org.junit.platform.suite.api.Suite + +@Suite @SelectPackages("org.stellar.anchor.platform.e2etest") class End2EndTests diff --git a/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/IntegrationTests.kt b/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/IntegrationTests.kt new file mode 100644 index 0000000000..55d01a1882 --- /dev/null +++ b/essential-tests/src/test/kotlin/org/stellar/anchor/platform/suite/IntegrationTests.kt @@ -0,0 +1,6 @@ +package org.stellar.anchor.platform.suite + +import org.junit.platform.suite.api.SelectPackages +import org.junit.platform.suite.api.Suite + +@Suite @SelectPackages("org.stellar.anchor.platform.integrationtest") class IntegrationTests diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/AbstractIntegrationTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/AbstractIntegrationTests.kt new file mode 100644 index 0000000000..a5d95b3ebf --- /dev/null +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/AbstractIntegrationTests.kt @@ -0,0 +1,53 @@ +package org.stellar.anchor.platform + +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.stellar.anchor.util.Sep1Helper.TomlContent +import org.stellar.anchor.util.Sep1Helper.parse +import org.stellar.walletsdk.ApplicationConfiguration +import org.stellar.walletsdk.StellarConfiguration +import org.stellar.walletsdk.Wallet +import org.stellar.walletsdk.anchor.auth +import org.stellar.walletsdk.auth.AuthToken +import org.stellar.walletsdk.horizon.SigningKeyPair + +abstract class AbstractIntegrationTests(val config: TestConfig) { + var toml: TomlContent = + parse(resourceAsString("${config.env["anchor.domain"]}/.well-known/stellar.toml")) + var wallet = + Wallet( + StellarConfiguration.Testnet, + ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } + ) + var walletKeyPair = SigningKeyPair.fromSecret(CLIENT_WALLET_SECRET) + var anchor = wallet.anchor(config.env["anchor.domain"]!!) + var token: AuthToken + private val submissionLock = Mutex() + + suspend fun transactionWithRetry( + maxAttempts: Int = 5, + delay: Int = 5, + transactionLogic: suspend () -> Unit + ) = + flow { submissionLock.withLock { transactionLogic() } } + .retryWhen { _, attempt -> + if (attempt < maxAttempts) { + delay((delay + (1..5).random()).seconds) + return@retryWhen true + } else { + return@retryWhen false + } + } + .collect {} + + init { + runBlocking { token = anchor.auth().authenticate(walletKeyPair) } + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/TestAccounts.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/TestAccounts.kt similarity index 100% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/TestAccounts.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/TestAccounts.kt diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24End2EndTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep24End2EndTests.kt similarity index 63% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24End2EndTests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep24End2EndTests.kt index 6a4243a95f..a71735526a 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24End2EndTests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep24End2EndTests.kt @@ -1,16 +1,23 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.e2etest import io.ktor.client.* import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.http.* -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.fail +import java.util.stream.Stream +import kotlin.test.fail import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Assertions.assertNotNull +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.springframework.web.util.UriComponentsBuilder import org.stellar.anchor.api.callback.SendEventRequest import org.stellar.anchor.api.callback.SendEventRequestPayload @@ -21,15 +28,13 @@ import org.stellar.anchor.api.sep.SepTransactionStatus import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.Sep24InteractiveUrlJwt +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.CLIENT_WALLET_SECRET import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.util.Log.info import org.stellar.reference.client.AnchorReferenceServerClient import org.stellar.reference.wallet.WalletServerClient -import org.stellar.walletsdk.ApplicationConfiguration import org.stellar.walletsdk.InteractiveFlowResponse -import org.stellar.walletsdk.StellarConfiguration -import org.stellar.walletsdk.Wallet import org.stellar.walletsdk.anchor.* import org.stellar.walletsdk.anchor.TransactionStatus.* import org.stellar.walletsdk.asset.IssuedAssetId @@ -38,16 +43,15 @@ import org.stellar.walletsdk.asset.XLM import org.stellar.walletsdk.auth.AuthToken import org.stellar.walletsdk.horizon.SigningKeyPair import org.stellar.walletsdk.horizon.sign -import org.stellar.walletsdk.horizon.transaction.transferWithdrawalTransaction - -class Sep24End2EndTests(config: TestConfig, val jwt: String) { - private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET - private val keypair = SigningKeyPair.fromSecret(walletSecretKey) - private val wallet = - Wallet( - StellarConfiguration.Testnet, - ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } - ) + +const val WITHDRAW_FUND_CLIENT_SECRET_1 = "SCGHF6KF6CBQ6Z4ZZUMU4DGRM6LR2PS7XOUN5VOETMPTPLD5BQE2FKL3" +const val WITHDRAW_FUND_CLIENT_SECRET_2 = "SBSO7FVRDHCETSGPYETIFNVK64LS4KH325GOAENGV5Z7L6FAP7YS2BPK" +const val DEPOSIT_FUND_CLIENT_SECRET_1 = "SDNZAK6LCYNR4HYEFBZY3I2KLRDLSCE5RCF6HZ2KBBC7JLCFNZAHJCBQ" +const val DEPOSIT_FUND_CLIENT_SECRET_2 = "SCW2SJEPTL4K7FFPFOFABFEFZJCG6LHULWVJX6JLIJ7TYIKTL6P473HM" + +@TestInstance(PER_CLASS) +@Execution(CONCURRENT) +class Sep24End2EndTests : AbstractIntegrationTests(TestConfig()) { private val client = HttpClient { install(HttpTimeout) { requestTimeoutMillis = 300000 @@ -55,14 +59,6 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { socketTimeoutMillis = 300000 } } - private val anchor = - wallet.anchor(config.env["anchor.domain"]!!) { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } private val maxTries = 30 private val anchorReferenceServerClient = AnchorReferenceServerClient(Url(config.env["reference.server.url"]!!)) @@ -77,39 +73,46 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { config.env["secret.custody_server.auth_secret"]!! ) - private fun `test typical deposit end-to-end flow`(asset: StellarAssetId, amount: String) = - runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val token = anchor.auth().authenticate(keypair) - val response = makeDeposit(asset, amount, token) - - // Assert the interactive URL JWT is valid - val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams - val cipher = params["token"]!![0] - val interactiveJwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) - assertEquals("referenceCustodial", interactiveJwt.claims[JwtService.CLIENT_NAME]) - - // Wait for the status to change to COMPLETED - waitForTxnStatus(response.id, COMPLETED, token) - - // Check if the transaction can be listed by stellar transaction id - val fetchedTxn = anchor.interactive().getTransaction(response.id, token) as DepositTransaction - val transactionByStellarId = - anchor - .interactive() - .getTransactionBy(token, stellarTransactionId = fetchedTxn.stellarTransactionId) - assertEquals(fetchedTxn.id, transactionByStellarId.id) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(response.id, 5) - assertEvents(actualEvents, expectedDepositStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(response.id, 5) - assertCallbacks(actualCallbacks, expectedDepositStatuses) - } + @ParameterizedTest + @MethodSource("depositAssetsAndAmounts") + fun `test typical deposit end-to-end flow`( + walletSecretKey: String, + asset: StellarAssetId, + amount: String + ) = runBlocking { + val keypair = SigningKeyPair.fromSecret(walletSecretKey) + walletServerClient.clearCallbacks() + anchorReferenceServerClient.clearEvents() + + val token = anchor.auth().authenticate(keypair) + val response = makeDeposit(asset, amount, token) + + // Assert the interactive URL JWT is valid + val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams + val cipher = params["token"]!![0] + val interactiveJwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) + assertEquals(keypair.address, interactiveJwt.sub) + assertEquals(amount, (interactiveJwt.claims["data"] as Map<*, *>)["amount"], amount) + + // Wait for the status to change to COMPLETED + waitForTxnStatus(response.id, COMPLETED, token) + + // Check if the transaction can be listed by stellar transaction id + val fetchedTxn = anchor.interactive().getTransaction(response.id, token) as DepositTransaction + val transactionByStellarId = + anchor + .interactive() + .getTransactionBy(token, stellarTransactionId = fetchedTxn.stellarTransactionId) + assertEquals(fetchedTxn.id, transactionByStellarId.id) + + // Check the events sent to the reference server are recorded correctly + val actualEvents = waitForBusinessServerEvents(response.id, 5) + assertEvents(actualEvents, expectedDepositStatuses) + + // Check the callbacks sent to the wallet reference server are recorded correctly + val actualCallbacks = waitForWalletServerCallbacks(response.id, 5) + assertCallbacks(actualCallbacks, expectedDepositStatuses) + } private suspend fun makeDeposit( asset: StellarAssetId, @@ -144,9 +147,7 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { assertNotNull(actualEvent.id) assertNotNull(actualEvent.timestamp) assertEquals(expectedStatus.first.type, actualEvent.type) - org.junit.jupiter.api.Assertions.assertTrue( - actualEvent.payload is SendEventRequestPayload - ) + assertTrue(actualEvent.payload is SendEventRequestPayload) assertEquals(expectedStatus.second, actualEvent.payload.transaction.status) } } @@ -170,14 +171,22 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { } } - private fun `test typical withdraw end-to-end flow`(asset: StellarAssetId, amount: String) { - `test typical withdraw end-to-end flow`(asset, mapOf("amount" to amount)) + @ParameterizedTest + @MethodSource("withdrawAssetsAndAmounts") + fun `test typical withdraw end-to-end flow`( + walletSecretKey: String, + asset: StellarAssetId, + amount: String + ) { + `test typical withdraw end-to-end flow`(walletSecretKey, asset, mapOf("amount" to amount)) } private fun `test typical withdraw end-to-end flow`( + walletSecretKey: String, asset: StellarAssetId, extraFields: Map ) = runBlocking { + val keypair = SigningKeyPair.fromSecret(walletSecretKey) walletServerClient.clearCallbacks() anchorReferenceServerClient.clearEvents() @@ -197,14 +206,17 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { // Submit transfer transaction val walletTxn = (anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction) - val transfer = - wallet - .stellar() - .transaction(walletTxn.from!!) - .transferWithdrawalTransaction(walletTxn, asset) - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) + transactionWithRetry { + val transfer = + wallet + .stellar() + .transaction(walletTxn.from!!) + .transferWithdrawalTransaction(walletTxn, asset) + .build() + transfer.sign(keypair) + + wallet.stellar().submitTransaction(transfer) + } // Wait for the status to change to PENDING_USER_TRANSFER_END waitForTxnStatus(withdrawTxn.id, COMPLETED, token) @@ -233,10 +245,7 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { var retries = 5 var callbacks: List? = null while (retries > 0) { - callbacks = - walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java).distinctBy { - it.transaction.status - } + callbacks = walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java) if (callbacks.size == count) { return callbacks } @@ -266,52 +275,57 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { private suspend fun waitForTxnStatus( id: String, expectedStatus: TransactionStatus, - token: AuthToken + token: AuthToken, + exitStatus: TransactionStatus = ERROR ) { var status: TransactionStatus? = null for (i in 0..maxTries) { // Get transaction info val transaction = anchor.interactive().getTransaction(id, token) - if (status != transaction.status) { status = transaction.status - info( "Transaction(id=${transaction.id}) status changed to $status. Message: ${transaction.message}" ) } - delay(1.seconds) + if (transaction.status == expectedStatus) return - if (transaction.status == expectedStatus) { - return - } + if (transaction.status == exitStatus) break + + delay(1.seconds) } fail("Transaction wasn't $expectedStatus in $maxTries tries, last status: $status") } - private fun `test created transactions show up in the get history call`( + @ParameterizedTest + @MethodSource("historyAssetsAndAmounts") + fun `test created transactions show up in the get history call`( + walletSecretKey: String, asset: StellarAssetId, amount: String ) = runBlocking { + val keypair = SigningKeyPair.fromSecret(walletSecretKey) val newAcc = wallet.stellar().account().createKeyPair() - val tx = - wallet - .stellar() - .transaction(keypair) - .sponsoring(keypair, newAcc) { - createAccount(newAcc) - addAssetSupport(USDC) - } - .build() - .sign(keypair) - .sign(newAcc) - - wallet.stellar().submitTransaction(tx) - + transactionWithRetry { + val tx = + wallet + .stellar() + .transaction(keypair) + .sponsoring(keypair, newAcc) { + createAccount(newAcc) + addAssetSupport(USDC) + } + .build() + .sign(keypair) + .sign(newAcc) + + wallet.stellar().submitTransaction(tx) + } + delay(5000) val token = anchor.auth().authenticate(newAcc) val deposits = (0..1).map { @@ -320,19 +334,8 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { txnId } val history = anchor.interactive().getTransactionsForAsset(asset, token) - - Assertions.assertThat(history).allMatch { deposits.contains(it.id) } - } - - fun testAll() { - info("Running SEP-24 USDC end-to-end tests...") - `test typical deposit end-to-end flow`(USDC, "1.1") - `test typical withdraw end-to-end flow`(USDC, "1.1") - `test created transactions show up in the get history call`(USDC, "1.1") - info("Running SEP-24 XLM end-to-end tests...") - `test typical deposit end-to-end flow`(XLM, "0.00001") - `test typical withdraw end-to-end flow`(XLM, "0.00001") - `test created transactions show up in the get history call`(XLM, "0.00001") + assertThat(history).allMatch { deposits.contains(it.id) } + println("test created transactions show up in the get history call passed") } companion object { @@ -354,5 +357,26 @@ class Sep24End2EndTests(config: TestConfig, val jwt: String) { TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_EXTERNAL, TRANSACTION_STATUS_CHANGED to SepTransactionStatus.COMPLETED ) + + @JvmStatic + fun depositAssetsAndAmounts(): Stream { + return Stream.of( + Arguments.of(DEPOSIT_FUND_CLIENT_SECRET_1, USDC, "0.01"), + Arguments.of(DEPOSIT_FUND_CLIENT_SECRET_2, XLM, "0.0001") + ) + } + + @JvmStatic + fun withdrawAssetsAndAmounts(): Stream { + return Stream.of( + Arguments.of(WITHDRAW_FUND_CLIENT_SECRET_1, USDC, "0.01"), + Arguments.of(WITHDRAW_FUND_CLIENT_SECRET_2, XLM, "0.0001") + ) + } + + @JvmStatic + fun historyAssetsAndAmounts(): Stream { + return Stream.of(Arguments.of(CLIENT_WALLET_SECRET, USDC, "0.01")) + } } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt similarity index 73% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt index 033bc8d793..a151395a85 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt @@ -1,6 +1,5 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.e2etest -import io.ktor.client.plugins.* import io.ktor.http.* import kotlin.test.DefaultAsserter.fail import kotlin.test.assertContentEquals @@ -8,43 +7,29 @@ import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode import org.stellar.anchor.api.sep.SepTransactionStatus import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.anchor.api.sep.sep6.GetTransactionResponse import org.stellar.anchor.api.shared.InstructionField -import org.stellar.anchor.platform.CLIENT_WALLET_SECRET -import org.stellar.anchor.platform.Sep6Client +import org.stellar.anchor.client.Sep6Client +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.Log import org.stellar.reference.client.AnchorReferenceServerClient import org.stellar.reference.wallet.WalletServerClient -import org.stellar.walletsdk.ApplicationConfiguration -import org.stellar.walletsdk.StellarConfiguration -import org.stellar.walletsdk.Wallet import org.stellar.walletsdk.anchor.MemoType import org.stellar.walletsdk.anchor.auth import org.stellar.walletsdk.anchor.customer import org.stellar.walletsdk.asset.IssuedAssetId -import org.stellar.walletsdk.horizon.SigningKeyPair import org.stellar.walletsdk.horizon.sign -class Sep6End2EndTest(val config: TestConfig, val jwt: String) { - private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET - private val keypair = SigningKeyPair.fromSecret(walletSecretKey) - private val wallet = - Wallet( - StellarConfiguration.Testnet, - ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } - ) - private val anchor = - wallet.anchor(config.env["anchor.domain"]!!) { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(ExecutionMode.CONCURRENT) +class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) { private val maxTries = 30 private val anchorReferenceServerClient = AnchorReferenceServerClient(Url(config.env["reference.server.url"]!!)) @@ -72,29 +57,34 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { ) } - private fun `test typical deposit end-to-end flow`() = runBlocking { - val token = anchor.auth().authenticate(keypair) + @Test + fun `test typical deposit end-to-end flow`() = runBlocking { + val memo = (10000..20000).random().toULong() + val token = anchor.auth().authenticate(walletKeyPair, memoId = memo) // TODO: migrate this to wallet-sdk when it's available val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token.token) // Create a customer before starting the transaction - anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! }) + anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! }, memo) val deposit = sep6Client.deposit( mapOf( "asset_code" to USDC.code, - "account" to keypair.address, + "account" to walletKeyPair.address, "amount" to "1", "type" to "SWIFT" ) ) + Log.info("Deposit initiated: ${deposit.id}") waitStatus(deposit.id, PENDING_CUSTOMER_INFO_UPDATE, sep6Client) // Supply missing KYC info to continue with the transaction val additionalRequiredFields = sep6Client.getTransaction(mapOf("id" to deposit.id)).transaction.requiredCustomerInfoUpdates - anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! }) + anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! }, memo) + Log.info("Submitted additional KYC info: $additionalRequiredFields") + Log.info("Bank transfer complete") waitStatus(deposit.id, COMPLETED, sep6Client) val completedDepositTxn = sep6Client.getTransaction(mapOf("id" to deposit.id)) @@ -122,7 +112,6 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { val expectedStatuses = listOf( INCOMPLETE, - PENDING_ANCHOR, // update amounts PENDING_CUSTOMER_INFO_UPDATE, // request KYC PENDING_USR_TRANSFER_START, // provide deposit instructions PENDING_ANCHOR, // deposit into user wallet @@ -133,43 +122,51 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { assertWalletReceivedStatuses(deposit.id, expectedStatuses) } - private fun `test typical withdraw end-to-end flow`() = runBlocking { - val token = anchor.auth().authenticate(keypair) + @Test + fun `test typical withdraw end-to-end flow`() = runBlocking { + val memo = (30000..40000).random().toULong() + val token = anchor.auth().authenticate(walletKeyPair, memoId = memo) // TODO: migrate this to wallet-sdk when it's available val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token.token) // Create a customer before starting the transaction - anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! }) + anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! }, memo) val withdraw = sep6Client.withdraw( mapOf("asset_code" to USDC.code, "amount" to "1", "type" to "bank_account") ) + Log.info("Withdrawal initiated: ${withdraw.id}") waitStatus(withdraw.id, PENDING_CUSTOMER_INFO_UPDATE, sep6Client) // Supply missing financial account info to continue with the transaction val additionalRequiredFields = sep6Client.getTransaction(mapOf("id" to withdraw.id)).transaction.requiredCustomerInfoUpdates - anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! }) + anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! }, memo) + Log.info("Submitted additional KYC info: $additionalRequiredFields") + waitStatus(withdraw.id, PENDING_USR_TRANSFER_START, sep6Client) val withdrawTxn = sep6Client.getTransaction(mapOf("id" to withdraw.id)).transaction // Transfer the withdrawal amount to the Anchor - val transfer = - wallet - .stellar() - .transaction(keypair, memo = Pair(MemoType.HASH, withdrawTxn.withdrawMemo)) - .transfer(withdrawTxn.withdrawAnchorAccount, USDC, "1") - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) + Log.info("Transferring 1 USDC to Anchor account: ${withdrawTxn.withdrawAnchorAccount}") + transactionWithRetry { + val transfer = + wallet + .stellar() + .transaction(walletKeyPair, memo = Pair(MemoType.HASH, withdrawTxn.withdrawMemo)) + .transfer(withdrawTxn.withdrawAnchorAccount, USDC, "1") + .build() + transfer.sign(walletKeyPair) + wallet.stellar().submitTransaction(transfer) + } + Log.info("Transfer complete") waitStatus(withdraw.id, COMPLETED, sep6Client) val expectedStatuses = listOf( INCOMPLETE, - PENDING_ANCHOR, // update amounts PENDING_CUSTOMER_INFO_UPDATE, // request KYC PENDING_USR_TRANSFER_START, // wait for onchain user transfer PENDING_ANCHOR, // funds available for pickup @@ -204,24 +201,20 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { expectedStatus: SepTransactionStatus, sep6Client: Sep6Client ) { + var status: String? = null for (i in 0..maxTries) { val transaction = sep6Client.getTransaction(mapOf("id" to id)) - if (expectedStatus.status != transaction.transaction.status) { - Log.info("Transaction status: ${transaction.transaction.status}") - } else { - Log.info("${GsonUtils.getInstance().toJson(transaction)}") + if (!status.equals(transaction.transaction.status)) { + status = transaction.transaction.status Log.info( - "Transaction status ${transaction.transaction.status} matched expected status $expectedStatus" + "Transaction(${transaction.transaction.id}) status changed to ${status}. Message: ${transaction.transaction.message}" ) + } + if (transaction.transaction.status == expectedStatus.status) { return } delay(1.seconds) } - fail("Transaction status did not match expected status $expectedStatus") - } - - fun testAll() { - `test typical deposit end-to-end flow`() - `test typical withdraw end-to-end flow`() + fail("Transaction status $status did not match expected status $expectedStatus") } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CallbackApiTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackApiTests.kt similarity index 87% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CallbackApiTests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackApiTests.kt index 7f7b409c82..d0a177dbbd 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CallbackApiTests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackApiTests.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import com.google.gson.Gson import java.time.Instant @@ -8,9 +8,10 @@ import java.time.format.DateTimeFormatter import java.util.* import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode import org.skyscreamer.jsonassert.JSONAssert import org.stellar.anchor.api.callback.GetCustomerRequest import org.stellar.anchor.api.callback.GetFeeRequest @@ -19,15 +20,20 @@ import org.stellar.anchor.api.exception.NotFoundException import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest import org.stellar.anchor.auth.AuthHelper import org.stellar.anchor.auth.JwtService -import org.stellar.anchor.platform.Sep12Client +import org.stellar.anchor.client.Sep12Client +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.callback.RestCustomerIntegration import org.stellar.anchor.platform.callback.RestFeeIntegration import org.stellar.anchor.platform.callback.RestRateIntegration +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer1Json +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer2Json import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Sep1Helper -class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, val jwt: String) { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(ExecutionMode.SAME_THREAD) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class CallbackApiTests : AbstractIntegrationTests(TestConfig()) { companion object { private const val JWT_EXPIRATION_MILLISECONDS: Long = 10000 @@ -36,7 +42,7 @@ class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" } - private val sep12Client: Sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt) + private val sep12Client: Sep12Client = Sep12Client(toml.getString("KYC_SERVER"), token.token) private val httpClient: OkHttpClient = OkHttpClient.Builder() @@ -67,13 +73,15 @@ class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, private val rfiClient = RestFeeIntegration(config.env["reference.server.url"]!!, httpClient, authHelper, gson) - private fun testCustomerIntegration() { + @Test + fun testCustomerIntegration() { assertThrows { rci.getCustomer(GetCustomerRequest.builder().id("1").build()) } } - private fun testRate_indicativePrice() { + @Test + fun testRate_indicativePrice() { val result = rriClient.getRate( GetRateRequest.builder() @@ -107,7 +115,8 @@ class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, JSONAssert.assertEquals(wantBody, org.stellar.anchor.platform.gson.toJson(result), true) } - private fun testRate_firm() { + @Test + fun testRate_firm() { val rate = rriClient .getRate( @@ -169,7 +178,8 @@ class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, JSONAssert.assertEquals(wantBody, org.stellar.anchor.platform.gson.toJson(gotQuote), true) } - private fun testGetFee() { + @Test + fun testGetFee() { // Create sender customer val senderCustomerRequest = GsonUtils.getInstance().fromJson(testCustomer1Json, Sep12PutCustomerRequest::class.java) @@ -204,13 +214,4 @@ class CallbackApiTests(val config: TestConfig, val toml: Sep1Helper.TomlContent, true ) } - - fun testAll() { - println("Performing Callback API tests...") - - testCustomerIntegration() - testRate_indicativePrice() - testRate_firm() - testGetFee() - } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/CallbackSignatureTest.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackSignatureTest.kt similarity index 95% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/CallbackSignatureTest.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackSignatureTest.kt index 4d66e7fd36..e057b97a8c 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/CallbackSignatureTest.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/CallbackSignatureTest.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.platform.integrationtest import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/EventProcessingServerTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/EventProcessingTests.kt similarity index 86% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/EventProcessingServerTests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/EventProcessingTests.kt index 2eaaaa5a7d..ce27eb9ae8 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/EventProcessingServerTests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/EventProcessingTests.kt @@ -1,18 +1,21 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest +import org.junit.jupiter.api.Test import org.stellar.anchor.api.event.AnchorEvent import org.stellar.anchor.event.EventService.EventQueue.TRANSACTION +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.config.PropertyEventConfig import org.stellar.anchor.platform.event.DefaultEventService import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Sep1Helper -class EventProcessingServerTests(config: TestConfig, toml: Sep1Helper.TomlContent, jwt: String) { +class EventProcessingServerTests : AbstractIntegrationTests(TestConfig()) { companion object { val eventConfig = GsonUtils.getInstance().fromJson(eventConfigJson, PropertyEventConfig::class.java)!! } + + @Test fun testOk() { val eventService = DefaultEventService(eventConfig) val session = eventService.createSession("testOk", TRANSACTION) @@ -20,10 +23,6 @@ class EventProcessingServerTests(config: TestConfig, toml: Sep1Helper.TomlConten session.publish(quoteEvent) } - fun testAll() { - println("Performing event processing server tests...") - testOk() - } } val testQuoteEvent = diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/PlatformApiTests.kt similarity index 51% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiTests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/PlatformApiTests.kt index d27567a9c2..4b859600d3 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiTests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/PlatformApiTests.kt @@ -1,8 +1,9 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import com.google.gson.reflect.TypeToken -import org.apache.http.HttpStatus +import org.apache.http.HttpStatus.SC_OK import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.skyscreamer.jsonassert.Customization import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode @@ -17,125 +18,165 @@ import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionRequest import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.Sep12Client -import org.stellar.anchor.platform.Sep24Client -import org.stellar.anchor.platform.Sep31Client +import org.stellar.anchor.client.Sep12Client +import org.stellar.anchor.client.Sep24Client +import org.stellar.anchor.client.Sep31Client +import org.stellar.anchor.client.Sep6Client +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Sep1Helper.TomlContent - -class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { - companion object { - private const val TX_ID = "testTxId" - private const val JSON_RPC_VERSION = "2.0" - private const val TX_ID_KEY = "TX_ID" - private const val RECEIVER_ID_KEY = "RECEIVER_ID" - private const val SENDER_ID_KEY = "SENDER_ID" - } +class PlatformApiTests : AbstractIntegrationTests(TestConfig()) { private val gson = GsonUtils.getInstance() private val platformApiClient = PlatformApiClient(AuthHelper.forNone(), config.env["platform.server.url"]!!) - private val sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt) - private val sep24Client = Sep24Client(toml.getString("TRANSFER_SERVER_SEP0024"), jwt) - private val sep31Client = Sep31Client(toml.getString("DIRECT_PAYMENT_SERVER"), jwt) - - fun testAll() { - println("Performing Platform API tests...") - `SEP-24 deposit complete short flow`() - `SEP-24 deposit complete full with trust flow`() - `SEP-24 deposit complete full with recovery flow`() - `SEP-24 deposit complete short partial refund flow`() - `SEP-24 withdraw complete short flow`() - `SEP-24 withdraw complete full via pending external`() - `SEP-24 withdraw complete full via pending user`() - `SEP-24 withdraw full refund`() - `SEP-31 refunded short`() - `SEP-31 complete full with recovery`() - `validations and errors`() - `send single rpc request`() - `send batch of rpc requests`() + private val sep12Client = Sep12Client(toml.getString("KYC_SERVER"), token.token) + private val sep6Client = Sep6Client(toml.getString("TRANSFER_SERVER"), token.token) + private val sep24Client = Sep24Client(toml.getString("TRANSFER_SERVER_SEP0024"), token.token) + private val sep31Client = Sep31Client(toml.getString("DIRECT_PAYMENT_SERVER"), token.token) + + /** + * 1. incomplete -> request_offchain_funds + * 2. pending_user_transfer_start -> notify_offchain_funds_received + * 3. pending_anchor -> notify_onchain_funds_sent + * 4. completed + */ + @Test + fun `SEP-6 deposit complete short flow`() { + `test sep6 deposit flow`( + SEP_6_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, + SEP_6_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES + ) } - private fun `send single rpc request`() { - val depositRequest = gson.fromJson(SEP_24_DEPOSIT_REQUEST, HashMap::class.java) + /** + * 1. incomplete -> request_offchain_funds + * 2. pending_user_transfer_start -> notify_offchain_funds_received + * 3. pending_anchor -> notify_onchain_funds_sent + * 4. completed + */ + @Test + fun `SEP-6 deposit-exchange complete short flow`() { + `test sep6 deposit-exchange flow`( + SEP_6_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, + SEP_6_DEPOSIT_EXCHANGE_COMPLETE_SHORT_FLOW_ACTION_RESPONSES + ) + } - @Suppress("UNCHECKED_CAST") - val depositResponse = sep24Client.deposit(depositRequest as HashMap) - val txId = depositResponse.id + /** + * 1. incomplete -> request_customer_info_update + * 2. pending_customer_info_update -> request_offchain_funds + * 3. pending_user_transfer_start -> notify_offchain_funds_received + * 4. pending_anchor -> request_trust + * 5. pending_trust -> notify_trust_set + * 6. pending_anchor -> notify_onchain_funds_sent + * 7. completed + */ + @Test + fun `SEP-6 deposit complete full with trust flow`() { + `test sep6 deposit flow`( + SEP_6_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS, + SEP_6_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES + ) + } - val requestOffchainFundsParams = - gson.fromJson(REQUEST_OFFCHAIN_FUNDS_PARAMS, RequestOffchainFundsRequest::class.java) - requestOffchainFundsParams.transactionId = txId - val rpcRequest = - RpcRequest.builder() - .method(REQUEST_OFFCHAIN_FUNDS.toString()) - .jsonrpc(JSON_RPC_VERSION) - .params(requestOffchainFundsParams) - .id(1) - .build() - val response = platformApiClient.sendRpcRequest(listOf(rpcRequest)) - assertEquals(HttpStatus.SC_OK, response.code) - JSONAssert.assertEquals( - EXPECTED_RPC_RESPONSE.replace(TX_ID, txId).trimIndent(), - response.body?.string()?.trimIndent(), - CustomComparator( - JSONCompareMode.STRICT, - Customization("[*].result.started_at") { _, _ -> true }, - Customization("[*].result.updated_at") { _, _ -> true } - ) + /** + * 1. incomplete -> request_offchain_funds + * 2. pending_user_transfer_start -> notify_offchain_funds_received + * 3. pending_anchor -> notify_transaction_error + * 4. error -> notify_transaction_recovery + * 5. pending_anchor -> notify_onchain_funds_sent + * 6. completed + */ + @Test + fun `SEP-6 deposit complete full with recovery flow`() { + `test sep6 deposit flow`( + SEP_6_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS, + SEP_6_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES ) + } - val txResponse = platformApiClient.getTransaction(txId) - assertEquals(SepTransactionStatus.PENDING_USR_TRANSFER_START, txResponse.status) + /** + * 1. incomplete -> request_offchain_funds + * 2. pending_user_transfer_start -> notify_offchain_funds_received + * 3. pending_anchor -> notify_refund_pending + * 4. pending_external -> notify_refund_sent + * 5. pending_anchor -> notify_onchain_funds_sent + * 6. completed + */ + @Test + fun `SEP-6 deposit complete short partial refund flow`() { + `test sep6 deposit flow`( + SEP_6_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUESTS, + SEP_6_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPONSES + ) } - private fun `send batch of rpc requests`() { - val depositRequest = gson.fromJson(SEP_24_DEPOSIT_REQUEST, HashMap::class.java) + /** + * 1. incomplete -> request_onchain_funds + * 2. pending_user_transfer_start -> notify_onchain_funds_received + * 3. pending_anchor -> notify_offchain_funds_sent + * 4. completed + */ + @Test + fun `SEP-6 withdraw complete short flow`() { + `test sep6 withdraw flow`( + SEP_6_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, + SEP_6_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES + ) + } - @Suppress("UNCHECKED_CAST") - val depositResponse = sep24Client.deposit(depositRequest as HashMap) - val txId = depositResponse.id + /** + * 1. incomplete -> request_onchain_funds + * 2. pending_user_transfer_start -> notify_onchain_funds_received + * 3. pending_anchor -> notify_offchain_funds_sent + * 4. completed + */ + @Test + fun `SEP-6 withdraw-exchange complete short flow`() { + `test sep6 withdraw-exchange flow`( + SEP_6_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, + SEP_6_WITHDRAW_EXCHANGE_COMPLETE_SHORT_FLOW_ACTION_RESPONSES + ) + } - val requestOffchainFundsParams = - gson.fromJson(REQUEST_OFFCHAIN_FUNDS_PARAMS, RequestOffchainFundsRequest::class.java) - val notifyOffchainFundsReceivedParams = - gson.fromJson( - NOTIFY_OFFCHAIN_FUNDS_RECEIVED_PARAMS, - NotifyOffchainFundsReceivedRequest::class.java - ) - requestOffchainFundsParams.transactionId = txId - notifyOffchainFundsReceivedParams.transactionId = txId - val rpcRequest1 = - RpcRequest.builder() - .id(1) - .method(REQUEST_OFFCHAIN_FUNDS.toString()) - .jsonrpc(JSON_RPC_VERSION) - .params(requestOffchainFundsParams) - .build() - val rpcRequest2 = - RpcRequest.builder() - .id(2) - .method(RpcMethod.NOTIFY_OFFCHAIN_FUNDS_RECEIVED.toString()) - .jsonrpc(JSON_RPC_VERSION) - .params(notifyOffchainFundsReceivedParams) - .build() - val response = platformApiClient.sendRpcRequest(listOf(rpcRequest1, rpcRequest2)) - assertEquals(HttpStatus.SC_OK, response.code) + /** + * 1. incomplete -> request_onchain_funds + * 2. pending_user_transfer_start -> notify_onchain_funds_received + * 3. pending_anchor -> notify_offchain_funds_pending + * 4. pending_external -> notify_offchain_funds_sent + * 5. completed + */ + @Test + fun `SEP-6 withdraw complete full via pending external`() { + `test sep6 withdraw flow`( + SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_REQUESTS, + SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_RESPONSES + ) + } - JSONAssert.assertEquals( - EXPECTED_RPC_BATCH_RESPONSE.replace(TX_ID, txId).trimIndent(), - response.body?.string()?.trimIndent(), - CustomComparator( - JSONCompareMode.STRICT, - Customization("[*].result.started_at") { _, _ -> true }, - Customization("[*].result.updated_at") { _, _ -> true } - ) + /** + * 1. incomplete -> request_onchain_funds + * 2. pending_user_transfer_start -> notify_onchain_funds_received + * 3. pending_anchor -> notify_offchain_funds_available + * 4. pending_user_transfer_complete -> notify_offchain_funds_sent + * 5. completed + */ + @Test + fun `SEP-6 withdraw complete full via pending user`() { + `test sep6 withdraw flow`( + SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQUESTS, + SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RESPONSES ) + } - val txResponse = platformApiClient.getTransaction(txId) - assertEquals(SepTransactionStatus.PENDING_ANCHOR, txResponse.status) + @Test + fun `SEP-6 withdraw full refund`() { + `test sep6 withdraw flow`( + SEP_6_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS, + SEP_6_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES + ) } /** @@ -144,8 +185,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 3. pending_anchor -> notify_onchain_funds_sent * 4. completed */ - private fun `SEP-24 deposit complete short flow`() { - `test deposit flow`( + @Test + fun `SEP-24 deposit complete short flow`() { + `test sep24 deposit flow`( SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES ) @@ -160,8 +202,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 6. pending_anchor -> notify_onchain_funds_sent * 7. completed */ - private fun `SEP-24 deposit complete full with trust flow`() { - `test deposit flow`( + @Test + fun `SEP-24 deposit complete full with trust flow`() { + `test sep24 deposit flow`( SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES ) @@ -175,13 +218,13 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 5. pending_anchor -> notify_onchain_funds_sent * 6. completed */ - private fun `SEP-24 deposit complete full with recovery flow`() { - `test deposit flow`( + @Test + fun `SEP-24 deposit complete full with recovery flow`() { + `test sep24 deposit flow`( SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES ) } - /** * 1. incomplete -> request_offchain_funds * 2. pending_user_transfer_start -> notify_offchain_funds_received @@ -190,8 +233,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 5. pending_anchor -> notify_onchain_funds_sent * 6. completed */ - private fun `SEP-24 deposit complete short partial refund flow`() { - `test deposit flow`( + @Test + fun `SEP-24 deposit complete short partial refund flow`() { + `test sep24 deposit flow`( SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPONSES ) @@ -203,13 +247,13 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 3. pending_anchor -> notify_offchain_funds_sent * 4. completed */ - private fun `SEP-24 withdraw complete short flow`() { - `test withdraw flow`( + @Test + fun `SEP-24 withdraw complete short flow`() { + `test sep24 withdraw flow`( SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES ) } - /** * 1. incomplete -> request_onchain_funds * 2. pending_user_transfer_start -> notify_onchain_funds_received @@ -217,8 +261,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 4. pending_external -> notify_offchain_funds_sent * 5. completed */ - private fun `SEP-24 withdraw complete full via pending external`() { - `test withdraw flow`( + @Test + fun `SEP-24 withdraw complete full via pending external`() { + `test sep24 withdraw flow`( SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_RESPONSES ) @@ -231,8 +276,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 4. pending_user_transfer_complete -> notify_offchain_funds_sent * 5. completed */ - private fun `SEP-24 withdraw complete full via pending user`() { - `test withdraw flow`( + @Test + fun `SEP-24 withdraw complete full via pending user`() { + `test sep24 withdraw flow`( SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RESPONSES ) @@ -244,8 +290,9 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 3. pending_anchor -> notify_refund_sent * 4. refunded */ - private fun `SEP-24 withdraw full refund`() { - `test withdraw flow`( + @Test + fun `SEP-24 withdraw full refund`() { + `test sep24 withdraw flow`( SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES ) @@ -256,7 +303,8 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 2. pending_receiver -> notify_refund_sent * 3. refunded */ - private fun `SEP-31 refunded short`() { + @Test + fun `SEP-31 refunded short`() { `test receive flow`( SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_REQUESTS, SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_RESPONSES @@ -273,23 +321,98 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { * 7. pending_external -> notify_offchain_funds_sent * 8. completed */ - private fun `SEP-31 complete full with recovery`() { + @Test + fun `SEP-31 complete full with recovery`() { `test receive flow`( SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS, SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES ) } - private fun `validations and errors`() { - `test deposit flow`(VALIDATIONS_AND_ERRORS_REQUESTS, VALIDATIONS_AND_ERRORS_RESPONSES) + @Test + fun `send single rpc request`() { + val depositRequest = gson.fromJson(SEP_24_DEPOSIT_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val depositResponse = sep24Client.deposit(depositRequest as HashMap) + val txId = depositResponse.id + + val requestOffchainFundsParams = + gson.fromJson(REQUEST_OFFCHAIN_FUNDS_PARAMS, RequestOffchainFundsRequest::class.java) + requestOffchainFundsParams.transactionId = txId + val rpcRequest = + RpcRequest.builder() + .method(REQUEST_OFFCHAIN_FUNDS.toString()) + .jsonrpc(JSON_RPC_VERSION) + .params(requestOffchainFundsParams) + .id(1) + .build() + val response = platformApiClient.sendRpcRequest(listOf(rpcRequest)) + assertEquals(SC_OK, response.code) + JSONAssert.assertEquals( + EXPECTED_RPC_RESPONSE.replace(TX_ID, txId).trimIndent(), + response.body?.string()?.trimIndent(), + CustomComparator( + JSONCompareMode.STRICT, + Customization("[*].result.started_at") { _, _ -> true }, + Customization("[*].result.updated_at") { _, _ -> true } + ) + ) + + val txResponse = platformApiClient.getTransaction(txId) + assertEquals(SepTransactionStatus.PENDING_USR_TRANSFER_START, txResponse.status) } - private fun `test deposit flow`(actionRequests: String, actionResponse: String) { - val depositRequest = gson.fromJson(SEP_24_DEPOSIT_FLOW_REQUEST, HashMap::class.java) + @Test + fun `send batch of rpc requests`() { + val depositRequest = gson.fromJson(SEP_24_DEPOSIT_REQUEST, HashMap::class.java) @Suppress("UNCHECKED_CAST") val depositResponse = sep24Client.deposit(depositRequest as HashMap) - `test flow`(depositResponse.id, actionRequests, actionResponse) + val txId = depositResponse.id + + val requestOffchainFundsParams = + gson.fromJson(REQUEST_OFFCHAIN_FUNDS_PARAMS, RequestOffchainFundsRequest::class.java) + val notifyOffchainFundsReceivedParams = + gson.fromJson( + NOTIFY_OFFCHAIN_FUNDS_RECEIVED_PARAMS, + NotifyOffchainFundsReceivedRequest::class.java + ) + requestOffchainFundsParams.transactionId = txId + notifyOffchainFundsReceivedParams.transactionId = txId + val rpcRequest1 = + RpcRequest.builder() + .id(1) + .method(REQUEST_OFFCHAIN_FUNDS.toString()) + .jsonrpc(JSON_RPC_VERSION) + .params(requestOffchainFundsParams) + .build() + val rpcRequest2 = + RpcRequest.builder() + .id(2) + .method(RpcMethod.NOTIFY_OFFCHAIN_FUNDS_RECEIVED.toString()) + .jsonrpc(JSON_RPC_VERSION) + .params(notifyOffchainFundsReceivedParams) + .build() + val response = platformApiClient.sendRpcRequest(listOf(rpcRequest1, rpcRequest2)) + assertEquals(SC_OK, response.code) + + JSONAssert.assertEquals( + EXPECTED_RPC_BATCH_RESPONSE.replace(TX_ID, txId).trimIndent(), + response.body?.string()?.trimIndent(), + CustomComparator( + JSONCompareMode.STRICT, + Customization("[*].result.started_at") { _, _ -> true }, + Customization("[*].result.updated_at") { _, _ -> true } + ) + ) + + val txResponse = platformApiClient.getTransaction(txId) + assertEquals(SepTransactionStatus.PENDING_ANCHOR, txResponse.status) + } + + private fun `validations and errors`() { + `test sep24 deposit flow`(VALIDATIONS_AND_ERRORS_REQUESTS, VALIDATIONS_AND_ERRORS_RESPONSES) } private fun `test receive flow`(actionRequests: String, actionResponses: String) { @@ -300,59 +423,2541 @@ class PlatformApiTests(config: TestConfig, toml: TomlContent, jwt: String) { GsonUtils.getInstance().fromJson(CUSTOMER_2, Sep12PutCustomerRequest::class.java) val senderCustomer = sep12Client.putCustomer(senderCustomerRequest) - val receiveRequestJson = - SEP_31_RECEIVE_FLOW_REQUEST.replace(RECEIVER_ID_KEY, receiverCustomer!!.id) - .replace(SENDER_ID_KEY, senderCustomer!!.id) - val receiveRequest = gson.fromJson(receiveRequestJson, Sep31PostTransactionRequest::class.java) - val receiveResponse = sep31Client.postTransaction(receiveRequest) + val receiveRequestJson = + SEP_31_RECEIVE_FLOW_REQUEST.replace(RECEIVER_ID_KEY, receiverCustomer!!.id) + .replace(SENDER_ID_KEY, senderCustomer!!.id) + val receiveRequest = gson.fromJson(receiveRequestJson, Sep31PostTransactionRequest::class.java) + val receiveResponse = sep31Client.postTransaction(receiveRequest) + + val updatedActionRequests = + actionRequests + .replace(RECEIVER_ID_KEY, receiverCustomer.id) + .replace(SENDER_ID_KEY, senderCustomer.id) + val updatedActionResponses = + actionResponses + .replace(RECEIVER_ID_KEY, receiverCustomer.id) + .replace(SENDER_ID_KEY, senderCustomer.id) + + `test flow`(receiveResponse.id, updatedActionRequests, updatedActionResponses) + } + + private fun `test sep6 withdraw flow`(actionRequests: String, actionResponse: String) { + val withdrawRequest = gson.fromJson(SEP_6_WITHDRAW_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val withdrawResponse = sep6Client.withdraw(withdrawRequest as HashMap) + `test flow`(withdrawResponse.id, actionRequests, actionResponse) + } + + private fun `test sep6 withdraw-exchange flow`(actionRequests: String, actionResponse: String) { + val withdrawRequest = gson.fromJson(SEP_6_WITHDRAW_EXCHANGE_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val withdrawResponse = + sep6Client.withdraw(withdrawRequest as HashMap, exchange = true) + `test flow`(withdrawResponse.id, actionRequests, actionResponse) + } + + private fun `test sep6 deposit flow`(actionRequests: String, actionResponse: String) { + val depositRequest = gson.fromJson(SEP_6_DEPOSIT_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val depositResponse = sep6Client.deposit(depositRequest as HashMap) + `test flow`(depositResponse.id, actionRequests, actionResponse) + } + + private fun `test sep6 deposit-exchange flow`(actionRequests: String, actionResponse: String) { + val depositRequest = gson.fromJson(SEP_6_DEPOSIT_EXCHANGE_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val depositResponse = + sep6Client.deposit(depositRequest as HashMap, exchange = true) + `test flow`(depositResponse.id, actionRequests, actionResponse) + } + + private fun `test sep24 withdraw flow`(actionRequests: String, actionResponse: String) { + val withdrawRequest = gson.fromJson(SEP_24_WITHDRAW_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val withdrawResponse = sep24Client.withdraw(withdrawRequest as HashMap) + `test flow`(withdrawResponse.id, actionRequests, actionResponse) + } + + private fun `test sep24 deposit flow`(actionRequests: String, actionResponse: String) { + val depositRequest = gson.fromJson(SEP_24_DEPOSIT_FLOW_REQUEST, HashMap::class.java) + + @Suppress("UNCHECKED_CAST") + val depositResponse = sep24Client.deposit(depositRequest as HashMap) + `test flow`(depositResponse.id, actionRequests, actionResponse) + } + + private fun `test flow`(txId: String, actionRequests: String, actionResponses: String) { + val rpcActionRequestsType = object : TypeToken>() {}.type + val rpcActionRequests: List = + gson.fromJson(actionRequests.replace(TX_ID_KEY, txId), rpcActionRequestsType) + + val rpcActionResponses = platformApiClient.sendRpcRequest(rpcActionRequests) + + val expectedResult = actionResponses.replace(TX_ID_KEY, txId).trimIndent() + val actualResult = rpcActionResponses.body?.string()?.trimIndent() + + JSONAssert.assertEquals( + expectedResult, + actualResult, + CustomComparator( + JSONCompareMode.LENIENT, + Customization("[*].result.started_at") { _, _ -> true }, + Customization("[*].result.updated_at") { _, _ -> true }, + Customization("[*].result.completed_at") { _, _ -> true }, + Customization("[*].result.memo") { _, _ -> true }, + Customization("[*].result.stellar_transactions[*].memo") { _, _ -> true } + ) + ) + } + + companion object { + private const val TX_ID = "testTxId" + private const val JSON_RPC_VERSION = "2.0" + private const val TX_ID_KEY = "TX_ID" + private const val RECEIVER_ID_KEY = "RECEIVER_ID" + private const val SENDER_ID_KEY = "SENDER_ID" + + private const val SEP_6_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_offchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "100", + "asset": "iso4217:USD" + }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": "5", + "asset": "iso4217:USD" + }, + "amount_expected": { + "amount": "100" + } + } + }, + { + "id": "2", + "method": "notify_offchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "funds_received_at": "2023-07-04T12:34:56Z", + "external_transaction_id": "ext-123456", + "amount_in": { + "amount": "100" + } + } + }, + { + "id": "3", + "method": "notify_onchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_user_transfer_start", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:42:15.946261Z", + "updated_at": "2023-11-15T21:42:17.021890Z", + "message": "test message 1", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:42:15.946261Z", + "updated_at": "2023-11-15T21:42:18.067475Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 2", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "completed", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:42:15.946261Z", + "updated_at": "2023-11-15T21:42:19.160375Z", + "completed_at": "2023-11-15T21:42:19.160373Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + } + ] + """ + + private const val SEP_6_DEPOSIT_EXCHANGE_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit-exchange", + "status": "pending_user_transfer_start", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-16T21:17:23.259258Z", + "updated_at": "2023-11-16T21:17:24.657547Z", + "message": "test message 1", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit-exchange", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-16T21:17:23.259258Z", + "updated_at": "2023-11-16T21:17:25.712902Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 2", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit-exchange", + "status": "completed", + "type": "SWIFT", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "100", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "5", "asset": "iso4217:USD" }, + "started_at": "2023-11-16T21:17:23.259258Z", + "updated_at": "2023-11-16T21:17:26.934996Z", + "completed_at": "2023-11-16T21:17:26.934991Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_customer_info_update", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "required_customer_info_updates": ["first_name", "last_name"] + } + }, + { + "id": "2", + "method": "request_offchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "amount_in": { + "amount": "10.11", + "asset": "iso4217:USD" + }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": "1.11", + "asset": "iso4217:USD" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "3", + "method": "notify_offchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "funds_received_at": "2023-07-04T12:34:56Z", + "external_transaction_id": "ext-123456", + "amount_in": { + "amount": "10.11" + }, + "amount_out": { + "amount": "9" + }, + "amount_fee": { + "amount": "1.11" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "4", + "method": "request_trust", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 4" + } + }, + { + "id": "5", + "method": "notify_trust_set", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 5" + } + }, + { + "id": "6", + "method": "notify_onchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 6", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_customer_info_update", + "type": "SWIFT", + "amount_expected": { + "amount": "1", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:34.102152Z", + "message": "test message 1", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_user_transfer_start", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:35.126452Z", + "message": "test message 2", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:36.146841Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 3", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "3" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_trust", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:36.165729Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 4", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "4" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:37.180044Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 5", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "5" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "completed", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T21:53:33.071238Z", + "updated_at": "2023-11-15T21:53:38.259639Z", + "completed_at": "2023-11-15T21:53:38.259637Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 6", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + }, + "required_customer_info_updates": ["first_name", "last_name"] + }, + "id": "6" + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_offchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "10.11", + "asset": "iso4217:USD" + }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": "1.11", + "asset": "iso4217:USD" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "2", + "method": "notify_offchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "funds_received_at": "2023-07-04T12:34:56Z", + "external_transaction_id": "ext-123456", + "amount_in": { + "amount": "10.11" + }, + "amount_out": { + "amount": "9" + }, + "amount_fee": { + "amount": "1.11" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "3", + "method": "notify_transaction_error", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3" + } + }, + { + "id": "4", + "method": "notify_transaction_recovery", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 4" + } + }, + { + "id": "5", + "method": "notify_onchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 5", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_user_transfer_start", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T22:02:41.822662Z", + "updated_at": "2023-11-15T22:02:43.023879Z", + "message": "test message 1", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T22:02:41.822662Z", + "updated_at": "2023-11-15T22:02:44.098221Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 2", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "error", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T22:02:41.822662Z", + "updated_at": "2023-11-15T22:02:45.177536Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 3", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T22:02:41.822662Z", + "updated_at": "2023-11-15T22:02:46.237378Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 4", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "4" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "completed", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T22:02:41.822662Z", + "updated_at": "2023-11-15T22:02:47.506156Z", + "completed_at": "2023-11-15T22:02:47.506151Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 5", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "5" + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_offchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "10.11", + "asset": "iso4217:USD" + }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": "1.11", + "asset": "iso4217:USD" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "2", + "method": "notify_offchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "funds_received_at": "2023-07-04T12:34:56Z", + "external_transaction_id": "ext-123456", + "amount_in": { + "amount": "1000.11" + }, + "amount_out": { + "amount": "9" + }, + "amount_fee": { + "amount": "1.11" + }, + "amount_expected": { + "amount": "10.11" + } + } + }, + { + "id": "3", + "method": "notify_refund_pending", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "refund": { + "id": "123456", + "amount": { + "amount": "989.11", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "1", + "asset": "iso4217:USD" + } + } + } + }, + { + "id": "4", + "method": "notify_refund_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 4", + "refund": { + "id": "123456", + "amount": { + "amount": "989.11", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "1", + "asset": "iso4217:USD" + } + } + } + }, + { + "id": "5", + "method": "notify_onchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 5", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + } + ] + """ + + private const val SEP_6_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_user_transfer_start", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "10.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T23:15:21.331844Z", + "updated_at": "2023-11-15T23:15:22.533295Z", + "message": "test message 1", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "1000.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T23:15:21.331844Z", + "updated_at": "2023-11-15T23:15:23.614615Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 2", + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_external", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "1000.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T23:15:21.331844Z", + "updated_at": "2023-11-15T23:15:24.715809Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 3", + "refunds": { + "amount_refunded": { "amount": "990.11", "asset": "iso4217:USD" }, + "amount_fee": { "amount": "1", "asset": "iso4217:USD" }, + "payments": [ + { + "id": "123456", + "id_type": "external", + "amount": { "amount": "989.11", "asset": "iso4217:USD" }, + "fee": { "amount": "1", "asset": "iso4217:USD" } + } + ] + }, + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "pending_anchor", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "1000.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T23:15:21.331844Z", + "updated_at": "2023-11-15T23:15:25.787787Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 4", + "refunds": { + "amount_refunded": { "amount": "990.11", "asset": "iso4217:USD" }, + "amount_fee": { "amount": "1", "asset": "iso4217:USD" }, + "payments": [ + { + "id": "123456", + "id_type": "external", + "amount": { "amount": "989.11", "asset": "iso4217:USD" }, + "fee": { "amount": "1", "asset": "iso4217:USD" } + } + ] + }, + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "4" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "deposit", + "status": "completed", + "type": "SWIFT", + "amount_expected": { + "amount": "10.11", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { "amount": "1000.11", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "9", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { "amount": "1.11", "asset": "iso4217:USD" }, + "started_at": "2023-11-15T23:15:21.331844Z", + "updated_at": "2023-11-15T23:15:27.000415Z", + "completed_at": "2023-11-15T23:15:27.000411Z", + "transfer_received_at": "2023-07-04T12:34:56Z", + "message": "test message 5", + "refunds": { + "amount_refunded": { "amount": "990.11", "asset": "iso4217:USD" }, + "amount_fee": { "amount": "1", "asset": "iso4217:USD" }, + "payments": [ + { + "id": "123456", + "id_type": "external", + "amount": { "amount": "989.11", "asset": "iso4217:USD" }, + "fee": { "amount": "1", "asset": "iso4217:USD" } + } + ] + }, + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "5" + } + ] + """ - val updatedActionRequests = - actionRequests - .replace(RECEIVER_ID_KEY, receiverCustomer.id) - .replace(SENDER_ID_KEY, senderCustomer.id) - val updatedActionResponses = - actionResponses - .replace(RECEIVER_ID_KEY, receiverCustomer.id) - .replace(SENDER_ID_KEY, senderCustomer.id) + private const val SEP_6_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_onchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { + "amount": "95", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_expected": { + "amount": "100" + } + } + }, + { + "id": "2", + "method": "notify_onchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + }, + { + "id": "3", + "method": "notify_offchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "external_transaction_id": "ext-123456" + } + } + ] + """ - `test flow`(receiveResponse.id, updatedActionRequests, updatedActionResponses) - } + private const val SEP_6_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_user_transfer_start", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:47:17.787600Z", + "updated_at": "2023-11-15T22:47:19.027317Z", + "message": "test message 1", + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "MmI1MjMzZDMtNzRmNS00ZjhiLTg5NGQtMWIwYTRiNzI\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_anchor", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:47:17.787600Z", + "updated_at": "2023-11-15T22:47:20.258606Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 2", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "MmI1MjMzZDMtNzRmNS00ZjhiLTg5NGQtMWIwYTRiNzI\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "completed", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:47:17.787600Z", + "updated_at": "2023-11-15T22:47:21.386785Z", + "completed_at": "2023-11-15T22:47:21.386780Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "MmI1MjMzZDMtNzRmNS00ZjhiLTg5NGQtMWIwYTRiNzI\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + } + ] + """ - private fun `test withdraw flow`(actionRequests: String, actionResponse: String) { - val withdrawRequest = gson.fromJson(SEP_24_WITHDRAW_FLOW_REQUEST, HashMap::class.java) + private const val SEP_6_WITHDRAW_EXCHANGE_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal-exchange", + "status": "pending_user_transfer_start", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-16T21:18:15.453486Z", + "updated_at": "2023-11-16T21:18:16.521002Z", + "message": "test message 1", + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "NjA0OTZjODgtNjc3ZC00ZmM2LThkYTktODQ2YWFhOWY\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal-exchange", + "status": "pending_anchor", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-16T21:18:15.453486Z", + "updated_at": "2023-11-16T21:18:17.599527Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 2", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "NjA0OTZjODgtNjc3ZC00ZmM2LThkYTktODQ2YWFhOWY\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal-exchange", + "status": "completed", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-16T21:18:15.453486Z", + "updated_at": "2023-11-16T21:18:18.643487Z", + "completed_at": "2023-11-16T21:18:18.643485Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "NjA0OTZjODgtNjc3ZC00ZmM2LThkYTktODQ2YWFhOWY\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + } + ] + """ - @Suppress("UNCHECKED_CAST") - val withdrawResponse = sep24Client.withdraw(withdrawRequest as HashMap) - `test flow`(withdrawResponse.id, actionRequests, actionResponse) - } + private const val SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_onchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { + "amount": "95", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_expected": { + "amount": "100" + } + } + }, + { + "id": "2", + "method": "notify_onchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + }, + { + "id": "3", + "method": "notify_offchain_funds_pending", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "external_transaction_id": "ext-123456" + } + }, + { + "id": "4", + "method": "notify_offchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 4", + "external_transaction_id": "ext-123456" + } + } + ] + """ - private fun `test flow`(txId: String, actionRequests: String, actionResponses: String) { - val rpcActionRequestsType = object : TypeToken>() {}.type - val rpcActionRequests: List = - gson.fromJson(actionRequests.replace(TX_ID_KEY, txId), rpcActionRequestsType) + private const val SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_user_transfer_start", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:00:49.250465Z", + "updated_at": "2023-11-15T23:00:50.293966Z", + "message": "test message 1", + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "NGQ1YWM0MzYtZTFiZi00YTc0LThhZWMtNTVmZGJmM2E\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_anchor", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:00:49.250465Z", + "updated_at": "2023-11-15T23:00:51.344410Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 2", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "NGQ1YWM0MzYtZTFiZi00YTc0LThhZWMtNTVmZGJmM2E\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_external", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:00:49.250465Z", + "updated_at": "2023-11-15T23:00:52.367691Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "NGQ1YWM0MzYtZTFiZi00YTc0LThhZWMtNTVmZGJmM2E\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "completed", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:00:49.250465Z", + "updated_at": "2023-11-15T23:00:53.387796Z", + "completed_at": "2023-11-15T23:00:53.387794Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 4", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "NGQ1YWM0MzYtZTFiZi00YTc0LThhZWMtNTVmZGJmM2E\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "4" + } + ] + """ - val rpcActionResponses = platformApiClient.sendRpcRequest(rpcActionRequests) + private const val SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_onchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { + "amount": "95", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_expected": { + "amount": "100" + } + } + }, + { + "id": "2", + "method": "notify_onchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + }, + { + "id": "3", + "method": "notify_offchain_funds_available", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "external_transaction_id": "ext-123456" + } + }, + { + "id": "4", + "method": "notify_offchain_funds_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 4", + "external_transaction_id": "ext-123456" + } + } + ] + """ - val expectedResult = actionResponses.replace(TX_ID_KEY, txId).trimIndent() - val actualResult = rpcActionResponses.body?.string()?.trimIndent() + private const val SEP_6_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_user_transfer_start", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:56:01.090169Z", + "updated_at": "2023-11-15T22:56:02.125785Z", + "message": "test message 1", + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "ZDQ1NjlkMWQtNzU5NC00YzgxLWIzYjAtYzMyMmM2Y2M\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_anchor", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:56:01.090169Z", + "updated_at": "2023-11-15T22:56:03.177061Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 2", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "ZDQ1NjlkMWQtNzU5NC00YzgxLWIzYjAtYzMyMmM2Y2M\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_user_transfer_complete", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:56:01.090169Z", + "updated_at": "2023-11-15T22:56:04.197512Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 3", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "ZDQ1NjlkMWQtNzU5NC00YzgxLWIzYjAtYzMyMmM2Y2M\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "completed", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T22:56:01.090169Z", + "updated_at": "2023-11-15T22:56:05.214808Z", + "completed_at": "2023-11-15T22:56:05.214806Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 4", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "external_transaction_id": "ext-123456", + "memo": "ZDQ1NjlkMWQtNzU5NC00YzgxLWIzYjAtYzMyMmM2Y2M\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "4" + } + ] + """ - JSONAssert.assertEquals( - expectedResult, - actualResult, - CustomComparator( - JSONCompareMode.STRICT, - Customization("[*].result.started_at") { _, _ -> true }, - Customization("[*].result.updated_at") { _, _ -> true }, - Customization("[*].result.completed_at") { _, _ -> true }, - Customization("[*].result.memo") { _, _ -> true }, - Customization("[*].result.stellar_transactions[*].memo") { _, _ -> true } - ) - ) - } -} + private const val SEP_6_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS = + """ + [ + { + "id": "1", + "method": "request_onchain_funds", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 1", + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { + "amount": "95", + "asset": "iso4217:USD" + }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_expected": { + "amount": "100" + } + } + }, + { + "id": "2", + "method": "notify_onchain_funds_received", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 2", + "stellar_transaction_id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1" + } + }, + { + "id": "3", + "method": "notify_refund_sent", + "jsonrpc": "2.0", + "params": { + "transaction_id": "TX_ID", + "message": "test message 3", + "refund": { + "id": "123456", + "amount": { + "amount": 95, + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": 5, + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + } + } + } + } + ] + """ -private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = - """ + private const val SEP_6_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = + """ + [ + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_user_transfer_start", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:17:41.318025Z", + "updated_at": "2023-11-15T23:17:42.367100Z", + "message": "test message 1", + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "OTBkMjAyYjUtNjk5Zi00MDNhLWFhZDQtZjI1YzdiZDg\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "1" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "pending_anchor", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:17:41.318025Z", + "updated_at": "2023-11-15T23:17:43.576954Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 2", + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "OTBkMjAyYjUtNjk5Zi00MDNhLWFhZDQtZjI1YzdiZDg\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "2" + }, + { + "jsonrpc": "2.0", + "result": { + "id": "TX_ID", + "sep": "6", + "kind": "withdrawal", + "status": "refunded", + "type": "bank_account", + "amount_expected": { + "amount": "100", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_in": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_out": { "amount": "95", "asset": "iso4217:USD" }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "started_at": "2023-11-15T23:17:41.318025Z", + "updated_at": "2023-11-15T23:17:44.626373Z", + "completed_at": "2023-11-15T23:17:44.626370Z", + "transfer_received_at": "2023-06-22T08:46:56Z", + "message": "test message 3", + "refunds": { + "amount_refunded": { + "amount": "100", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "amount_fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payments": [ + { + "id": "123456", + "id_type": "stellar", + "amount": { + "amount": "95", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "fee": { + "amount": "5", + "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + } + } + ] + }, + "stellar_transactions": [ + { + "id": "fba01f815acfe1f493271017f02929e97e30656ba57a5ac8f3d1356dd4926ea1", + "created_at": "2023-06-22T08:46:56Z", + "envelope": "AAAAAgAAAABBsSNsYI9mqhg2INua8oEzk88ixjqc/Yiq0/4MNDIcAwAPQkAAAcGcAAAACAAAAAEAAAAAIHqjOgAAAABklDSfAAAAAAAAAAEAAAAAAAAAAQAAAAC9yF4ErTewnyaxhbV7fzgFiY7A8k7xt62CIhYMXt/ovgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAAAAKupUAAAAAAAAAAATQyHAMAAABAUjCaXkOy4VHDpkVwG42lF7ZKK471bMsKSjP2EZtYnBo4e/kYtcVNp+z15EX/qHZBvGWtbFiCBBLXQs7hmu15Cg\u003d\u003d", + "payments": [ + { + "id": "563306435719169", + "amount": { + "amount": "4.5000000", + "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + }, + "payment_type": "payment", + "source_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", + "destination_account": "GC64QXQEVU33BHZGWGC3K637HACYTDWA6JHPDN5NQIRBMDC637UL4F2W" + } + ] + } + ], + "source_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "memo": "OTBkMjAyYjUtNjk5Zi00MDNhLWFhZDQtZjI1YzdiZDg\u003d", + "memo_type": "hash", + "customers": { + "sender": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "receiver": { + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + } + } + }, + "id": "3" + } + ] + """ + + private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -405,8 +3010,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = ] """ -private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -523,8 +3128,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = ] """ -private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -628,8 +3233,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_REQUESTS = ] """ -private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -838,8 +3443,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_TRUST_FLOW_ACTION_RESPONSES ] """ -private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -919,8 +3524,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUEST ] """ -private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -1099,8 +3704,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONS ] """ -private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -1202,8 +3807,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_REQUE ] """ -private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -1454,8 +4059,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_SHORT_PARTIAL_REFUND_FLOW_ACTION_RESPO ] """ -private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -1504,8 +4109,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_REQUESTS = ] """ -private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -1653,8 +4258,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_SHORT_FLOW_ACTION_RESPONSES = ] """ -private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -1713,8 +4318,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION ] """ -private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -1917,8 +4522,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_EXTERNAL_FLOW_ACTION ] """ -private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -2121,8 +4726,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_RES ] """ -private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -2181,8 +4786,8 @@ private const val SEP_24_WITHDRAW_COMPLETE_FULL_VIA_PENDING_USER_FLOW_ACTION_REQ ] """ -private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS = - """ + private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -2241,8 +4846,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS = ] """ -private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = - """ + private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -2413,8 +5018,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = ] """ -private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_REQUESTS = - """ + private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -2449,8 +5054,8 @@ private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_REQUESTS = ] """ -private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_RESPONSES = - """ + private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -2597,8 +5202,8 @@ private const val SEP_31_RECEIVE_REFUNDED_SHORT_FLOW_ACTION_RESPONSES = ] """ -private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS = - """ + private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUESTS = + """ [ { "id": "1", @@ -2669,8 +5274,8 @@ private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_REQUEST ] """ -private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES = - """ + private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -2899,7 +5504,7 @@ private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONS "id": "SENDER_ID" }, "receiver": { - "id": "SENDER_ID" + "id": "RECEIVER_ID" } }, "creator": { @@ -3091,8 +5696,8 @@ private const val SEP_31_RECEIVE_COMPLETE_FULL_WITH_RECOVERY_FLOW_ACTION_RESPONS ] """ -private const val VALIDATIONS_AND_ERRORS_REQUESTS = - """ + private const val VALIDATIONS_AND_ERRORS_REQUESTS = + """ [ { "id": "1", @@ -3524,8 +6129,8 @@ private const val VALIDATIONS_AND_ERRORS_REQUESTS = ] """ -private const val VALIDATIONS_AND_ERRORS_RESPONSES = - """ + private const val VALIDATIONS_AND_ERRORS_RESPONSES = + """ [ { "jsonrpc": "2.0", @@ -3814,15 +6419,54 @@ private const val VALIDATIONS_AND_ERRORS_RESPONSES = } ] """ + private const val SEP_6_WITHDRAW_FLOW_REQUEST = + """ + { + "asset_code": "USDC", + "type": "bank_account", + "amount": "1" + } + """ + + private const val SEP_6_WITHDRAW_EXCHANGE_FLOW_REQUEST = + """ + { + "destination_asset": "iso4217:USD", + "source_asset": "USDC", + "amount": "1", + "type": "bank_account" + } + """ + + private const val SEP_6_DEPOSIT_FLOW_REQUEST = + """ + { + "asset_code": "USDC", + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "amount": "1", + "type": "SWIFT" + } + """ + + private const val SEP_6_DEPOSIT_EXCHANGE_FLOW_REQUEST = + """ + { + "destination_asset": "USDC", + "source_asset": "iso4217:USD", + "amount": "1", + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "type": "SWIFT" + } + """ -private const val SEP_24_DEPOSIT_FLOW_REQUEST = """ + private const val SEP_24_DEPOSIT_FLOW_REQUEST = """ { "asset_code": "USDC" } """ -private const val SEP_24_WITHDRAW_FLOW_REQUEST = - """{ + private const val SEP_24_WITHDRAW_FLOW_REQUEST = + """{ "amount": "10", "asset_code": "USDC", "asset_issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", @@ -3830,8 +6474,8 @@ private const val SEP_24_WITHDRAW_FLOW_REQUEST = "lang": "en" }""" -private const val SEP_31_RECEIVE_FLOW_REQUEST = - """ + private const val SEP_31_RECEIVE_FLOW_REQUEST = + """ { "amount": "10", "asset_code": "USDC", @@ -3848,15 +6492,15 @@ private const val SEP_31_RECEIVE_FLOW_REQUEST = } """ -private const val SEP_24_DEPOSIT_REQUEST = - """{ + private const val SEP_24_DEPOSIT_REQUEST = + """{ "asset_code": "USDC", "asset_issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", "lang": "en" }""" -private const val REQUEST_OFFCHAIN_FUNDS_PARAMS = - """{ + private const val REQUEST_OFFCHAIN_FUNDS_PARAMS = + """{ "transaction_id": "testTxId", "message": "test message", "amount_in": { @@ -3876,8 +6520,8 @@ private const val REQUEST_OFFCHAIN_FUNDS_PARAMS = } }""" -private const val NOTIFY_OFFCHAIN_FUNDS_RECEIVED_PARAMS = - """{ + private const val NOTIFY_OFFCHAIN_FUNDS_RECEIVED_PARAMS = + """{ "transaction_id": "testTxId", "message": "test message", "amount_in": { @@ -3892,8 +6536,8 @@ private const val NOTIFY_OFFCHAIN_FUNDS_RECEIVED_PARAMS = "external_transaction_id": "1" }""" -private const val EXPECTED_RPC_RESPONSE = - """ + private const val EXPECTED_RPC_RESPONSE = + """ [ { "jsonrpc":"2.0", @@ -3921,15 +6565,16 @@ private const val EXPECTED_RPC_RESPONSE = "started_at":"2023-07-20T08:57:05.380736Z", "updated_at":"2023-07-20T08:57:16.672110400Z", "message":"test message", - "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" }, "id":1 } ] """ -private const val EXPECTED_RPC_BATCH_RESPONSE = - """ + private const val EXPECTED_RPC_BATCH_RESPONSE = + """ [ { "jsonrpc":"2.0", @@ -3957,7 +6602,8 @@ private const val EXPECTED_RPC_BATCH_RESPONSE = "started_at":"2023-07-20T09:07:51.007629Z", "updated_at":"2023-07-20T09:07:59.425534900Z", "message":"test message", - "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" }, "id":1 }, @@ -3988,15 +6634,16 @@ private const val EXPECTED_RPC_BATCH_RESPONSE = "updated_at":"2023-07-20T09:07:59.448888600Z", "message":"test message", "external_transaction_id": "1", - "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account":"GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" }, "id":2 } ] """ -private const val CUSTOMER_1 = - """ + private const val CUSTOMER_1 = + """ { "first_name": "John", "last_name": "Doe", @@ -4012,8 +6659,8 @@ private const val CUSTOMER_1 = } """ -private const val CUSTOMER_2 = - """ + private const val CUSTOMER_2 = + """ { "first_name": "Jane", "last_name": "Doe", @@ -4028,3 +6675,5 @@ private const val CUSTOMER_2 = "bank_account_type": "checking" } """ + } +} diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ReferenceServerTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ReferenceServerTests.kt new file mode 100644 index 0000000000..59864af7d5 --- /dev/null +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ReferenceServerTests.kt @@ -0,0 +1,3 @@ +package org.stellar.anchor.platform.integrationtest + +// The test is removed in favor of Sep24Tests.kt diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep10Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep10Tests.kt similarity index 79% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep10Tests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep10Tests.kt index 20b74ead5b..c5f5e8dae5 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep10Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep10Tests.kt @@ -1,12 +1,13 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import kotlin.test.assertFailsWith +import org.junit.jupiter.api.Test import org.stellar.anchor.api.exception.SepNotAuthorizedException import org.stellar.anchor.api.sep.sep10.ValidationRequest +import org.stellar.anchor.client.Sep10Client import org.stellar.anchor.platform.* -import org.stellar.anchor.util.Sep1Helper.TomlContent -class Sep10Tests(val toml: TomlContent) { +class Sep10Tests : AbstractIntegrationTests(TestConfig()) { lateinit var sep10Client: Sep10Client lateinit var sep10ClientMultiSig: Sep10Client @@ -35,14 +36,17 @@ class Sep10Tests(val toml: TomlContent) { } } - fun testMultiSig() { - sep10ClientMultiSig.auth() + @Test + fun testAuth() { + sep10Client.auth() } - fun testOk(): String { - return sep10Client.auth() + @Test + fun testMultiSig() { + sep10ClientMultiSig.auth() } + @Test fun testUnsignedChallenge() { val challenge = sep10Client.challenge() @@ -51,12 +55,4 @@ class Sep10Tests(val toml: TomlContent) { block = { sep10Client.validate(ValidationRequest.of(challenge.transaction)) } ) } - - fun testAll() { - println("Performing SEP10 tests...") - - testOk() - testUnsignedChallenge() - testMultiSig() - } } diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep12Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep12Tests.kt new file mode 100644 index 0000000000..dd3f7ff0b9 --- /dev/null +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep12Tests.kt @@ -0,0 +1,123 @@ +package org.stellar.anchor.platform.integrationtest + +import io.ktor.client.plugins.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.stellar.anchor.api.sep.sep12.Sep12Status +import org.stellar.anchor.platform.AbstractIntegrationTests +import org.stellar.anchor.platform.TestConfig +import org.stellar.anchor.platform.printRequest +import org.stellar.anchor.platform.printResponse +import org.stellar.sdk.KeyPair +import org.stellar.walletsdk.anchor.auth +import org.stellar.walletsdk.horizon.SigningKeyPair + +open class Sep12Tests : AbstractIntegrationTests(TestConfig()) { + init { + runBlocking { + // We have to override the default CLIENT_WALLET_SECRET because the deletion of the customer + // will fail other tests that uses the same wallet. + walletKeyPair = SigningKeyPair(KeyPair.random()) + token = anchor.auth().authenticate(walletKeyPair) + } + } + + @Test + fun `test put, get customers`() = runBlocking { + customerJson + val customer = Json.decodeFromString>(customerJson).toMutableMap() + customer.remove("emailAddress") + + // Upload a customer + printRequest("Calling PUT /customer", customer) + var pr = anchor.sep12(token).add(customer, type = "sep24") + printResponse(pr) + + // make sure the customer was uploaded correctly. + printRequest("Calling GET /customer", customer) + var gr = anchor.sep12(token).get(pr.id, type = "sep24") + printResponse(gr) + + assertEquals(pr.id, gr.id) + + customer["emailAddress"] = "john.doe@stellar.org" + + // Modify the customer + printRequest("Calling PUT /customer", customer) + pr = anchor.sep12(token).add(customer, type = "sep31-receiver") + printResponse(pr) + + // Make sure the customer is modified correctly. + printRequest("Calling GET /customer", customer) + gr = anchor.sep12(token).get(pr.id, type = "sep31-receiver") + printResponse(gr) + + assertEquals(pr.id, gr.id) + assertEquals(Sep12Status.ACCEPTED.name, gr.status!!.status) + + // Delete the customer + printRequest("Calling DELETE /customer/$walletKeyPair.address") + anchor.sep12(token).delete(walletKeyPair.address) + + val ex: ClientRequestException = assertThrows { + anchor.sep12(token).get(pr.id, type = "sep31-receiver") + } + println(ex) + } + + companion object { + const val customerJson = + """ +{ + "first_name": "John", + "last_name": "Doe", + "email_address": "johndoe@test.com", + "address": "123 Washington Street", + "city": "San Francisco", + "state_or_province": "CA", + "address_country_code": "US", + "clabe_number": "1234", + "bank_number": "abcd", + "bank_account_number": "1234", + "bank_account_type": "checking" +} +""" + + const val testCustomer1Json = + """ +{ + "first_name": "John", + "last_name": "Doe", + "email_address": "johndoe@test.com", + "address": "123 Washington Street", + "city": "San Francisco", + "state_or_province": "CA", + "address_country_code": "US", + "clabe_number": "1234", + "bank_number": "abcd", + "bank_account_number": "1234", + "bank_account_type": "checking" +} +""" + + const val testCustomer2Json = + """ +{ + "first_name": "Jane", + "last_name": "Doe", + "email_address": "janedoe@test.com", + "address": "321 Washington Street", + "city": "San Francisco", + "state_or_province": "CA", + "address_country_code": "US", + "clabe_number": "5678", + "bank_number": "efgh", + "bank_account_number": "5678", + "bank_account_type": "checking" +} +""" + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt similarity index 50% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24Tests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt index 2ffda78a9d..754a3a23d4 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt @@ -1,29 +1,42 @@ -@file:Suppress("UNCHECKED_CAST") +package org.stellar.anchor.platform.integrationtest -package org.stellar.anchor.platform.test - -import kotlin.test.assertNotNull +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.MethodOrderer.* +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD import org.skyscreamer.jsonassert.JSONAssert -import org.skyscreamer.jsonassert.JSONCompareMode.LENIENT +import org.skyscreamer.jsonassert.JSONCompareMode import org.springframework.web.util.UriComponentsBuilder import org.stellar.anchor.api.exception.SepException import org.stellar.anchor.api.platform.PatchTransactionsRequest -import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.Sep24InteractiveUrlJwt import org.stellar.anchor.auth.Sep24MoreInfoUrlJwt -import org.stellar.anchor.platform.* -import org.stellar.anchor.util.Sep1Helper.* +import org.stellar.anchor.platform.AbstractIntegrationTests +import org.stellar.anchor.platform.TestConfig +import org.stellar.anchor.platform.gson +import org.stellar.anchor.platform.printRequest +import org.stellar.anchor.platform.printResponse +import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.StringHelper.json +import org.stellar.walletsdk.anchor.IncompleteDepositTransaction +import org.stellar.walletsdk.anchor.IncompleteWithdrawalTransaction +import org.stellar.walletsdk.asset.IssuedAssetId -lateinit var savedWithdrawTxn: Sep24GetTransactionResponse -lateinit var savedDepositTxn: Sep24GetTransactionResponse - -class Sep24Tests(val config: TestConfig, val toml: TomlContent, jwt: String) { +// The tests must be executed in order. Currency is disabled. +// Some of the tests depend on the result of previous tests. The lifecycle must be PER_CLASS +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(SAME_THREAD) +@TestMethodOrder(OrderAnnotation::class) +class Sep24Tests : AbstractIntegrationTests(TestConfig()) { private val jwtService: JwtService = JwtService( config.env["secret.sep10.jwt_secret"]!!, @@ -36,22 +49,48 @@ class Sep24Tests(val config: TestConfig, val toml: TomlContent, jwt: String) { private val platformApiClient = PlatformApiClient(AuthHelper.forNone(), config.env["platform.server.url"]!!) - private val sep24Client = Sep24Client(toml.getString("TRANSFER_SERVER_SEP0024"), jwt) - private fun `test Sep24 info endpoint`() { + private lateinit var savedWithdrawTxn: IncompleteWithdrawalTransaction + private lateinit var savedDepositTxn: IncompleteDepositTransaction + + @Test + @Order(10) + fun `test Sep24 info endpoint`() = runBlocking { printRequest("Calling GET /info") - val info = sep24Client.getInfo() - JSONAssert.assertEquals(expectedSep24Info, gson.toJson(info), LENIENT) + val info = anchor.sep24().getServicesInfo() + println(gson.toJson(info)) + JSONAssert.assertEquals(expectedSep24Info, gson.toJson(info), JSONCompareMode.LENIENT) } - private fun `test Sep24 withdraw`() { + @Test + @Order(20) + fun `test Sep24 withdraw`() = runBlocking { printRequest("POST /transactions/withdraw/interactive") - val withdrawRequest = gson.fromJson(withdrawRequest, HashMap::class.java) - val response = sep24Client.withdraw(withdrawRequest as HashMap) - printResponse("POST /transactions/withdraw/interactive response:", response) - savedWithdrawTxn = sep24Client.getTransaction(response.id, "USDC") - printResponse(savedWithdrawTxn) - JSONAssert.assertEquals(expectedSep24WithdrawResponse, json(savedWithdrawTxn), LENIENT) + val withdrawRequest: HashMap = + gson.fromJson(withdrawRequest, object : TypeToken>() {}.type) + println(withdrawRequest) + val response = + anchor + .sep24() + .withdraw( + IssuedAssetId("USDC", "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"), + token, + withdrawRequest, + "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4" + ) + printResponse( + "POST /transactions/withdraw/interactive response:", + Json.encodeToString(response) + ) + savedWithdrawTxn = + anchor.sep24().getTransaction(response.id, token) as IncompleteWithdrawalTransaction + assertEquals(response.id, savedWithdrawTxn.id) + assertNotNull(savedWithdrawTxn.moreInfoUrl) + assertEquals("INCOMPLETE", savedWithdrawTxn.status.name) + assertEquals( + "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", + savedWithdrawTxn.from?.address + ) // check the returning Sep24InteractiveUrlJwt val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams val cipher = params["token"]!![0] @@ -59,48 +98,97 @@ class Sep24Tests(val config: TestConfig, val toml: TomlContent, jwt: String) { assertEquals(response.id, jwt.jti) } - private fun `test Sep24 deposit`() { + @Test + @Order(30) + fun `test Sep24 deposit`() = runBlocking { printRequest("POST /transactions/withdraw/interactive") - val depositRequest = gson.fromJson(depositRequest, HashMap::class.java) - val response = sep24Client.deposit(depositRequest as HashMap) - printResponse("POST /transactions/deposit/interactive response:", response) - savedDepositTxn = sep24Client.getTransaction(response.id, "USDC") - printResponse(savedDepositTxn) - JSONAssert.assertEquals(expectedSep24DepositResponse, json(savedDepositTxn), LENIENT) + val depositRequest = GsonUtils.fromJsonToMap(depositRequestJson) + val response = + anchor + .sep24() + .deposit( + IssuedAssetId(depositRequest["asset_code"]!!, depositRequest["asset_issuer"]!!), + token, + depositRequest as HashMap + ) + printResponse("POST /transactions/deposit/interactive response:", Json.encodeToString(response)) + savedDepositTxn = + anchor.sep24().getTransaction(response.id, token) as IncompleteDepositTransaction + printResponse(Json.encodeToString(savedDepositTxn)) + assertEquals(savedDepositTxn.id, response.id) + assertNotNull(savedDepositTxn.moreInfoUrl) + assertEquals("INCOMPLETE", savedDepositTxn.status.name) + assertEquals( + "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + savedDepositTxn.to?.address + ) // check the returning Sep24InteractiveUrlJwt val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams val cipher = params["token"]!![0] val jwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) assertEquals(response.id, jwt.jti) assertNotNull(jwt.claims["data"]) - assertNotNull((jwt.claims["data"] as HashMap)["asset"]) + assertNotNull((jwt.claims["data"] as HashMap<*, *>)["asset"]) } - private fun `test Sep24 deposit no issuer`() { - printRequest("POST /transactions/withdraw/interactive") - val depositRequest = gson.fromJson(depositRequestNoIssuer, HashMap::class.java) - val response = sep24Client.deposit(depositRequest as HashMap) - printResponse("POST /transactions/deposit/interactive response:", response) - savedDepositTxn = sep24Client.getTransaction(response.id, "USDC") - printResponse(savedDepositTxn) - JSONAssert.assertEquals(expectedSep24DepositResponse, json(savedDepositTxn), LENIENT) - // check the returning Sep24InteractiveUrlJwt - val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams - val cipher = params["token"]!![0] - val jwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) - assertEquals(response.id, jwt.jti) - assertNotNull(jwt.claims["data"]) - assertNotNull((jwt.claims["data"] as HashMap)["asset"]) - } + /* + The following test case is not supported by the wallet sdk. It is commented out until a proper solution is found. + + private val depositRequestNoIssuerJson = + """{ + "amount": "10", + "asset_code": "USDC", + "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "lang": "en" + }""" - private fun `test Sep24 GET transaction and check the JWT`() { + data class AssetIdNoIssuer(val code: String) : StellarAssetId { + override val id = "$code" + override fun toString() = sep38 + } + @Order(40) + private fun `test Sep24 deposit no issuer`() = runBlocking { + printRequest("POST /transactions/withdraw/interactive") + val depositRequest = GsonUtils.fromJsonToMap(depositRequestNoIssuerJson) + val response = + anchor + .sep24() + .deposit( + AssetIdNoIssuer(depositRequest["asset_code"]!!), + token, + depositRequest as HashMap + ) + printResponse("POST /transactions/deposit/interactive response:", response) + savedDepositTxn = + anchor.sep24().getTransaction(response.id, token) as IncompleteDepositTransaction + printResponse(savedDepositTxn) + assertEquals(savedDepositTxn.id, response.id) + assertNotNull(savedDepositTxn.moreInfoUrl) + assertEquals("INCOMPLETE", savedDepositTxn.status.name) + assertEquals( + "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + savedDepositTxn.to?.address + ) + // check the returning Sep24InteractiveUrlJwt + val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams + val cipher = params["token"]!![0] + val jwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) + assertEquals(response.id, jwt.jti) + assertNotNull(jwt.claims["data"]) + assertNotNull((jwt.claims["data"] as HashMap)["asset"]) + } + */ + @Test + @Order(50) + fun `test Sep24 GET transaction and check the JWT`() = runBlocking { val txn = - sep24Client - .getTransaction( - savedDepositTxn.transaction.id, + anchor + .sep24() + .getTransactionBy( + token, + savedDepositTxn.id, "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" ) - .transaction val params = UriComponentsBuilder.fromUriString(txn.moreInfoUrl).build().queryParams val cipher = params["token"]!![0] @@ -108,63 +196,79 @@ class Sep24Tests(val config: TestConfig, val toml: TomlContent, jwt: String) { assertEquals(txn.id, jwt.jti) } - private fun `test PlatformAPI GET transaction for deposit and withdrawal`() { - val actualWithdrawTxn = platformApiClient.getTransaction(savedWithdrawTxn.transaction.id) - assertEquals(actualWithdrawTxn.id, savedWithdrawTxn.transaction.id) - println(expectedWithdrawTransactionResponse) - println(json(actualWithdrawTxn)) - JSONAssert.assertEquals(expectedWithdrawTransactionResponse, json(actualWithdrawTxn), LENIENT) + @Test + @Order(60) + fun `test PlatformAPI GET transaction for deposit and withdrawal`() { + val actualWithdrawTxn = platformApiClient.getTransaction(savedWithdrawTxn.id) + assertEquals(actualWithdrawTxn.id, savedWithdrawTxn.id) + JSONAssert.assertEquals( + expectedWithdrawTransactionResponse, + json(actualWithdrawTxn), + JSONCompareMode.LENIENT + ) - val actualDepositTxn = platformApiClient.getTransaction(savedDepositTxn.transaction.id) + val actualDepositTxn = platformApiClient.getTransaction(savedDepositTxn.id) printResponse(actualDepositTxn) - assertEquals(actualDepositTxn.id, savedDepositTxn.transaction.id) - JSONAssert.assertEquals(expectedDepositTransactionResponse, json(actualDepositTxn), LENIENT) + assertEquals(actualDepositTxn.id, savedDepositTxn.id) + JSONAssert.assertEquals( + expectedDepositTransactionResponse, + json(actualDepositTxn), + JSONCompareMode.LENIENT + ) } - private fun `test patch, get and compare`() { + @Test + @Order(70) + fun `test patch, get and compare`() { val patch = gson.fromJson(patchWithdrawTransactionRequest, PatchTransactionsRequest::class.java) // create patch request and patch - patch.records[0].transaction.id = savedWithdrawTxn.transaction.id - patch.records[1].transaction.id = savedDepositTxn.transaction.id + patch.records[0].transaction.id = savedWithdrawTxn.id + patch.records[1].transaction.id = savedDepositTxn.id platformApiClient.patchTransaction(patch) // check if the patched transactions are as expected - var afterPatchWithdraw = platformApiClient.getTransaction(savedWithdrawTxn.transaction.id) - assertEquals(afterPatchWithdraw.id, savedWithdrawTxn.transaction.id) - JSONAssert.assertEquals(expectedAfterPatchWithdraw, json(afterPatchWithdraw), LENIENT) + var afterPatchWithdraw = platformApiClient.getTransaction(savedWithdrawTxn.id) + assertEquals(afterPatchWithdraw.id, savedWithdrawTxn.id) + JSONAssert.assertEquals( + expectedAfterPatchWithdraw, + json(afterPatchWithdraw), + JSONCompareMode.LENIENT + ) - var afterPatchDeposit = platformApiClient.getTransaction(savedDepositTxn.transaction.id) - assertEquals(afterPatchDeposit.id, savedDepositTxn.transaction.id) - JSONAssert.assertEquals(expectedAfterPatchDeposit, json(afterPatchDeposit), LENIENT) + var afterPatchDeposit = platformApiClient.getTransaction(savedDepositTxn.id) + assertEquals(afterPatchDeposit.id, savedDepositTxn.id) + JSONAssert.assertEquals( + expectedAfterPatchDeposit, + json(afterPatchDeposit), + JSONCompareMode.LENIENT + ) // Test patch idempotency - afterPatchWithdraw = platformApiClient.getTransaction(savedWithdrawTxn.transaction.id) - assertEquals(afterPatchWithdraw.id, savedWithdrawTxn.transaction.id) - JSONAssert.assertEquals(expectedAfterPatchWithdraw, json(afterPatchWithdraw), LENIENT) + afterPatchWithdraw = platformApiClient.getTransaction(savedWithdrawTxn.id) + assertEquals(afterPatchWithdraw.id, savedWithdrawTxn.id) + JSONAssert.assertEquals( + expectedAfterPatchWithdraw, + json(afterPatchWithdraw), + JSONCompareMode.LENIENT + ) - afterPatchDeposit = platformApiClient.getTransaction(savedDepositTxn.transaction.id) - assertEquals(afterPatchDeposit.id, savedDepositTxn.transaction.id) - JSONAssert.assertEquals(expectedAfterPatchDeposit, json(afterPatchDeposit), LENIENT) + afterPatchDeposit = platformApiClient.getTransaction(savedDepositTxn.id) + assertEquals(afterPatchDeposit.id, savedDepositTxn.id) + JSONAssert.assertEquals( + expectedAfterPatchDeposit, + json(afterPatchDeposit), + JSONCompareMode.LENIENT + ) } - private fun `test GET transactions with bad ids`() { + @Test + @Order(80) + fun `test GET transactions with bad ids`() { val badTxnIds = listOf("null", "bad id", "123", null) for (txnId in badTxnIds) { assertThrows { platformApiClient.getTransaction(txnId) } } } - - fun testAll() { - println("Performing SEP24 tests...") - `test Sep24 info endpoint`() - `test Sep24 withdraw`() - `test Sep24 deposit`() - `test Sep24 deposit no issuer`() - `test Sep24 GET transaction and check the JWT`() - `test PlatformAPI GET transaction for deposit and withdrawal`() - `test patch, get and compare`() - `test GET transactions with bad ids`() - } } private const val withdrawRequest = @@ -176,7 +280,7 @@ private const val withdrawRequest = "lang": "en" }""" -private const val depositRequest = +private const val depositRequestJson = """{ "amount": "10", "asset_code": "USDC", @@ -185,14 +289,6 @@ private const val depositRequest = "lang": "en" }""" -private const val depositRequestNoIssuer = - """{ - "amount": "10", - "asset_code": "USDC", - "account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "lang": "en" -}""" - private const val patchWithdrawTransactionRequest = """ { @@ -352,61 +448,53 @@ private const val expectedAfterPatchDeposit = private const val expectedSep24Info = """ - { - "deposit": { - "JPYC": { - "enabled": true - }, - "USD": { - "enabled": true - }, - "USDC": { - "enabled": true - } +{ + "deposit": { + "JPYC": { + "enabled": true }, - "withdraw": { - "JPYC": { - "enabled": true - }, - "USD": { - "enabled": true - }, - "USDC": { - "enabled": true - } + "native": { + "enabled": true, + "maxAmount": 1000000.0 }, - "fee": { - "enabled": false + "USD": { + "enabled": true, + "minAmount": 0.0, + "maxAmount": 10000.0 }, - "features": { - "account_creation": false, - "claimable_balances": false - } - } -""" - -private const val expectedSep24WithdrawResponse = - """ - { - "transaction": { - "kind": "withdrawal", - "status": "incomplete", - "refunded": false, - "from": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4" + "USDC": { + "enabled": true, + "minAmount": 1.0, + "maxAmount": 1000000.0 } - } -""" - -private const val expectedSep24DepositResponse = - """ - { - "transaction": { - "kind": "deposit", - "status": "incomplete", - "refunded": false, - "to": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + }, + "withdraw": { + "JPYC": { + "enabled": true + }, + "native": { + "enabled": true, + "maxAmount": 1000000.0 + }, + "USD": { + "enabled": true, + "minAmount": 0.0, + "maxAmount": 10000.0 + }, + "USDC": { + "enabled": true, + "minAmount": 1.0, + "maxAmount": 1000000.0 } + }, + "fee": { + "enabled": false + }, + "features": { + "accountCreation": false, + "claimableBalances": false } +} """ private const val expectedWithdrawTransactionResponse = diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep31Tests.kt similarity index 85% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31Tests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep31Tests.kt index 5ec471a44c..e30d4bd675 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep31Tests.kt @@ -1,20 +1,22 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import java.time.Instant import kotlin.streams.toList +import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.skyscreamer.jsonassert.JSONCompareMode.LENIENT -import org.springframework.data.domain.Sort +import org.springframework.data.domain.Sort.Direction +import org.springframework.data.domain.Sort.Direction.* import org.stellar.anchor.api.exception.SepException import org.stellar.anchor.api.platform.* import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_31 import org.stellar.anchor.api.platform.PlatformTransactionData.builder -import org.stellar.anchor.api.platform.TransactionsOrderBy -import org.stellar.anchor.api.platform.TransactionsSeps import org.stellar.anchor.api.sep.SepTransactionStatus +import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerResponse import org.stellar.anchor.api.sep.sep12.Sep12Status @@ -23,34 +25,40 @@ import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionRequest import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionResponse import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.* +import org.stellar.anchor.client.* +import org.stellar.anchor.platform.AbstractIntegrationTests +import org.stellar.anchor.platform.TestConfig +import org.stellar.anchor.platform.gson +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer1Json +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer2Json +import org.stellar.anchor.platform.printRequest import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Sep1Helper.TomlContent import org.stellar.anchor.util.StringHelper.json lateinit var savedTxn: Sep31GetTransactionResponse -class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { - private val sep12Client: Sep12Client - private val sep31Client: Sep31Client - private val sep38Client: Sep38Client - private val platformApiClient: PlatformApiClient - - init { - println("Performing SEP31 tests...") - sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt) - sep31Client = Sep31Client(toml.getString("DIRECT_PAYMENT_SERVER"), jwt) - sep38Client = Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), jwt) - - platformApiClient = PlatformApiClient(AuthHelper.forNone(), config.env["platform.server.url"]!!) - } - private fun `test info endpoint`() { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(SAME_THREAD) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class Sep31Tests : AbstractIntegrationTests(TestConfig()) { + private val sep12Client: Sep12Client = Sep12Client(toml.getString("KYC_SERVER"), this.token.token) + private val sep31Client: Sep31Client = + Sep31Client(toml.getString("DIRECT_PAYMENT_SERVER"), this.token.token) + private val sep38Client: Sep38Client = + Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), this.token.token) + private val platformApiClient: PlatformApiClient = + PlatformApiClient(AuthHelper.forNone(), config.env["platform.server.url"]!!) + + @Test + fun `test info endpoint`() { printRequest("Calling GET /info") val info = sep31Client.getInfo() JSONAssert.assertEquals(gson.toJson(info), expectedSep31Info, JSONCompareMode.STRICT) } - private fun `test post and get transactions`() { + @Test + @Order(30) + fun `test post and get transactions`() { val (senderCustomer, receiverCustomer) = mkCustomers() val postTxResponse = createTx(senderCustomer, receiverCustomer) @@ -60,7 +68,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { JSONAssert.assertEquals(expectedTxn, json(savedTxn), LENIENT) assertEquals(postTxResponse.id, savedTxn.transaction.id) assertEquals(postTxResponse.stellarMemo, savedTxn.transaction.stellarMemo) - assertEquals(SepTransactionStatus.PENDING_SENDER.status, savedTxn.transaction.status) + assertEquals(PENDING_SENDER.status, savedTxn.transaction.status) } private fun mkCustomers(): Pair { @@ -77,7 +85,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { return senderCustomer!! to receiverCustomer!! } - private fun createTx( + fun createTx( senderCustomer: Sep12PutCustomerResponse, receiverCustomer: Sep12PutCustomerResponse ): Sep31PostTransactionResponse { @@ -102,7 +110,9 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { return postTxResponse } - private fun `test transactions`() { + @Test + @Order(20) + fun `test transactions`() { val (senderCustomer, receiverCustomer) = mkCustomers() val tx1 = createTx(senderCustomer, receiverCustomer) @@ -114,48 +124,38 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { println("Created transactions ${tx1.id} ${tx2.id} ${tx3.id}") // Basic test - val txs = getTransactions() + val txs = getTransactions(pageSize = 1000) assertOrderCorrect(all, txs.records) // Order test val descTxs = getTransactions( - order = Sort.Direction.DESC, + order = DESC, ) assertOrderCorrect(all.reversed(), descTxs.records) patchForTest(tx3, tx2) // OrderBy test - val orderByTxs = getTransactions(orderBy = TransactionsOrderBy.TRANSFER_RECEIVED_AT) + val orderByTxs = + getTransactions(orderBy = TransactionsOrderBy.TRANSFER_RECEIVED_AT, pageSize = 1000) assertOrderCorrect(listOf(tx2, tx3, tx1), orderByTxs.records) val orderByDesc = getTransactions( orderBy = TransactionsOrderBy.TRANSFER_RECEIVED_AT, - order = Sort.Direction.DESC + order = DESC, + pageSize = 1000 ) assertOrderCorrect(listOf(tx3, tx2, tx1), orderByDesc.records) // Statuses test - val statusesTxs = - getTransactions( - statuses = listOf(SepTransactionStatus.PENDING_SENDER, SepTransactionStatus.REFUNDED), - ) + val statusesTxs = getTransactions(statuses = listOf(PENDING_SENDER, REFUNDED), pageSize = 1000) assertOrderCorrect(listOf(tx1, tx2), statusesTxs.records) - - // Pagination test - val pageNull = getTransactions(pageSize = 1, order = Sort.Direction.DESC) - val page0 = getTransactions(pageSize = 1, pageNumber = 0, order = Sort.Direction.DESC) - assertOrderCorrect(listOf(tx3), pageNull.records) - assertOrderCorrect(listOf(tx3), page0.records) - - val page2 = getTransactions(pageSize = 1, pageNumber = 2, order = Sort.Direction.DESC) - assertOrderCorrect(listOf(tx1), page2.records) } private fun getTransactions( - order: Sort.Direction? = null, + order: Direction? = null, orderBy: TransactionsOrderBy? = null, statuses: List? = null, pageSize: Int? = null, @@ -191,17 +191,13 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { .records( listOf( PatchTransactionRequest( - builder() - .id(tx3.id) - .transferReceivedAt(Instant.now()) - .status(SepTransactionStatus.COMPLETED) - .build() + builder().id(tx3.id).transferReceivedAt(Instant.now()).status(COMPLETED).build() ), PatchTransactionRequest( builder() .id(tx2.id) .transferReceivedAt(Instant.now().minusSeconds(12345)) - .status(SepTransactionStatus.REFUNDED) + .status(REFUNDED) .build() ) ) @@ -210,7 +206,8 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { ) } - private fun testBadAsset() { + @Test + fun testBadAsset() { val customer = GsonUtils.getInstance().fromJson(testCustomer1Json, Sep12PutCustomerRequest::class.java) val pr = sep12Client.putCustomer(customer) @@ -222,7 +219,9 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { assertThrows { sep31Client.postTransaction(txnRequest) } } - private fun `test patch, get and compare`() { + @Test + @Order(40) + fun `test patch, get and compare`() { val patch = gson.fromJson(patchRequest, PatchTransactionsRequest::class.java) // create patch request and patch patch.records[0].transaction.id = savedTxn.transaction.id @@ -239,6 +238,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { JSONAssert.assertEquals(expectedAfterPatch, json(afterPatch), LENIENT) } + @Test fun `test bad requests`() { // Create sender customer val senderCustomerRequest = @@ -266,7 +266,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { // GET platformAPI transaction val getTxResponse = platformApiClient.getTransaction(postTxResponse.id) assertEquals(postTxResponse.id, getTxResponse.id) - assertEquals(SepTransactionStatus.PENDING_SENDER, getTxResponse.status) + assertEquals(PENDING_SENDER, getTxResponse.status) assertEquals(txnRequest.amount, getTxResponse.amountIn.amount) assertTrue(getTxResponse.amountIn.asset.contains(txnRequest.assetCode)) assertEquals(SEP_31, getTxResponse.sep) @@ -289,7 +289,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { .transaction( builder() .id(getTxResponse.id) - .status(SepTransactionStatus.PENDING_CUSTOMER_INFO_UPDATE) + .status(PENDING_CUSTOMER_INFO_UPDATE) .message("The receiving customer clabe_number is invalid!") .build() ) @@ -301,7 +301,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { assertEquals(1, patchTxResponse.records.size) var patchedTx = patchTxResponse.records[0] assertEquals(getTxResponse.id, patchedTx.id) - assertEquals(SepTransactionStatus.PENDING_CUSTOMER_INFO_UPDATE, patchedTx.status) + assertEquals(PENDING_CUSTOMER_INFO_UPDATE, patchedTx.status) assertEquals(SEP_31, patchedTx.sep) assertEquals("The receiving customer clabe_number is invalid!", patchedTx.message) assertTrue(patchedTx.updatedAt > patchedTx.startedAt) @@ -309,10 +309,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { // GET SEP-31 transaction should return PENDING_CUSTOMER_INFO_UPDATE with a message var gotSep31TxResponse = sep31Client.getTransaction(postTxResponse.id) assertEquals(postTxResponse.id, gotSep31TxResponse.transaction.id) - assertEquals( - SepTransactionStatus.PENDING_CUSTOMER_INFO_UPDATE.status, - gotSep31TxResponse.transaction.status - ) + assertEquals(PENDING_CUSTOMER_INFO_UPDATE.status, gotSep31TxResponse.transaction.status) assertEquals( "The receiving customer clabe_number is invalid!", gotSep31TxResponse.transaction.requiredInfoMessage @@ -333,11 +330,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { patchTxRequest = PatchTransactionRequest.builder() .transaction( - builder() - .id(getTxResponse.id) - .completedAt(Instant.now()) - .status(SepTransactionStatus.COMPLETED) - .build() + builder().id(getTxResponse.id).completedAt(Instant.now()).status(COMPLETED).build() ) .build() patchTxResponse = @@ -347,7 +340,7 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { assertEquals(1, patchTxResponse.records.size) patchedTx = patchTxResponse.records[0] assertEquals(getTxResponse.id, patchedTx.id) - assertEquals(SepTransactionStatus.COMPLETED, patchedTx.status) + assertEquals(COMPLETED, patchedTx.status) assertEquals(SEP_31, patchedTx.sep) assertNull(patchedTx.message) assertTrue(patchedTx.startedAt < patchedTx.updatedAt) @@ -356,20 +349,10 @@ class Sep31Tests(config: TestConfig, toml: TomlContent, jwt: String) { // GET SEP-31 transaction should return COMPLETED with no message gotSep31TxResponse = sep31Client.getTransaction(postTxResponse.id) assertEquals(postTxResponse.id, gotSep31TxResponse.transaction.id) - assertEquals(SepTransactionStatus.COMPLETED.status, gotSep31TxResponse.transaction.status) + assertEquals(COMPLETED.status, gotSep31TxResponse.transaction.status) assertNull(gotSep31TxResponse.transaction.requiredInfoMessage) assertNotNull(patchedTx.completedAt) } - - fun testAll() { - println("Performing Sep31 tests...") - `test info endpoint`() - `test transactions`() - `test post and get transactions`() - `test patch, get and compare`() - `test bad requests`() - testBadAsset() - } } private const val postTxnRequest = diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep38Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep38Tests.kt similarity index 81% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep38Tests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep38Tests.kt index 168645a900..e41ee854a6 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep38Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep38Tests.kt @@ -1,24 +1,23 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import java.time.Instant import java.time.format.DateTimeFormatter import kotlin.test.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.stellar.anchor.api.exception.SepException import org.stellar.anchor.api.sep.sep38.Sep38Context.SEP31 -import org.stellar.anchor.platform.Sep38Client +import org.stellar.anchor.client.Sep38Client +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.printRequest import org.stellar.anchor.platform.printResponse -import org.stellar.anchor.util.Sep1Helper.TomlContent -class Sep38Tests(config: TestConfig, toml: TomlContent, jwt: String) { - private val sep38Client: Sep38Client - - init { - sep38Client = Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), jwt) - } +class Sep38Tests : AbstractIntegrationTests(TestConfig()) { + private val sep38Client: Sep38Client = + Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), this.token.token) + @Test fun `test sep38 info, price and prices endpoints`() { // GET {SEP38}/info printRequest("Calling GET /info") @@ -72,6 +71,7 @@ class Sep38Tests(config: TestConfig, toml: TomlContent, jwt: String) { assertEquals(postQuote, getQuote) } + @Test fun `test selling over asset limit throws an exception`() { printRequest("Calling GET /price") @@ -84,10 +84,4 @@ class Sep38Tests(config: TestConfig, toml: TomlContent, jwt: String) { ) } } - - fun testAll() { - println("Performing SEP38 tests...") - `test sep38 info, price and prices endpoints`() - `test selling over asset limit throws an exception`() - } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep6Tests.kt similarity index 84% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep6Tests.kt index 7efba7816b..fadf870f71 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep6Tests.kt @@ -1,26 +1,175 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest +import org.junit.jupiter.api.Test import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.sep.sep38.Sep38Context +import org.stellar.anchor.client.Sep38Client +import org.stellar.anchor.client.Sep6Client +import org.stellar.anchor.platform.AbstractIntegrationTests import org.stellar.anchor.platform.CLIENT_WALLET_ACCOUNT -import org.stellar.anchor.platform.Sep38Client -import org.stellar.anchor.platform.Sep6Client +import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.gson import org.stellar.anchor.util.Log -import org.stellar.anchor.util.Sep1Helper.TomlContent -class Sep6Tests(val toml: TomlContent, jwt: String) { - private val sep6Client: Sep6Client - private val sep38Client: Sep38Client +class Sep6Tests : AbstractIntegrationTests(TestConfig()) { + private val sep6Client = Sep6Client(toml.getString("TRANSFER_SERVER"), token.token) + private val sep38Client = Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), this.token.token) - init { - sep6Client = Sep6Client(toml.getString("TRANSFER_SERVER"), jwt) - sep38Client = Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), jwt) + @Test + fun `test Sep6 info endpoint`() { + val info = sep6Client.getInfo() + JSONAssert.assertEquals(expectedSep6Info, gson.toJson(info), JSONCompareMode.STRICT) } - private val expectedSep6Info = - """ + @Test + fun `test sep6 deposit`() { + val request = + mapOf( + "asset_code" to "USDC", + "account" to CLIENT_WALLET_ACCOUNT, + "amount" to "1", + "type" to "SWIFT" + ) + val response = sep6Client.deposit(request) + Log.info("GET /deposit response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6DepositResponse, + gson.toJson(savedDepositTxn), + JSONCompareMode.LENIENT + ) + } + + @Test + fun `test sep6 deposit-exchange without quote`() { + val request = + mapOf( + "destination_asset" to "USDC", + "source_asset" to "iso4217:USD", + "amount" to "1", + "account" to CLIENT_WALLET_ACCOUNT, + "type" to "SWIFT" + ) + + val response = sep6Client.deposit(request, exchange = true) + Log.info("GET /deposit-exchange response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6DepositExchangeResponse, + gson.toJson(savedDepositTxn), + JSONCompareMode.LENIENT + ) + } + + @Test + fun `test sep6 deposit-exchange with quote`() { + val quoteId = + postQuote( + "iso4217:USD", + "10", + "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + ) + val request = + mapOf( + "destination_asset" to "USDC", + "source_asset" to "iso4217:USD", + "amount" to "10", + "account" to CLIENT_WALLET_ACCOUNT, + "type" to "SWIFT", + "quote_id" to quoteId + ) + + val response = sep6Client.deposit(request, exchange = true) + Log.info("GET /deposit-exchange response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6DepositExchangeWithQuoteResponse, + gson.toJson(savedDepositTxn), + JSONCompareMode.LENIENT + ) + } + + @Test + fun `test sep6 withdraw`() { + val request = mapOf("asset_code" to "USDC", "type" to "bank_account", "amount" to "1") + val response = sep6Client.withdraw(request) + Log.info("GET /withdraw response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedWithdrawTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6WithdrawResponse, + gson.toJson(savedWithdrawTxn), + JSONCompareMode.LENIENT + ) + } + + @Test + fun `test sep6 withdraw-exchange without quote`() { + val request = + mapOf( + "destination_asset" to "iso4217:USD", + "source_asset" to "USDC", + "amount" to "1", + "type" to "bank_account" + ) + + val response = sep6Client.withdraw(request, exchange = true) + Log.info("GET /withdraw-exchange response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6WithdrawExchangeResponse, + gson.toJson(savedDepositTxn), + JSONCompareMode.LENIENT + ) + } + + @Test + fun `test sep6 withdraw-exchange with quote`() { + val quoteId = + postQuote( + "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", + "10", + "iso4217:USD" + ) + val request = + mapOf( + "destination_asset" to "iso4217:USD", + "source_asset" to "USDC", + "amount" to "10", + "type" to "bank_account", + "quote_id" to quoteId + ) + + val response = sep6Client.withdraw(request, exchange = true) + Log.info("GET /withdraw-exchange response: $response") + assert(!response.id.isNullOrEmpty()) + + val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) + JSONAssert.assertEquals( + expectedSep6WithdrawExchangeWithQuoteResponse, + gson.toJson(savedDepositTxn), + JSONCompareMode.LENIENT + ) + } + + private fun postQuote(sellAsset: String, sellAmount: String, buyAsset: String): String { + return sep38Client.postQuote(sellAsset, sellAmount, buyAsset, Sep38Context.SEP6).id + } + + companion object { + + private val expectedSep6Info = + """ { "deposit": { "USDC": { @@ -104,10 +253,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6DepositResponse = - """ + private val expectedSep6DepositResponse = + """ { "transaction": { "kind": "deposit", @@ -115,10 +264,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6DepositExchangeResponse = - """ + private val expectedSep6DepositExchangeResponse = + """ { "transaction": { "kind": "deposit-exchange", @@ -133,10 +282,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6DepositExchangeWithQuoteResponse = - """ + private val expectedSep6DepositExchangeWithQuoteResponse = + """ { "transaction": { "kind": "deposit-exchange", @@ -151,10 +300,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6WithdrawResponse = - """ + private val expectedSep6WithdrawResponse = + """ { "transaction": { "kind": "withdrawal", @@ -163,10 +312,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6WithdrawExchangeResponse = - """ + private val expectedSep6WithdrawExchangeResponse = + """ { "transaction": { "kind": "withdrawal-exchange", @@ -181,10 +330,10 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() + .trimIndent() - private val expectedSep6WithdrawExchangeWithQuoteResponse = - """ + private val expectedSep6WithdrawExchangeWithQuoteResponse = + """ { "transaction": { "kind": "withdrawal-exchange", @@ -199,159 +348,6 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { } } """ - .trimIndent() - - private fun `test Sep6 info endpoint`() { - val info = sep6Client.getInfo() - JSONAssert.assertEquals(expectedSep6Info, gson.toJson(info), JSONCompareMode.STRICT) - } - - private fun `test sep6 deposit`() { - val request = - mapOf( - "asset_code" to "USDC", - "account" to CLIENT_WALLET_ACCOUNT, - "amount" to "1", - "type" to "SWIFT" - ) - val response = sep6Client.deposit(request) - Log.info("GET /deposit response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6DepositResponse, - gson.toJson(savedDepositTxn), - JSONCompareMode.LENIENT - ) - } - - private fun `test sep6 deposit-exchange without quote`() { - val request = - mapOf( - "destination_asset" to "USDC", - "source_asset" to "iso4217:USD", - "amount" to "1", - "account" to CLIENT_WALLET_ACCOUNT, - "type" to "SWIFT" - ) - - val response = sep6Client.deposit(request, exchange = true) - Log.info("GET /deposit-exchange response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6DepositExchangeResponse, - gson.toJson(savedDepositTxn), - JSONCompareMode.LENIENT - ) - } - - private fun `test sep6 deposit-exchange with quote`() { - val quoteId = - postQuote( - "iso4217:USD", - "10", - "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" - ) - val request = - mapOf( - "destination_asset" to "USDC", - "source_asset" to "iso4217:USD", - "amount" to "10", - "account" to CLIENT_WALLET_ACCOUNT, - "type" to "SWIFT", - "quote_id" to quoteId - ) - - val response = sep6Client.deposit(request, exchange = true) - Log.info("GET /deposit-exchange response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6DepositExchangeWithQuoteResponse, - gson.toJson(savedDepositTxn), - JSONCompareMode.LENIENT - ) - } - - private fun `test sep6 withdraw`() { - val request = mapOf("asset_code" to "USDC", "type" to "bank_account", "amount" to "1") - val response = sep6Client.withdraw(request) - Log.info("GET /withdraw response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedWithdrawTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6WithdrawResponse, - gson.toJson(savedWithdrawTxn), - JSONCompareMode.LENIENT - ) - } - - private fun `test sep6 withdraw-exchange without quote`() { - val request = - mapOf( - "destination_asset" to "iso4217:USD", - "source_asset" to "USDC", - "amount" to "1", - "type" to "bank_account" - ) - - val response = sep6Client.withdraw(request, exchange = true) - Log.info("GET /withdraw-exchange response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6WithdrawExchangeResponse, - gson.toJson(savedDepositTxn), - JSONCompareMode.LENIENT - ) - } - - private fun `test sep6 withdraw-exchange with quote`() { - val quoteId = - postQuote( - "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "10", - "iso4217:USD" - ) - val request = - mapOf( - "destination_asset" to "iso4217:USD", - "source_asset" to "USDC", - "amount" to "10", - "type" to "bank_account", - "quote_id" to quoteId - ) - - val response = sep6Client.withdraw(request, exchange = true) - Log.info("GET /withdraw-exchange response: $response") - assert(!response.id.isNullOrEmpty()) - - val savedDepositTxn = sep6Client.getTransaction(mapOf("id" to response.id!!)) - JSONAssert.assertEquals( - expectedSep6WithdrawExchangeWithQuoteResponse, - gson.toJson(savedDepositTxn), - JSONCompareMode.LENIENT - ) - } - - private fun postQuote(sellAsset: String, sellAmount: String, buyAsset: String): String { - return sep38Client.postQuote(sellAsset, sellAmount, buyAsset, Sep38Context.SEP6).id - } - - fun testAll() { - Log.info("Performing SEP6 tests") - `test Sep6 info endpoint`() - `test sep6 deposit`() - `test sep6 deposit-exchange without quote`() - `test sep6 deposit-exchange with quote`() - `test sep6 withdraw`() - `test sep6 withdraw-exchange without quote`() - `test sep6 withdraw-exchange with quote`() + .trimIndent() } } diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ServerHealthTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ServerHealthTests.kt new file mode 100644 index 0000000000..19642e6030 --- /dev/null +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ServerHealthTests.kt @@ -0,0 +1,25 @@ +package org.stellar.anchor.platform.integrationtest + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.stellar.anchor.apiclient.PlatformApiClient +import org.stellar.anchor.auth.AuthHelper +import org.stellar.anchor.platform.TestConfig + +class PlatformServerHealthTests { + val config = TestConfig() + private val platformApiClient = + PlatformApiClient(AuthHelper.forNone(), config.env["sep.server.url"]!!) + + @Test + fun testHealth() { + val response = platformApiClient.health(listOf("all")) + Assertions.assertEquals(5, response.size) + Assertions.assertEquals(1L, response["number_of_checks"]) + Assertions.assertNotNull(response["checks"]) + Assertions.assertNotNull(response["started_at"]) + Assertions.assertNotNull(response["elapsed_time_ms"]) + Assertions.assertNotNull(response["number_of_checks"]) + Assertions.assertNotNull(response["version"]) + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/StellarObserverTests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/StellarObserverTests.kt similarity index 89% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/StellarObserverTests.kt rename to essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/StellarObserverTests.kt index c73e8719fd..cfd40a9333 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/StellarObserverTests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/StellarObserverTests.kt @@ -1,15 +1,19 @@ -package org.stellar.anchor.platform.test +package org.stellar.anchor.platform.integrationtest import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.Request import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.stellar.anchor.platform.AbstractIntegrationTests +import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.gson -class StellarObserverTests { +class StellarObserverTests : AbstractIntegrationTests(TestConfig()) { companion object { const val OBSERVER_HEALTH_SERVER_PORT = 8083 } + private val httpClient: OkHttpClient = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.MINUTES) @@ -17,6 +21,7 @@ class StellarObserverTests { .writeTimeout(10, TimeUnit.MINUTES) .build() + @Test fun testStellarObserverHealth() { val httpRequest = Request.Builder() @@ -56,9 +61,4 @@ class StellarObserverTests { Assertions.assertEquals(false, stream1["stopped"]) Assertions.assertNotNull(stream1["last_event_id"]) } - - fun testAll() { - println("Performing Stellar observer tests...") - testStellarObserverHealth() - } } diff --git a/extended-tests/build.gradle.kts b/extended-tests/build.gradle.kts new file mode 100644 index 0000000000..6a6679cb9c --- /dev/null +++ b/extended-tests/build.gradle.kts @@ -0,0 +1,46 @@ +// The alias call in plugins scope produces IntelliJ false error which is suppressed here. +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + `java-library` + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.kotlin.jvm) +} + +repositories { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } + +dependencies { + testImplementation(libs.stellar.wallet.sdk) + testImplementation(libs.okhttp3.mockserver) + testImplementation(libs.assertj.core) + testImplementation("org.springframework.boot:spring-boot-starter-web") + testImplementation(libs.kotlin.serialization.json) + testImplementation(libs.ktor.client.json) + // project dependencies + testImplementation(project(":lib-util")) + testImplementation(project(":api-schema")) + testImplementation(project(":core")) + testImplementation(project(":platform")) + testImplementation(project(":service-runner")) + testImplementation(project(":wallet-reference-server")) + testImplementation(project(":kotlin-reference-server")) + testImplementation(testFixtures(project(":essential-tests"))) +} + +tasks { bootJar { enabled = false } } + +// The following is to enable test concurrency +apply(from = "$rootDir/scripts.gradle.kts") +@Suppress("UNCHECKED_CAST") +val enableTestConcurrency = extra["enableTestConcurrency"] as (Test) -> Unit + +tasks.test { +// Useful exclusions for debugging tests +// exclude("**/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest**") +// exclude("**/org/stellar/anchor/platform/AnchorPlatformCustodyApiRpcEnd2EndTest**") +// exclude("**/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest**") +// exclude("**/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest**") +// exclude("**/org/stellar/anchor/platform/CustodyApiKeyAuthIntegrationTest**") +// exclude("**/org/stellar/anchor/platform/CustodyJwtAuthIntegrationTest**") +// exclude("**/org/stellar/anchor/platform/PostgresMigrationTest**") +} diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractAuthIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractAuthIntegrationTest.kt new file mode 100644 index 0000000000..b5090d774d --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractAuthIntegrationTest.kt @@ -0,0 +1,42 @@ +package org.stellar.anchor.platform + +import org.stellar.anchor.auth.AuthHelper +import org.stellar.anchor.auth.JwtService +import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.ANCHOR_TO_PLATFORM_SECRET +import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.PLATFORM_TO_ANCHOR_SECRET +import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.PLATFORM_TO_CUSTODY_SECRET + +const val GET_TRANSACTIONS_ENDPOINT = "GET,/transactions" +const val PATCH_TRANSACTIONS_ENDPOINT = "PATCH,/transactions" +const val GET_TRANSACTIONS_MY_ID_ENDPOINT = "GET,/transactions/my_id" +const val GET_EXCHANGE_QUOTES_ENDPOINT = "GET,/exchange/quotes" +const val GET_EXCHANGE_QUOTES_ID_ENDPOINT = "GET,/exchange/quotes/id" +const val POST_CUSTODY_TRANSACTION_ENDPOINT = "POST,/transactions" + +abstract class AbstractAuthIntegrationTest { + companion object { + private val jwtService = + JwtService( + null, + null, + null, + PLATFORM_TO_ANCHOR_SECRET, + ANCHOR_TO_PLATFORM_SECRET, + PLATFORM_TO_CUSTODY_SECRET + ) + private val jwtWrongKeyService = + JwtService( + null, + null, + null, + PLATFORM_TO_ANCHOR_SECRET + "bad", + ANCHOR_TO_PLATFORM_SECRET + "bad", + PLATFORM_TO_CUSTODY_SECRET + "bad" + ) + + internal val jwtAuthHelper = AuthHelper.forJwtToken(jwtService, 10000) + internal val jwtWrongKeyAuthHelper = AuthHelper.forJwtToken(jwtWrongKeyService, 10000) + internal val jwtExpiredAuthHelper = AuthHelper.forJwtToken(jwtService, 0) + internal lateinit var testProfileRunner: TestProfileExecutor + } +} diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt new file mode 100644 index 0000000000..d1361fc76e --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt @@ -0,0 +1,73 @@ +package org.stellar.anchor.platform + +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import org.stellar.anchor.platform.test.CustodyApiTests +import org.stellar.anchor.platform.test.PlatformApiCustodyTests +import org.stellar.anchor.platform.test.Sep24BaseEnd2EndTest +import org.stellar.anchor.platform.test.Sep31End2EndTests +import org.stellar.anchor.util.Sep1Helper +import org.stellar.walletsdk.ApplicationConfiguration +import org.stellar.walletsdk.StellarConfiguration +import org.stellar.walletsdk.Wallet +import org.stellar.walletsdk.anchor.auth +import org.stellar.walletsdk.horizon.SigningKeyPair + +open class AbstractIntegrationTest(private val config: TestConfig) { + companion object { + const val ANCHOR_TO_PLATFORM_SECRET = "myAnchorToPlatformSecret" + const val PLATFORM_TO_ANCHOR_SECRET = "myPlatformToAnchorSecret" + const val PLATFORM_TO_CUSTODY_SECRET = "myPlatformToCustodySecret" + const val PLATFORM_SERVER_PORT = 8085 + const val CUSTODY_SERVER_SERVER_PORT = 8086 + const val REFERENCE_SERVER_PORT = 8091 + const val JWT_EXPIRATION_MILLISECONDS = 10000L + } + + init { + System.getProperties() + .setProperty("REFERENCE_SERVER_CONFIG", "classpath:/anchor-reference-server.yaml") + } + + val testProfileRunner = TestProfileExecutor(config) + lateinit var platformApiCustodyTests: PlatformApiCustodyTests + lateinit var custodyApiTests: CustodyApiTests + lateinit var sep24RpcE2eTests: Sep24BaseEnd2EndTest + lateinit var sep31RpcE2eTests: Sep31End2EndTests + + fun setUp(envMap: Map) { + envMap.forEach { (key, value) -> config.env[key] = value } + testProfileRunner.start() + setupTests() + } + + fun tearDown() { + testProfileRunner.shutdown() + } + + private fun setupTests() = runBlocking { + // Query SEP-1 + val toml = + Sep1Helper.parse(resourceAsString("${config.env["anchor.domain"]}/.well-known/stellar.toml")) + + // Get JWT + val jwt = auth() + + platformApiCustodyTests = PlatformApiCustodyTests(config, toml, jwt) + custodyApiTests = CustodyApiTests(config, toml, jwt) + sep24RpcE2eTests = Sep24BaseEnd2EndTest(config, jwt) + sep31RpcE2eTests = Sep31End2EndTests(config, toml, jwt) + } + + private suspend fun auth(): String { + val wallet = + Wallet( + StellarConfiguration.Testnet, + ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } + ) + val walletKeyPair = SigningKeyPair.fromSecret(CLIENT_WALLET_SECRET) + val anchor = wallet.anchor(config.env["anchor.domain"]!!) + return anchor.auth().authenticate(walletKeyPair).token + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt similarity index 58% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt index fb50143c3c..e011ba0f6f 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformApiRpcEnd2EndTest.kt @@ -4,7 +4,19 @@ import org.junit.jupiter.api.* @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class AnchorPlatformApiRpcEnd2EndTest : - AbstractIntegrationTest(TestConfig(testProfileName = "default-rpc")) { + AbstractIntegrationTest( + TestConfig(testProfileName = "default-rpc").also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_SEP_SERVER] = "true" + it.env[RUN_PLATFORM_SERVER] = "true" + it.env[RUN_EVENT_PROCESSING_SERVER] = "true" + it.env[RUN_PAYMENT_OBSERVER] = "true" + it.env[RUN_CUSTODY_SERVER] = "true" + it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" + it.env[RUN_WALLET_SERVER] = "true" + } + ) { companion object { private val singleton = AnchorPlatformApiRpcEnd2EndTest() diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt similarity index 68% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt index 0fd1db73c6..2315d2dd2b 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyIntegrationTest.kt @@ -3,11 +3,22 @@ package org.stellar.anchor.platform import kotlinx.coroutines.* import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.* -import org.stellar.anchor.platform.test.* @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class AnchorPlatformCustodyIntegrationTest : - AbstractIntegrationTest(TestConfig(testProfileName = "default-custody")) { + AbstractIntegrationTest( + TestConfig(testProfileName = "default-custody").also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_SEP_SERVER] = "true" + it.env[RUN_PLATFORM_SERVER] = "true" + it.env[RUN_EVENT_PROCESSING_SERVER] = "true" + it.env[RUN_PAYMENT_OBSERVER] = "true" + it.env[RUN_CUSTODY_SERVER] = "true" + it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" + it.env[RUN_WALLET_SERVER] = "false" + } + ) { companion object { private val singleton = AnchorPlatformCustodyIntegrationTest() private val custodyMockServer = MockWebServer() diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyApiKeyAuthIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyApiKeyAuthIntegrationTest.kt new file mode 100644 index 0000000000..bdb13eb9f2 --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyApiKeyAuthIntegrationTest.kt @@ -0,0 +1,85 @@ +package org.stellar.anchor.platform + +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.stellar.anchor.util.GsonUtils +import org.stellar.anchor.util.OkHttpUtil + +internal class CustodyApiKeyAuthIntegrationTest : AbstractAuthIntegrationTest() { + companion object { + @BeforeAll + @JvmStatic + fun setup() { + println("Running CustodyApiKeyAuthIntegrationTest") + testProfileRunner = + TestProfileExecutor( + TestConfig(testProfileName = "default-custody").also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_CUSTODY_SERVER] = "true" + + // enable custody server api_key auth + it.env["custody_server.auth.type"] = "api_key" + } + ) + testProfileRunner.start() + } + + @AfterAll + @JvmStatic + fun breakdown() { + testProfileRunner.shutdown() + } + } + + private val gson = GsonUtils.getInstance() + private val httpClient: OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.MINUTES) + .readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES) + .build() + + @ParameterizedTest + @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) + fun test_incomingCustodyAuth_emptyApiKey_authFails(method: String, endpoint: String) { + val httpRequest = + Request.Builder() + .url("http://localhost:${AbstractIntegrationTest.CUSTODY_SERVER_SERVER_PORT}$endpoint") + .header("Content-Type", "application/json") + .method(method, getCustodyDummyRequestBody()) + .build() + val response = httpClient.newCall(httpRequest).execute() + assertEquals(403, response.code) + } + + @ParameterizedTest + @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) + fun test_incomingCustodyAuth_emptyApiKey_authPasses(method: String, endpoint: String) { + val httpRequest = + Request.Builder() + .url("http://localhost:${AbstractIntegrationTest.CUSTODY_SERVER_SERVER_PORT}$endpoint") + .header("Content-Type", "application/json") + .header("X-Api-Key", AbstractIntegrationTest.PLATFORM_TO_CUSTODY_SECRET) + .method(method, getCustodyDummyRequestBody()) + .build() + val response = httpClient.newCall(httpRequest).execute() + assertEquals(200, response.code) + } + + private fun getPlatformDummyRequestBody(method: String): RequestBody? { + return if (method != "PATCH") null + else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) + } + + private fun getCustodyDummyRequestBody(): RequestBody? { + return OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("id" to "testId"))) + } +} diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyJwtAuthIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyJwtAuthIntegrationTest.kt new file mode 100644 index 0000000000..64ef873594 --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/CustodyJwtAuthIntegrationTest.kt @@ -0,0 +1,102 @@ +package org.stellar.anchor.platform + +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.stellar.anchor.api.custody.CreateCustodyTransactionRequest +import org.stellar.anchor.api.exception.CustodyException +import org.stellar.anchor.platform.apiclient.CustodyApiClient +import org.stellar.anchor.platform.config.CustodyApiConfig +import org.stellar.anchor.platform.config.PropertyCustodySecretConfig +import org.stellar.anchor.util.OkHttpUtil + +internal class CustodyJwtAuthIntegrationTest : AbstractAuthIntegrationTest() { + companion object { + @BeforeAll + @JvmStatic + fun setup() { + println("Running CustodyJwtAuthIntegrationTest") + testProfileRunner = + TestProfileExecutor( + TestConfig(testProfileName = "default-custody").also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_CUSTODY_SERVER] = "true" + + // enable custody server jwt auth + it.env["custody_server.auth.type"] = "jwt" + } + ) + testProfileRunner.start() + } + + @AfterAll + @JvmStatic + fun breakdown() { + testProfileRunner.shutdown() + } + } + + private val httpClient: OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.MINUTES) + .readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES) + .build() + + private val custodyApiConfig: CustodyApiConfig = + CustodyApiConfig(PropertyCustodySecretConfig()).apply { baseUrl = "http://localhost:8086" } + + private val jwtCustodyClient: CustodyApiClient = + CustodyApiClient(httpClient, jwtAuthHelper, custodyApiConfig) + private val jwtWrongKeyCustodyClient: CustodyApiClient = + CustodyApiClient(httpClient, jwtWrongKeyAuthHelper, custodyApiConfig) + private val jwtExpiredTokenCustodyClient: CustodyApiClient = + CustodyApiClient(httpClient, jwtExpiredAuthHelper, custodyApiConfig) + + @Test + fun `test the custody endpoints with JWT auth`() { + // Assert the request does not throw a 403. + // As for the correctness of the request/response, it should be tested in the custody server + // integration tests. + jwtCustodyClient.createTransaction(getCustodyDummyRequest()) + } + + @ParameterizedTest + @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) + fun `test JWT protection of the custody server`(method: String, endpoint: String) { + // Check if the request without JWT will cause a 403. + val httpRequest = + Request.Builder() + .url("http://localhost:${AbstractIntegrationTest.CUSTODY_SERVER_SERVER_PORT}${endpoint}") + .header("Content-Type", "application/json") + .method(method, getCustodyDummyRequestBody()) + .build() + val response = httpClient.newCall(httpRequest).execute() + assertEquals(403, response.code) + + // Check if the wrong JWT key will cause a 403. + assertThrows { + jwtWrongKeyCustodyClient.createTransaction(getCustodyDummyRequest()) + } + assertThrows { + jwtExpiredTokenCustodyClient.createTransaction(getCustodyDummyRequest()) + } + } + + private fun getCustodyDummyRequest(): CreateCustodyTransactionRequest { + return CreateCustodyTransactionRequest.builder().id("testId").build() + } + + private fun getCustodyDummyRequestBody(): RequestBody? { + return OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("id" to "testId"))) + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt similarity index 92% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt index 1b4d35468f..e3f446d943 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/DatabaseMigrationTests.kt @@ -5,8 +5,7 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test val PostgresConfig = - TestConfig("default").also { - it.env[RUN_DOCKER] = "false" + TestConfig().also { it.env[RUN_ALL_SERVERS] = "false" it.env[RUN_SEP_SERVER] = "true" it.env["data.flyway_enabled"] = "true" diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformApiKeyAuthIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformApiKeyAuthIntegrationTest.kt new file mode 100644 index 0000000000..07e33c8e2a --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformApiKeyAuthIntegrationTest.kt @@ -0,0 +1,100 @@ +package org.stellar.anchor.platform + +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.stellar.anchor.util.GsonUtils +import org.stellar.anchor.util.OkHttpUtil + +internal class PlatformApiKeyAuthIntegrationTest : AbstractAuthIntegrationTest() { + companion object { + @BeforeAll + @JvmStatic + fun setup() { + println("Running PlatformApiKeyAuthIntegrationTest") + testProfileRunner = + TestProfileExecutor( + TestConfig().also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_PLATFORM_SERVER] = "true" + + // enable platform server api_key auth + it.env["platform_server.auth.type"] = "api_key" + } + ) + testProfileRunner.start() + } + + @AfterAll + @JvmStatic + fun breakdown() { + testProfileRunner.shutdown() + } + } + + private val gson = GsonUtils.getInstance() + private val httpClient: OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.MINUTES) + .readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES) + .build() + + @ParameterizedTest + @CsvSource( + value = + [ + GET_TRANSACTIONS_ENDPOINT, + PATCH_TRANSACTIONS_ENDPOINT, + GET_TRANSACTIONS_MY_ID_ENDPOINT, + GET_EXCHANGE_QUOTES_ENDPOINT, + GET_EXCHANGE_QUOTES_ID_ENDPOINT + ] + ) + fun `test API_KEY auth protection of the platform server`(method: String, endpoint: String) { + val httpRequest = + Request.Builder() + .url("http://localhost:${AbstractIntegrationTest.PLATFORM_SERVER_PORT}$endpoint") + .header("Content-Type", "application/json") + .method(method, getPlatformDummyRequestBody(method)) + .build() + val response = httpClient.newCall(httpRequest).execute() + assertEquals(403, response.code) + } + + @ParameterizedTest + @CsvSource( + value = + [ + GET_TRANSACTIONS_ENDPOINT, + PATCH_TRANSACTIONS_ENDPOINT, + GET_TRANSACTIONS_MY_ID_ENDPOINT, + GET_EXCHANGE_QUOTES_ENDPOINT, + GET_EXCHANGE_QUOTES_ID_ENDPOINT + ] + ) + fun `test the platform endpoints with API_KEY auth`(method: String, endpoint: String) { + val httpRequest = + Request.Builder() + .url("http://localhost:${AbstractIntegrationTest.PLATFORM_SERVER_PORT}$endpoint") + .header("Content-Type", "application/json") + .header("X-Api-Key", AbstractIntegrationTest.ANCHOR_TO_PLATFORM_SECRET) + .method(method, getPlatformDummyRequestBody(method)) + .build() + val response = httpClient.newCall(httpRequest).execute() + Assertions.assertNotEquals(403, response.code) + } + + private fun getPlatformDummyRequestBody(method: String): RequestBody? { + return if (method != "PATCH") null + else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) + } +} diff --git a/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformJwtAuthIntegrationTest.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformJwtAuthIntegrationTest.kt new file mode 100644 index 0000000000..0c47948fe9 --- /dev/null +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/PlatformJwtAuthIntegrationTest.kt @@ -0,0 +1,244 @@ +package org.stellar.anchor.platform + +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.stellar.anchor.api.callback.GetCustomerRequest +import org.stellar.anchor.api.callback.GetFeeRequest +import org.stellar.anchor.api.callback.GetRateRequest +import org.stellar.anchor.api.exception.* +import org.stellar.anchor.apiclient.PlatformApiClient +import org.stellar.anchor.platform.callback.RestCustomerIntegration +import org.stellar.anchor.platform.callback.RestFeeIntegration +import org.stellar.anchor.platform.callback.RestRateIntegration +import org.stellar.anchor.util.OkHttpUtil + +internal class PlatformJwtAuthIntegrationTest : AbstractAuthIntegrationTest() { + companion object { + @BeforeAll + @JvmStatic + fun setup() { + println("Running PlatformJwtAuthIntegrationTest") + testProfileRunner = + TestProfileExecutor( + TestConfig().also { + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_PLATFORM_SERVER] = "true" + + // enable platform server jwt auth + it.env["platform_server.auth.type"] = "JWT" + // enable business server callback auth + it.env["auth.type"] = "JWT" + it.env["auth.platformToAnchorSecret"] = + AbstractIntegrationTest.PLATFORM_TO_ANCHOR_SECRET + it.env["auth.anchorToPlatformSecret"] = + AbstractIntegrationTest.ANCHOR_TO_PLATFORM_SECRET + it.env["auth.expirationMilliseconds"] = + AbstractIntegrationTest.JWT_EXPIRATION_MILLISECONDS.toString() + } + ) + testProfileRunner.start() + } + + @AfterAll + @JvmStatic + fun breakdown() { + testProfileRunner.shutdown() + } + } + + // TODO - to be deprecated by platformAPI client + private val httpClient: OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.MINUTES) + .readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES) + .build() + + private val jwtPlatformClient: PlatformApiClient = + PlatformApiClient(jwtAuthHelper, "http://localhost:8085") + private val jwtWrongKeyPlatformClient: PlatformApiClient = + PlatformApiClient(jwtWrongKeyAuthHelper, "http://localhost:8085") + private val jwtExpiredTokenPlatformClient: PlatformApiClient = + PlatformApiClient(jwtExpiredAuthHelper, "http://localhost:8085") + + @ParameterizedTest + @CsvSource( + value = + [ + GET_TRANSACTIONS_ENDPOINT, + PATCH_TRANSACTIONS_ENDPOINT, + GET_TRANSACTIONS_MY_ID_ENDPOINT, + GET_EXCHANGE_QUOTES_ENDPOINT, + GET_EXCHANGE_QUOTES_ID_ENDPOINT + ] + ) + fun `test the platform endpoints with JWT auth`(method: String, endpoint: String) { + // Assert the request does not throw a 403. + // As for the correctness of the request/response, it should be tested in the platform server + // integration tests. + assertThrows { jwtPlatformClient.getTransaction("my_id") } + } + + @ParameterizedTest + @CsvSource( + value = + [ + GET_TRANSACTIONS_ENDPOINT, + PATCH_TRANSACTIONS_ENDPOINT, + GET_TRANSACTIONS_MY_ID_ENDPOINT, + GET_EXCHANGE_QUOTES_ENDPOINT, + GET_EXCHANGE_QUOTES_ID_ENDPOINT + ] + ) + fun `test JWT protection of the platform server`(method: String, endpoint: String) { + // Check if the request without JWT will cause a 403. + val httpRequest = + Request.Builder() + .url("http://localhost:8085/transactions") + .header("Content-Type", "application/json") + .method(method, getPlatformDummyRequestBody(method)) + .build() + val response = httpClient.newCall(httpRequest).execute() + assertEquals(403, response.code) + + // Check if the wrong JWT key will cause a 403. + assertThrows { jwtWrongKeyPlatformClient.getTransaction("my_id") } + assertThrows { + jwtExpiredTokenPlatformClient.getTransaction("my_id") + } + } + + private fun getPlatformDummyRequestBody(method: String): RequestBody? { + return if (method != "PATCH") null + else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) + } + + @Test + // This is disabled because it is testing the callback auth instead of platform auth + @Disabled + fun `test the callback customer endpoint with JWT auth`() { + val rci = + RestCustomerIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtAuthHelper, + gson + ) + // Assert the request does not throw a 403. + assertThrows { + rci.getCustomer(GetCustomerRequest.builder().id("1").build()) + } + } + + @Test + // This is disabled because it is testing the callback auth instead of platform auth + @Disabled + fun `test the callback rate endpoint with JWT auth`() { + val rri = + RestRateIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtAuthHelper, + gson + ) + // Assert the request does not throw a 403. + assertThrows { rri.getRate(GetRateRequest.builder().build()) } + } + + @Test + // This is disabled because it is testing the callback auth instead of platform auth + @Disabled + fun `test the callback fee endpoint with JWT auth`() { + val rfi = + RestFeeIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtAuthHelper, + gson + ) + // Assert the request does not throw a 403. + assertThrows { rfi.getFee(GetFeeRequest.builder().build()) } + } + + @Test + fun `test JWT protection of callback customer endpoint`() { + val badTokenClient = + RestCustomerIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtWrongKeyAuthHelper, + gson + ) + assertThrows { + badTokenClient.getCustomer(GetCustomerRequest.builder().id("1").build()) + } + + val expiredTokenClient = + RestCustomerIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtWrongKeyAuthHelper, + gson + ) + assertThrows { + expiredTokenClient.getCustomer(GetCustomerRequest.builder().id("1").build()) + } + } + + @Test + fun `test JWT protection of callback rate endpoint`() { + val badTokenClient = + RestRateIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtWrongKeyAuthHelper, + gson + ) + assertThrows { badTokenClient.getRate(GetRateRequest.builder().build()) } + + val expiredTokenClient = + RestRateIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtExpiredAuthHelper, + gson + ) + assertThrows { + expiredTokenClient.getRate(GetRateRequest.builder().build()) + } + } + + @Test + fun `test JWT protection of callback fee endpoint with bad token`() { + val badTokenClient = + RestFeeIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtWrongKeyAuthHelper, + gson + ) + assertThrows { badTokenClient.getFee(GetFeeRequest.builder().build()) } + + val expiredTokenClient = + RestFeeIntegration( + "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", + httpClient, + jwtExpiredAuthHelper, + gson + ) + assertThrows { + expiredTokenClient.getFee(GetFeeRequest.builder().build()) + } + } +} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt similarity index 99% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt index 0197b52243..527aac9619 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/CustodyApiTests.kt @@ -19,8 +19,8 @@ import org.stellar.anchor.api.rpc.method.RpcMethod.* import org.stellar.anchor.api.sep.SepTransactionStatus import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.CustodyApiClient -import org.stellar.anchor.platform.Sep24Client +import org.stellar.anchor.client.CustodyApiClient +import org.stellar.anchor.client.Sep24Client import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.platform.custody.fireblocks.FireblocksEventService.FIREBLOCKS_SIGNATURE_HEADER import org.stellar.anchor.platform.gson @@ -523,7 +523,8 @@ private const val EXPECTED_TRANSACTION_RESPONSE = ] } ], - "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" } """ @@ -602,7 +603,8 @@ private const val EXPECTED_TXN_REFUND_RESPONSE = "memo": "testTag", "memo_type": "id", "refund_memo": "12345", - "refund_memo_type": "id" + "refund_memo_type": "id", + "client_name": "referenceCustodial" } """ diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt similarity index 96% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt index faa3f0cc50..784ce3264a 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt @@ -14,9 +14,9 @@ import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionRequest import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.Sep12Client -import org.stellar.anchor.platform.Sep24Client -import org.stellar.anchor.platform.Sep31Client +import org.stellar.anchor.client.Sep12Client +import org.stellar.anchor.client.Sep24Client +import org.stellar.anchor.client.Sep31Client import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.Sep1Helper @@ -59,17 +59,11 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, `SEP-24 deposit complete full`() `SEP-24 withdraw full refund`() + // TODO: This is temporarily disabled because of lacking support for debugging. This should be + // re-enabled once the tests are cleaned up. `SEP-31 refunded do_stellar_refund`() } - private fun `SEP-6 deposit complete full`() { - // TODO(philip): add this after custody changes are merged - } - - private fun `SEP-6 withdraw full refund`() { - // TODO(philip): add this after custody changes are merged - } - /** * 1. incomplete -> notify_interactive_flow_complete * 2. pending_anchor -> request_offchain_funds @@ -78,7 +72,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, * 5. completed */ private fun `SEP-24 deposit complete full`() { - `test SEP-24 deposit flow`( + `test deposit flow`( SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES ) @@ -93,7 +87,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, * 6. refunded */ private fun `SEP-24 withdraw full refund`() { - `test SEP-24 withdraw flow`( + `test withdraw flow`( SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES ) @@ -112,7 +106,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, ) } - private fun `test SEP-24 deposit flow`(actionRequests: String, actionResponse: String) { + private fun `test deposit flow`(actionRequests: String, actionResponse: String) { val depositRequest = gson.fromJson(SEP_24_DEPOSIT_FLOW_REQUEST, HashMap::class.java) val depositResponse = sep24Client.deposit(depositRequest as HashMap) `test flow`(depositResponse.id, actionRequests, actionResponse) @@ -144,7 +138,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, `test flow`(receiveResponse.id, updatedActionRequests, updatedActionResponses) } - private fun `test SEP-24 withdraw flow`(actionRequests: String, actionResponse: String) { + private fun `test withdraw flow`(actionRequests: String, actionResponse: String) { val withdrawRequest = gson.fromJson(SEP_24_WITHDRAW_FLOW_REQUEST, HashMap::class.java) val withdrawResponse = sep24Client.withdraw(withdrawRequest as HashMap) `test flow`(withdrawResponse.id, actionRequests, actionResponse) @@ -285,7 +279,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES = "started_at": "2023-08-07T12:52:01.663006Z", "updated_at": "2023-08-07T12:52:03.100242Z", "message": "test message 1", - "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" }, "id": "1" }, @@ -315,7 +310,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES = "started_at": "2023-08-07T12:52:01.663006Z", "updated_at": "2023-08-07T12:52:04.165625Z", "message": "test message 2", - "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" + "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "client_name": "referenceCustodial" }, "id": "2" }, @@ -346,7 +342,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES = "updated_at": "2023-08-07T12:52:05.241766Z", "message": "test message 3", "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "external_transaction_id": "ext-123456" + "external_transaction_id": "ext-123456", + "client_name": "referenceCustodial" }, "id": "3" }, @@ -377,7 +374,8 @@ private const val SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES = "updated_at": "2023-08-07T12:52:07.016199Z", "message": "test message 4", "destination_account": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "external_transaction_id": "ext-123456" + "external_transaction_id": "ext-123456", + "client_name": "referenceCustodial" }, "id": "4" } @@ -521,7 +519,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = "updated_at": "2023-08-07T10:35:39.973638Z", "message": "test message 1", "source_account": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", - "destination_account": "GBN4NNCDGJO4XW4KQU3CBIESUJWFVBUZPOKUZHT7W7WRB7CWOA7BXVQF" + "destination_account": "GBN4NNCDGJO4XW4KQU3CBIESUJWFVBUZPOKUZHT7W7WRB7CWOA7BXVQF", + "client_name": "referenceCustodial" }, "id": "1" }, @@ -554,7 +553,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = "source_account": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", "destination_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", "memo": "testMemo", - "memo_type": "id" + "memo_type": "id", + "client_name": "referenceCustodial" }, "id": "2" }, @@ -608,7 +608,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = "source_account": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", "destination_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", "memo": "testMemo", - "memo_type": "id" + "memo_type": "id", + "client_name": "referenceCustodial" }, "id": "3" }, @@ -662,7 +663,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = "source_account": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", "destination_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", "memo": "testMemo", - "memo_type": "id" + "memo_type": "id", + "client_name": "referenceCustodial" }, "id": "4" }, @@ -741,7 +743,8 @@ private const val SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES = "source_account": "GAIUIZPHLIHQEMNJGSZKCEUWHAZVGUZDBDMO2JXNAJZZZVNSVHQCEWJ4", "destination_account": "GBA3CI3MMCHWNKQYGYQNXGXSQEZZHTZCYY5JZ7MIVLJ74DBUGIOAGNV6", "memo": "testMemo", - "memo_type": "id" + "memo_type": "id", + "client_name": "referenceCustodial" }, "id": "5" } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt similarity index 93% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt index 69481f789e..6438511d3f 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/ReferenceServerTests.kt @@ -1,7 +1,7 @@ package org.stellar.anchor.platform.test import org.junit.jupiter.api.Assertions -import org.stellar.anchor.platform.ReferenceServerClient +import org.stellar.anchor.client.ReferenceServerClient lateinit var referenceServerClient: ReferenceServerClient diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24RpcEnd2EndTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24BaseEnd2EndTest.kt similarity index 88% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24RpcEnd2EndTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24BaseEnd2EndTest.kt index e7156fe1cb..afd1201fdb 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24RpcEnd2EndTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24BaseEnd2EndTest.kt @@ -26,6 +26,7 @@ import org.stellar.anchor.platform.CLIENT_WALLET_SECRET import org.stellar.anchor.platform.TestConfig import org.stellar.anchor.util.Log.info import org.stellar.reference.client.AnchorReferenceServerClient +import org.stellar.reference.transactionWithRetry import org.stellar.reference.wallet.WalletServerClient import org.stellar.walletsdk.ApplicationConfiguration import org.stellar.walletsdk.InteractiveFlowResponse @@ -35,13 +36,12 @@ import org.stellar.walletsdk.anchor.* import org.stellar.walletsdk.anchor.TransactionStatus.* import org.stellar.walletsdk.asset.IssuedAssetId import org.stellar.walletsdk.asset.StellarAssetId -import org.stellar.walletsdk.asset.XLM import org.stellar.walletsdk.auth.AuthToken import org.stellar.walletsdk.horizon.SigningKeyPair import org.stellar.walletsdk.horizon.sign -import org.stellar.walletsdk.horizon.transaction.transferWithdrawalTransaction -class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { +/** TODO: This should be replaced by Sep24End2EndTest */ +class Sep24BaseEnd2EndTest(config: TestConfig, val jwt: String) { private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET private val keypair = SigningKeyPair.fromSecret(walletSecretKey) private val wallet = @@ -93,7 +93,7 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { assertEquals("referenceCustodial", interactiveJwt.claims[JwtService.CLIENT_NAME]) // Wait for the status to change to COMPLETED - waitForTxnStatus(response.id, COMPLETED, token) + waitForTxnStatus(response.id, COMPLETED, ERROR, token) // Check if the transaction can be listed by stellar transaction id val fetchedTxn = anchor.interactive().getTransaction(response.id, token) as DepositTransaction @@ -192,20 +192,22 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { info("accessing ${withdrawTxn.url}...") assertEquals(200, resp.status.value) // Wait for the status to change to PENDING_USER_TRANSFER_START - waitForTxnStatus(withdrawTxn.id, PENDING_USER_TRANSFER_START, token) + waitForTxnStatus(withdrawTxn.id, PENDING_USER_TRANSFER_START, ERROR, token) // Submit transfer transaction val walletTxn = (anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction) - val transfer = - wallet - .stellar() - .transaction(walletTxn.from!!) - .transferWithdrawalTransaction(walletTxn, asset) - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) + transactionWithRetry { + val transfer = + wallet + .stellar() + .transaction(walletTxn.from!!) + .transferWithdrawalTransaction(walletTxn, asset) + .build() + transfer.sign(keypair) + wallet.stellar().submitTransaction(transfer) + } // Wait for the status to change to PENDING_USER_TRANSFER_END - waitForTxnStatus(withdrawTxn.id, COMPLETED, token) + waitForTxnStatus(withdrawTxn.id, COMPLETED, ERROR, token) // Check if the transaction can be listed by stellar transaction id val fetchTxn = @@ -232,10 +234,7 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { var retries = 5 var callbacks: List? = null while (retries > 0) { - callbacks = - walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java).distinctBy { - it.transaction.status - } + callbacks = walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java) if (callbacks.size == count) { return callbacks } @@ -265,6 +264,7 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { private suspend fun waitForTxnStatus( id: String, expectedStatus: TransactionStatus, + exitStatus: TransactionStatus, token: AuthToken ) { var status: TransactionStatus? = null @@ -272,20 +272,18 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { for (i in 0..maxTries) { // Get transaction info val transaction = anchor.interactive().getTransaction(id, token) - if (status != transaction.status) { status = transaction.status - info( "Transaction(id=${transaction.id}) status changed to $status. Message: ${transaction.message}" ) } - delay(1.seconds) + if (transaction.status == expectedStatus) return - if (transaction.status == expectedStatus) { - return - } + if (transaction.status == exitStatus) break + + delay(1.seconds) } fail("Transaction wasn't $expectedStatus in $maxTries tries, last status: $status") @@ -297,41 +295,39 @@ class Sep24RpcEnd2EndTests(config: TestConfig, val jwt: String) { ) = runBlocking { val newAcc = wallet.stellar().account().createKeyPair() - val tx = - wallet - .stellar() - .transaction(keypair) - .sponsoring(keypair, newAcc) { - createAccount(newAcc) - addAssetSupport(USDC) - } - .build() - .sign(keypair) - .sign(newAcc) - - wallet.stellar().submitTransaction(tx) + transactionWithRetry { + val tx = + wallet + .stellar() + .transaction(keypair) + .sponsoring(keypair, newAcc) { + createAccount(newAcc) + addAssetSupport(USDC) + } + .build() + .sign(keypair) + .sign(newAcc) + + wallet.stellar().submitTransaction(tx) + } val token = anchor.auth().authenticate(newAcc) val deposits = (0..1).map { val txnId = makeDeposit(asset, amount, token).id - waitForTxnStatus(txnId, COMPLETED, token) + waitForTxnStatus(txnId, COMPLETED, ERROR, token) txnId } - val history = anchor.interactive().getHistory(asset, token) + val history = anchor.interactive().getTransactionsForAsset(asset, token) Assertions.assertThat(history).allMatch { deposits.contains(it.id) } } fun testAll() { info("Running SEP-24 USDC end-to-end tests...") - `test typical deposit end-to-end flow`(USDC, "1.1") - `test typical withdraw end-to-end flow`(USDC, "1.1") + `test typical deposit end-to-end flow`(USDC, "0.01") + `test typical withdraw end-to-end flow`(USDC, "0.01") `test created transactions show up in the get history call`(USDC, "1.1") - info("Running SEP-24 XLM end-to-end tests...") - `test typical deposit end-to-end flow`(XLM, "0.00001") - `test typical withdraw end-to-end flow`(XLM, "0.00001") - `test created transactions show up in the get history call`(XLM, "0.00001") } companion object { diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31RpcEnd2EndTests.kt b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31End2EndTests.kt similarity index 91% rename from integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31RpcEnd2EndTests.kt rename to extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31End2EndTests.kt index 1d4a504180..d5f16b78f3 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31RpcEnd2EndTests.kt +++ b/extended-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31End2EndTests.kt @@ -12,14 +12,21 @@ import org.junit.jupiter.api.Assertions.assertNotNull import org.stellar.anchor.api.callback.SendEventRequest import org.stellar.anchor.api.callback.SendEventRequestPayload import org.stellar.anchor.api.event.AnchorEvent -import org.stellar.anchor.api.event.AnchorEvent.Type.* +import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_CREATED +import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_STATUS_CHANGED import org.stellar.anchor.api.sep.SepTransactionStatus import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest import org.stellar.anchor.api.sep.sep31.Sep31GetTransactionResponse import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionRequest import org.stellar.anchor.apiclient.PlatformApiClient import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.* +import org.stellar.anchor.client.Sep12Client +import org.stellar.anchor.client.Sep31Client +import org.stellar.anchor.client.Sep38Client +import org.stellar.anchor.platform.CLIENT_WALLET_SECRET +import org.stellar.anchor.platform.TestConfig +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer1Json +import org.stellar.anchor.platform.integrationtest.Sep12Tests.Companion.testCustomer2Json import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.Log.info import org.stellar.anchor.util.MemoHelper @@ -35,7 +42,8 @@ import org.stellar.walletsdk.asset.StellarAssetId import org.stellar.walletsdk.horizon.SigningKeyPair import org.stellar.walletsdk.horizon.sign -class Sep31RpcEnd2EndTests(config: TestConfig, val toml: Sep1Helper.TomlContent, val jwt: String) { +/** TODO: This should be moved into essential tests */ +class Sep31End2EndTests(config: TestConfig, val toml: Sep1Helper.TomlContent, val jwt: String) { private val gson = GsonUtils.getInstance() private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET private val keypair = SigningKeyPair.fromSecret(walletSecretKey) @@ -163,10 +171,7 @@ class Sep31RpcEnd2EndTests(config: TestConfig, val toml: Sep1Helper.TomlContent, var retries = 5 var callbacks: List? = null while (retries > 0) { - callbacks = - walletServerClient.getCallbacks(txnId, Sep31GetTransactionResponse::class.java).distinctBy { - it.transaction.status - } + callbacks = walletServerClient.getCallbacks(txnId, Sep31GetTransactionResponse::class.java) if (callbacks.size == count) { return callbacks } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 976b4b7312..1e7ede0a34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ # Library versions abdera = "1.1.3" apache-commons-lang3 = "3.12.0" +assertj = "3.24.2" aws-iam-auth = "1.1.4" aws-rds = "1.12.248" aws-sqs = "1.12.200" @@ -23,7 +24,7 @@ h2database = "2.1.214" hibernate-types = "2.18.0" hoplite = "2.7.0" jackson-dataformat-yaml = "2.14.0" -java-stellar-sdk = "0.41.0" +java-stellar-sdk = "0.42.0" javax-jaxb-api = "2.3.1" javax-transaction-api = "1.3" jjwt = "0.9.1" @@ -34,7 +35,7 @@ kafka = "3.3.1" kafka-json-schema = "7.0.1" kotlin = "1.8.20" kotlin-logging = "3.0.2" -kotlinx-json = "1.5.0" +kotlinx-json = "1.5.1" ktor = "2.3.3" log4j = "2.19.0" log4j-template-json = "2.19.0" @@ -54,7 +55,7 @@ aws-java-sdk-s3 = "1.12.342" sqlite-jdbc = "3.34.0" slf4j = "1.7.35" slf4j2 = "2.0.5" -stellar-wallet-sdk = "0.11.0" +stellar-wallet-sdk = "1.0.0" toml4j = "0.7.2" # Plugin versions @@ -66,6 +67,7 @@ jacoco = "0.8.10" [libraries] abdera = { module = "org.apache.abdera:abdera-i18n", version.ref = "abdera" } apache-commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "apache-commons-lang3" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } aws-rds = { module = "com.amazonaws:aws-java-sdk-rds", version.ref = "aws-rds" } aws-sqs = { module = "com.amazonaws:aws-java-sdk-sqs", version.ref = "aws-sqs" } aws-iam-auth = { module = "software.amazon.msk:aws-msk-iam-auth", version.ref = "aws-iam-auth" } diff --git a/integration-tests/build.gradle.kts b/integration-tests/build.gradle.kts deleted file mode 100644 index f1d00ce0d9..0000000000 --- a/integration-tests/build.gradle.kts +++ /dev/null @@ -1,59 +0,0 @@ -// The alias call in plugins scope produces IntelliJ false error which is suppressed here. -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - `java-library` - alias(libs.plugins.spring.boot) - alias(libs.plugins.spring.dependency.management) - alias(libs.plugins.kotlin.jvm) -} - -repositories { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } - -dependencies { - implementation("org.springframework.boot:spring-boot") - implementation("org.springframework.boot:spring-boot-autoconfigure") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation( - libs.snakeyaml - ) // used to force the version of snakeyaml (used by springboot) to a safer one. - implementation("org.springframework.boot:spring-boot-starter-web") - - implementation(libs.commons.cli) - implementation(libs.dotenv) - implementation(variantOf(libs.java.stellar.sdk) { classifier("uber") }) - implementation(libs.google.gson) - implementation(libs.okhttp3) - implementation(libs.log4j2.api) - implementation(libs.log4j2.core) - implementation(libs.log4j2.slf4j) - implementation(libs.docker.compose.rule) - implementation(libs.stellar.wallet.sdk) - implementation(libs.kotlin.serialization.json) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.okhttp) - - // project dependencies - implementation(project(":api-schema")) - implementation(project(":core")) - implementation(project(":platform")) - implementation(project(":kotlin-reference-server")) - implementation(project(":wallet-reference-server")) - implementation(project(":service-runner")) - - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") - - testImplementation(libs.okhttp3.mockserver) - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation(libs.docker.compose.rule) - testImplementation(libs.dotenv) -} - -tasks { - bootJar { enabled = false } - test { - useJUnitPlatform() - // Setting forkEvery to 1 makes Gradle test execution to start a separeate JVM for each integration test classes. - // This is to to avoid the interaction between static states between each integration test classes. - setForkEvery(1) - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt deleted file mode 100644 index bec5e363d8..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AbstractIntegrationTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.stellar.anchor.platform - -import org.stellar.anchor.platform.test.* -import org.stellar.anchor.util.Sep1Helper - -open class AbstractIntegrationTest(private val config: TestConfig) { - companion object { - const val ANCHOR_TO_PLATFORM_SECRET = "myAnchorToPlatformSecret" - const val PLATFORM_TO_ANCHOR_SECRET = "myPlatformToAnchorSecret" - const val PLATFORM_TO_CUSTODY_SECRET = "myPlatformToCustodySecret" - const val PLATFORM_SERVER_PORT = 8085 - const val CUSTODY_SERVER_SERVER_PORT = 8086 - const val REFERENCE_SERVER_PORT = 8091 - const val JWT_EXPIRATION_MILLISECONDS = 10000L - } - - init { - System.getProperties() - .setProperty("REFERENCE_SERVER_CONFIG", "classpath:/anchor-reference-server.yaml") - } - - val testProfileRunner = TestProfileExecutor(config) - lateinit var sep6Tests: Sep6Tests - lateinit var sep10Tests: Sep10Tests - lateinit var sep12Tests: Sep12Tests - lateinit var sep24Tests: Sep24Tests - lateinit var sep31Tests: Sep31Tests - lateinit var sep38Tests: Sep38Tests - lateinit var sepHealthTests: SepHealthTests - lateinit var platformApiTests: PlatformApiTests - lateinit var platformApiCustodyTests: PlatformApiCustodyTests - lateinit var callbackApiTests: CallbackApiTests - lateinit var stellarObserverTests: StellarObserverTests - lateinit var custodyApiTests: CustodyApiTests - lateinit var eventProcessingServerTests: EventProcessingServerTests - lateinit var sep24E2eTests: Sep24End2EndTests - lateinit var sep6E2eTests: Sep6End2EndTest - lateinit var sep24RpcE2eTests: Sep24RpcEnd2EndTests - lateinit var sep24CustodyE2eTests: Sep24CustodyEnd2EndTests - lateinit var sep24CustodyRpcE2eTests: Sep24CustodyRpcEnd2EndTests - lateinit var sep31RpcE2eTests: Sep31RpcEnd2EndTests - lateinit var sep31CustodyRpcE2eTests: Sep31CustodyRpcEnd2EndTests - - fun setUp(envMap: Map) { - envMap.forEach { (key, value) -> config.env[key] = value } - testProfileRunner.start() - setupTests() - } - - fun tearDown() { - testProfileRunner.shutdown() - } - - private fun setupTests() { - // Query SEP-1 - val toml = - Sep1Helper.parse(resourceAsString("${config.env["anchor.domain"]}/.well-known/stellar.toml")) - - // Create Sep10Tests - sep10Tests = Sep10Tests(toml) - - // Get JWT - val jwt = sep10Tests.sep10Client.auth() - - sep6Tests = Sep6Tests(toml, jwt) - sep12Tests = Sep12Tests(config, toml, jwt) - sep24Tests = Sep24Tests(config, toml, jwt) - sep31Tests = Sep31Tests(config, toml, jwt) - sep38Tests = Sep38Tests(config, toml, jwt) - sepHealthTests = SepHealthTests(config, toml, jwt) - platformApiTests = PlatformApiTests(config, toml, jwt) - platformApiCustodyTests = PlatformApiCustodyTests(config, toml, jwt) - callbackApiTests = CallbackApiTests(config, toml, jwt) - stellarObserverTests = StellarObserverTests() - custodyApiTests = CustodyApiTests(config, toml, jwt) - sep24E2eTests = Sep24End2EndTests(config, jwt) - sep6E2eTests = Sep6End2EndTest(config, jwt) - sep24CustodyE2eTests = Sep24CustodyEnd2EndTests(config, jwt) - sep24RpcE2eTests = Sep24RpcEnd2EndTests(config, jwt) - sep24CustodyRpcE2eTests = Sep24CustodyRpcEnd2EndTests(config, jwt) - sep31RpcE2eTests = Sep31RpcEnd2EndTests(config, toml, jwt) - sep31CustodyRpcE2eTests = Sep31CustodyRpcEnd2EndTests(config, toml, jwt) - eventProcessingServerTests = EventProcessingServerTests(config, toml, jwt) - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyApiRpcEnd2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyApiRpcEnd2EndTest.kt deleted file mode 100644 index e26213ecfd..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyApiRpcEnd2EndTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.stellar.anchor.platform - -import org.junit.jupiter.api.* - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class AnchorPlatformCustodyApiRpcEnd2EndTest : - AbstractIntegrationTest(TestConfig(testProfileName = "default-custody-rpc")) { - - companion object { - private val singleton = AnchorPlatformCustodyApiRpcEnd2EndTest() - - @BeforeAll - @JvmStatic - fun construct() { - println("Running AnchorPlatformCustodyApiRpcEnd2EndTest") - singleton.setUp(mapOf()) - } - - @AfterAll - @JvmStatic - fun destroy() { - singleton.tearDown() - } - } - - @Test - @Order(1) - fun runSep24Test() { - singleton.sep24CustodyRpcE2eTests.testAll() - } - - @Test - @Order(11) - fun runSep31Test() { - singleton.sep31CustodyRpcE2eTests.testAll() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt deleted file mode 100644 index b2e49eb590..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.stellar.anchor.platform - -import org.junit.jupiter.api.* - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class AnchorPlatformCustodyEnd2EndTest : - AbstractIntegrationTest(TestConfig(testProfileName = "default-custody")) { - - companion object { - private val singleton = AnchorPlatformCustodyEnd2EndTest() - - @BeforeAll - @JvmStatic - fun construct() { - println("Running AnchorPlatformCustodyEnd2EndTest") - singleton.setUp(mapOf()) - } - - @AfterAll - @JvmStatic - fun destroy() { - singleton.tearDown() - } - } - - @Test - @Order(1) - fun runSep24Test() { - singleton.sep24CustodyE2eTests.testAll() - } - - @Test - @Order(11) - fun runSep6Test() { - // The SEP-6 reference server implementation only implements RPC, so technically this test - // should be in the RPC test suite. - singleton.sep6E2eTests.testAll() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt deleted file mode 100644 index 8d38b58316..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.stellar.anchor.platform - -import org.junit.jupiter.api.* - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class AnchorPlatformEnd2EndTest : AbstractIntegrationTest(TestConfig(testProfileName = "default")) { - - companion object { - private val singleton = AnchorPlatformEnd2EndTest() - - @BeforeAll - @JvmStatic - fun construct() { - println("Running AnchorPlatformEnd2EndTest") - singleton.setUp(mapOf()) - } - - @AfterAll - @JvmStatic - fun destroy() { - singleton.tearDown() - } - } - - @Test - @Order(1) - fun runSep24Test() { - singleton.sep24E2eTests.testAll() - } - - @Test - @Order(2) - fun runSep6Test() { - // The SEP-6 reference server implementation only implements RPC, so technically this test - // should be in the RPC test suite. - singleton.sep6E2eTests.testAll() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformIntegrationTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformIntegrationTest.kt deleted file mode 100644 index 0d8136d508..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformIntegrationTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.stellar.anchor.platform - -import kotlinx.coroutines.* -import okhttp3.mockwebserver.MockWebServer -import org.junit.jupiter.api.* -import org.stellar.anchor.platform.test.* - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class AnchorPlatformIntegrationTest : - AbstractIntegrationTest(TestConfig(testProfileName = "default")) { - companion object { - private val singleton = AnchorPlatformIntegrationTest() - private val custodyMockServer = MockWebServer() - - @BeforeAll - @JvmStatic - fun construct() { - println("Running AnchorPlatformIntegrationTest") - custodyMockServer.start() - val mockServerUrl = custodyMockServer.url("").toString() - singleton.setUp(mapOf("custody.fireblocks.base_url" to mockServerUrl)) - } - - @AfterAll - @JvmStatic - fun destroy() { - custodyMockServer.shutdown() - singleton.tearDown() - } - } - - @Test - @Order(11) - fun runSep10Test() { - singleton.sep10Tests.testAll() - } - - @Test - @Order(12) - fun runSep12Test() { - singleton.sep12Tests.testAll() - } - - @Test - @Order(13) - fun runSep24Test() { - singleton.sep24Tests.testAll() - } - - @Test - @Order(14) - fun runSep31Test() { - singleton.sep31Tests.testAll() - } - - @Test - @Order(15) - fun runSep38Test() { - singleton.sep38Tests.testAll() - } - - @Test - @Order(16) - fun runSep6Test() { - singleton.sep6Tests.testAll() - } - - @Test - @Order(21) - fun runSepHealthTest() { - singleton.sepHealthTests.testAll() - } - - @Test - @Order(31) - fun runPlatformApiTest() { - singleton.platformApiTests.testAll() - } - - @Test - @Order(41) - fun runCallbackApiTest() { - singleton.callbackApiTests.testAll() - } - - @Test - @Order(51) - fun runStellarObserverTest() { - singleton.stellarObserverTests.testAll() - } - - @Test - @Order(61) - fun runEventProcessingServerTest() { - singleton.eventProcessingServerTests.testAll() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AuthIntegrationTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AuthIntegrationTest.kt deleted file mode 100644 index fe59f6331b..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AuthIntegrationTest.kt +++ /dev/null @@ -1,514 +0,0 @@ -package org.stellar.anchor.platform - -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import org.junit.jupiter.api.* -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.stellar.anchor.api.callback.GetCustomerRequest -import org.stellar.anchor.api.callback.GetFeeRequest -import org.stellar.anchor.api.callback.GetRateRequest -import org.stellar.anchor.api.custody.CreateCustodyTransactionRequest -import org.stellar.anchor.api.exception.* -import org.stellar.anchor.apiclient.PlatformApiClient -import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.auth.JwtService -import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.ANCHOR_TO_PLATFORM_SECRET -import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.CUSTODY_SERVER_SERVER_PORT -import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.JWT_EXPIRATION_MILLISECONDS -import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.PLATFORM_TO_ANCHOR_SECRET -import org.stellar.anchor.platform.AbstractIntegrationTest.Companion.PLATFORM_TO_CUSTODY_SECRET -import org.stellar.anchor.platform.callback.RestCustomerIntegration -import org.stellar.anchor.platform.callback.RestFeeIntegration -import org.stellar.anchor.platform.callback.RestRateIntegration -import org.stellar.anchor.platform.config.CustodyApiConfig -import org.stellar.anchor.platform.config.PropertyCustodySecretConfig -import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.OkHttpUtil - -const val GET_TRANSACTIONS_ENDPOINT = "GET,/transactions" -const val PATCH_TRANSACTIONS_ENDPOINT = "PATCH,/transactions" -const val GET_TRANSACTIONS_MY_ID_ENDPOINT = "GET,/transactions/my_id" -const val GET_EXCHANGE_QUOTES_ENDPOINT = "GET,/exchange/quotes" -const val GET_EXCHANGE_QUOTES_ID_ENDPOINT = "GET,/exchange/quotes/id" -const val POST_CUSTODY_TRANSACTION_ENDPOINT = "POST,/transactions" - -open class AbstractAuthIntegrationTest { - companion object { - private val jwtService = - JwtService( - null, - null, - null, - PLATFORM_TO_ANCHOR_SECRET, - ANCHOR_TO_PLATFORM_SECRET, - PLATFORM_TO_CUSTODY_SECRET - ) - private val jwtWrongKeyService = - JwtService( - null, - null, - null, - PLATFORM_TO_ANCHOR_SECRET + "bad", - ANCHOR_TO_PLATFORM_SECRET + "bad", - PLATFORM_TO_CUSTODY_SECRET + "bad" - ) - - internal val jwtAuthHelper = AuthHelper.forJwtToken(jwtService, 10000) - internal val jwtWrongKeyAuthHelper = AuthHelper.forJwtToken(jwtWrongKeyService, 10000) - internal val jwtExpiredAuthHelper = AuthHelper.forJwtToken(jwtService, 0) - internal lateinit var testProfileRunner: TestProfileExecutor - } -} - -internal class PlatformJwtAuthIntegrationTest : AbstractAuthIntegrationTest() { - companion object { - @BeforeAll - @JvmStatic - fun setup() { - println("Running PlatformJwtAuthIntegrationTest") - testProfileRunner = - TestProfileExecutor( - TestConfig(testProfileName = "default").also { - // enable platform server jwt auth - it.env["platform_server.auth.type"] = "JWT" - // enable business server callback auth - it.env["auth.type"] = "JWT" - it.env["auth.platformToAnchorSecret"] = PLATFORM_TO_ANCHOR_SECRET - it.env["auth.anchorToPlatformSecret"] = ANCHOR_TO_PLATFORM_SECRET - it.env["auth.expirationMilliseconds"] = JWT_EXPIRATION_MILLISECONDS.toString() - } - ) - testProfileRunner.start() - } - - @AfterAll - @JvmStatic - fun breakdown() { - testProfileRunner.shutdown() - } - } - - // TODO - to be deprecated by platformAPI client - private val httpClient: OkHttpClient = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.MINUTES) - .readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES) - .build() - - private val jwtPlatformClient: PlatformApiClient = - PlatformApiClient(jwtAuthHelper, "http://localhost:8085") - private val jwtWrongKeyPlatformClient: PlatformApiClient = - PlatformApiClient(jwtWrongKeyAuthHelper, "http://localhost:8085") - private val jwtExpiredTokenPlatformClient: PlatformApiClient = - PlatformApiClient(jwtExpiredAuthHelper, "http://localhost:8085") - - @ParameterizedTest - @CsvSource( - value = - [ - GET_TRANSACTIONS_ENDPOINT, - PATCH_TRANSACTIONS_ENDPOINT, - GET_TRANSACTIONS_MY_ID_ENDPOINT, - GET_EXCHANGE_QUOTES_ENDPOINT, - GET_EXCHANGE_QUOTES_ID_ENDPOINT - ] - ) - fun `test the platform endpoints with JWT auth`(method: String, endpoint: String) { - // Assert the request does not throw a 403. - // As for the correctness of the request/response, it should be tested in the platform server - // integration tests. - assertThrows { jwtPlatformClient.getTransaction("my_id") } - } - - @ParameterizedTest - @CsvSource( - value = - [ - GET_TRANSACTIONS_ENDPOINT, - PATCH_TRANSACTIONS_ENDPOINT, - GET_TRANSACTIONS_MY_ID_ENDPOINT, - GET_EXCHANGE_QUOTES_ENDPOINT, - GET_EXCHANGE_QUOTES_ID_ENDPOINT - ] - ) - fun `test JWT protection of the platform server`(method: String, endpoint: String) { - // Check if the request without JWT will cause a 403. - val httpRequest = - Request.Builder() - .url("http://localhost:8085/transactions") - .header("Content-Type", "application/json") - .method(method, getPlatformDummyRequestBody(method)) - .build() - val response = httpClient.newCall(httpRequest).execute() - assertEquals(403, response.code) - - // Check if the wrong JWT key will cause a 403. - assertThrows { jwtWrongKeyPlatformClient.getTransaction("my_id") } - assertThrows { - jwtExpiredTokenPlatformClient.getTransaction("my_id") - } - } - - private fun getPlatformDummyRequestBody(method: String): RequestBody? { - return if (method != "PATCH") null - else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) - } - - @Test - fun `test the callback customer endpoint with JWT auth`() { - val rci = - RestCustomerIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtAuthHelper, - gson - ) - // Assert the request does not throw a 403. - assertThrows { - rci.getCustomer(GetCustomerRequest.builder().id("1").build()) - } - } - - @Test - fun `test the callback rate endpoint with JWT auth`() { - val rri = - RestRateIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtAuthHelper, - gson - ) - // Assert the request does not throw a 403. - assertThrows { rri.getRate(GetRateRequest.builder().build()) } - } - - @Test - fun `test the callback fee endpoint with JWT auth`() { - val rfi = - RestFeeIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtAuthHelper, - gson - ) - // Assert the request does not throw a 403. - assertThrows { rfi.getFee(GetFeeRequest.builder().build()) } - } - - @Test - fun `test JWT protection of callback customer endpoint`() { - val badTokenClient = - RestCustomerIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtWrongKeyAuthHelper, - gson - ) - assertThrows { - badTokenClient.getCustomer(GetCustomerRequest.builder().id("1").build()) - } - - val expiredTokenClient = - RestCustomerIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtWrongKeyAuthHelper, - gson - ) - assertThrows { - expiredTokenClient.getCustomer(GetCustomerRequest.builder().id("1").build()) - } - } - - @Test - fun `test JWT protection of callback rate endpoint`() { - val badTokenClient = - RestRateIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtWrongKeyAuthHelper, - gson - ) - assertThrows { badTokenClient.getRate(GetRateRequest.builder().build()) } - - val expiredTokenClient = - RestRateIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtExpiredAuthHelper, - gson - ) - assertThrows { - expiredTokenClient.getRate(GetRateRequest.builder().build()) - } - } - - @Test - fun `test JWT protection of callback fee endpoint with bad token`() { - val badTokenClient = - RestFeeIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtWrongKeyAuthHelper, - gson - ) - assertThrows { badTokenClient.getFee(GetFeeRequest.builder().build()) } - - val expiredTokenClient = - RestFeeIntegration( - "http://localhost:${AbstractIntegrationTest.REFERENCE_SERVER_PORT}", - httpClient, - jwtExpiredAuthHelper, - gson - ) - assertThrows { - expiredTokenClient.getFee(GetFeeRequest.builder().build()) - } - } -} - -internal class CustodyJwtAuthIntegrationTest : AbstractAuthIntegrationTest() { - companion object { - @BeforeAll - @JvmStatic - fun setup() { - println("Running CustodyJwtAuthIntegrationTest") - testProfileRunner = - TestProfileExecutor( - TestConfig(testProfileName = "default-custody").also { - // enable custody server jwt auth - it.env["custody_server.auth.type"] = "jwt" - } - ) - testProfileRunner.start() - } - - @AfterAll - @JvmStatic - fun breakdown() { - testProfileRunner.shutdown() - } - } - - private val httpClient: OkHttpClient = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.MINUTES) - .readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES) - .build() - - private val custodyApiConfig: CustodyApiConfig = - CustodyApiConfig(PropertyCustodySecretConfig()).apply { baseUrl = "http://localhost:8086" } - - private val jwtCustodyClient: org.stellar.anchor.platform.apiclient.CustodyApiClient = - org.stellar.anchor.platform.apiclient.CustodyApiClient( - httpClient, - jwtAuthHelper, - custodyApiConfig - ) - private val jwtWrongKeyCustodyClient: org.stellar.anchor.platform.apiclient.CustodyApiClient = - org.stellar.anchor.platform.apiclient.CustodyApiClient( - httpClient, - jwtWrongKeyAuthHelper, - custodyApiConfig - ) - private val jwtExpiredTokenCustodyClient: org.stellar.anchor.platform.apiclient.CustodyApiClient = - org.stellar.anchor.platform.apiclient.CustodyApiClient( - httpClient, - jwtExpiredAuthHelper, - custodyApiConfig - ) - - @Test - fun `test the custody endpoints with JWT auth`() { - // Assert the request does not throw a 403. - // As for the correctness of the request/response, it should be tested in the custody server - // integration tests. - jwtCustodyClient.createTransaction(getCustodyDummyRequest()) - } - - @ParameterizedTest - @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) - fun `test JWT protection of the custody server`(method: String, endpoint: String) { - // Check if the request without JWT will cause a 403. - val httpRequest = - Request.Builder() - .url("http://localhost:${CUSTODY_SERVER_SERVER_PORT}${endpoint}") - .header("Content-Type", "application/json") - .method(method, getCustodyDummyRequestBody()) - .build() - val response = httpClient.newCall(httpRequest).execute() - assertEquals(403, response.code) - - // Check if the wrong JWT key will cause a 403. - assertThrows { - jwtWrongKeyCustodyClient.createTransaction(getCustodyDummyRequest()) - } - assertThrows { - jwtExpiredTokenCustodyClient.createTransaction(getCustodyDummyRequest()) - } - } - - private fun getCustodyDummyRequest(): CreateCustodyTransactionRequest { - return CreateCustodyTransactionRequest.builder().id("testId").build() - } - - private fun getCustodyDummyRequestBody(): RequestBody? { - return OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("id" to "testId"))) - } -} - -internal class PlatformApiKeyAuthIntegrationTest : AbstractAuthIntegrationTest() { - companion object { - @BeforeAll - @JvmStatic - fun setup() { - println("Running PlatformApiKeyAuthIntegrationTest") - testProfileRunner = - TestProfileExecutor( - TestConfig(testProfileName = "default").also { - // enable platform server api_key auth - it.env["platform_server.auth.type"] = "api_key" - } - ) - testProfileRunner.start() - } - - @AfterAll - @JvmStatic - fun breakdown() { - testProfileRunner.shutdown() - } - } - - private val gson = GsonUtils.getInstance() - private val httpClient: OkHttpClient = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.MINUTES) - .readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES) - .build() - - @ParameterizedTest - @CsvSource( - value = - [ - GET_TRANSACTIONS_ENDPOINT, - PATCH_TRANSACTIONS_ENDPOINT, - GET_TRANSACTIONS_MY_ID_ENDPOINT, - GET_EXCHANGE_QUOTES_ENDPOINT, - GET_EXCHANGE_QUOTES_ID_ENDPOINT - ] - ) - fun `test API_KEY auth protection of the platform server`(method: String, endpoint: String) { - val httpRequest = - Request.Builder() - .url("http://localhost:${AbstractIntegrationTest.PLATFORM_SERVER_PORT}$endpoint") - .header("Content-Type", "application/json") - .method(method, getPlatformDummyRequestBody(method)) - .build() - val response = httpClient.newCall(httpRequest).execute() - assertEquals(403, response.code) - } - - @ParameterizedTest - @CsvSource( - value = - [ - GET_TRANSACTIONS_ENDPOINT, - PATCH_TRANSACTIONS_ENDPOINT, - GET_TRANSACTIONS_MY_ID_ENDPOINT, - GET_EXCHANGE_QUOTES_ENDPOINT, - GET_EXCHANGE_QUOTES_ID_ENDPOINT - ] - ) - fun `test the platform endpoints with API_KEY auth`(method: String, endpoint: String) { - val httpRequest = - Request.Builder() - .url("http://localhost:${AbstractIntegrationTest.PLATFORM_SERVER_PORT}$endpoint") - .header("Content-Type", "application/json") - .header("X-Api-Key", ANCHOR_TO_PLATFORM_SECRET) - .method(method, getPlatformDummyRequestBody(method)) - .build() - val response = httpClient.newCall(httpRequest).execute() - Assertions.assertNotEquals(403, response.code) - } - - private fun getPlatformDummyRequestBody(method: String): RequestBody? { - return if (method != "PATCH") null - else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) - } -} - -internal class CustodyApiKeyAuthIntegrationTest : AbstractAuthIntegrationTest() { - companion object { - @BeforeAll - @JvmStatic - fun setup() { - println("Running CustodyApiKeyAuthIntegrationTest") - testProfileRunner = - TestProfileExecutor( - TestConfig(testProfileName = "default-custody").also { - // enable custody server api_key auth - it.env["custody_server.auth.type"] = "api_key" - } - ) - testProfileRunner.start() - } - - @AfterAll - @JvmStatic - fun breakdown() { - testProfileRunner.shutdown() - } - } - - private val gson = GsonUtils.getInstance() - private val httpClient: OkHttpClient = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.MINUTES) - .readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES) - .build() - - @ParameterizedTest - @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) - fun test_incomingCustodyAuth_emptyApiKey_authFails(method: String, endpoint: String) { - val httpRequest = - Request.Builder() - .url("http://localhost:${CUSTODY_SERVER_SERVER_PORT}$endpoint") - .header("Content-Type", "application/json") - .method(method, getCustodyDummyRequestBody()) - .build() - val response = httpClient.newCall(httpRequest).execute() - assertEquals(403, response.code) - } - - @ParameterizedTest - @CsvSource(value = [POST_CUSTODY_TRANSACTION_ENDPOINT]) - fun test_incomingCustodyAuth_emptyApiKey_authPasses(method: String, endpoint: String) { - val httpRequest = - Request.Builder() - .url("http://localhost:${CUSTODY_SERVER_SERVER_PORT}$endpoint") - .header("Content-Type", "application/json") - .header("X-Api-Key", PLATFORM_TO_CUSTODY_SECRET) - .method(method, getCustodyDummyRequestBody()) - .build() - val response = httpClient.newCall(httpRequest).execute() - assertEquals(200, response.code) - } - - private fun getPlatformDummyRequestBody(method: String): RequestBody? { - return if (method != "PATCH") null - else OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("proposedAssetsJson" to "bar"))) - } - - private fun getCustodyDummyRequestBody(): RequestBody? { - return OkHttpUtil.buildJsonRequestBody(gson.toJson(mapOf("id" to "testId"))) - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/KotlinReferenceServerIntegrationTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/KotlinReferenceServerIntegrationTest.kt deleted file mode 100644 index 8a8b7886cb..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/KotlinReferenceServerIntegrationTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.stellar.anchor.platform - -import io.ktor.http.* -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.skyscreamer.jsonassert.JSONAssert -import org.stellar.anchor.api.callback.SendEventRequest -import org.stellar.anchor.util.StringHelper.json -import org.stellar.reference.client.AnchorReferenceServerClient - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class KotlinReferenceServerIntegrationTest { - private val testProfileRunner = - TestProfileExecutor( - TestConfig("default").also { - it.env[RUN_DOCKER] = "false" - it.env[RUN_ALL_SERVERS] = "false" - it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" - } - ) - - @BeforeAll - fun setup() { - println("Running KotlinReferenceServerIntegrationTest") - testProfileRunner.start() - } - - @AfterAll - fun destroy() { - testProfileRunner.shutdown() - } - - @Test - fun `test if the reference server records the events sent by sendEvent() method`() { - val client = AnchorReferenceServerClient(Url("http://localhost:8091")) - val sendEventRequest1 = gson.fromJson(sendEventRequestJson, SendEventRequest::class.java) - val sendEventRequest2 = gson.fromJson(sendEventRequestJson2, SendEventRequest::class.java) - runBlocking { - // Send event1 - client.sendEvent(sendEventRequest1) - var latestEvent = client.getLatestEvent() - Assertions.assertNotNull(latestEvent) - JSONAssert.assertEquals(json(latestEvent), json(sendEventRequest1), true) - // send event2 - client.sendEvent(sendEventRequest2) - latestEvent = client.getLatestEvent() - Assertions.assertNotNull(latestEvent) - JSONAssert.assertEquals(json(latestEvent), json(sendEventRequest2), true) - // check if there are totally two events recorded - assertEquals(client.getEvents().size, 2) - JSONAssert.assertEquals(json(client.getEvents()[0]), json(sendEventRequest1), true) - JSONAssert.assertEquals(json(client.getEvents()[1]), json(sendEventRequest2), true) - } - } - - companion object { - val sendEventRequestJson = - """ - { - "timestamp": "2011-10-05T14:48:00.000Z", - "id": "2a419880-0dde-4821-90cb-f3bfcb671ea3", - "type": "transaction_created", - "payload": { - "transaction": { - "amount_in": { - "amount": "10.0", - "asset": "USDC" - } - } - } - } - """ - .trimIndent() - - val sendEventRequestJson2 = - """ -{ - "id": "2a419880-0dde-4821-90cb-f3bfcb671ea3", - "timestamp": "2011-10-05T14:48:00.000Z", - "type": "quote_created", - "payload": { - "quote": { - "sell_amount": "10.0", - "buy_amount": "1" - } - } -} - """ - .trimIndent() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep12Tests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep12Tests.kt deleted file mode 100644 index 22ae3f78eb..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep12Tests.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.stellar.anchor.platform.test - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.assertThrows -import org.stellar.anchor.api.exception.SepNotFoundException -import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest -import org.stellar.anchor.api.sep.sep12.Sep12Status -import org.stellar.anchor.platform.* -import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Sep1Helper - -const val testCustomer1Json = - """ -{ - "first_name": "John", - "last_name": "Doe", - "email_address": "johndoe@test.com", - "address": "123 Washington Street", - "city": "San Francisco", - "state_or_province": "CA", - "address_country_code": "US", - "clabe_number": "1234", - "bank_number": "abcd", - "bank_account_number": "1234", - "bank_account_type": "checking" -} -""" - -const val testCustomer2Json = - """ -{ - "first_name": "Jane", - "last_name": "Doe", - "email_address": "janedoe@test.com", - "address": "321 Washington Street", - "city": "San Francisco", - "state_or_province": "CA", - "address_country_code": "US", - "clabe_number": "5678", - "bank_number": "efgh", - "bank_account_number": "5678", - "bank_account_type": "checking" -} -""" - -class Sep12Tests(config: TestConfig, toml: Sep1Helper.TomlContent, jwt: String) { - private lateinit var sep12Client: Sep12Client - - init { - println("Performing SEP12 tests...") - if (!::sep12Client.isInitialized) { - sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt) - } - } - - fun `test put, get customers`() { - val customer = - GsonUtils.getInstance().fromJson(testCustomer1Json, Sep12PutCustomerRequest::class.java) - customer.emailAddress = null - - // Upload a customer - printRequest("Calling PUT /customer", customer) - var pr = sep12Client.putCustomer(customer) - printResponse(pr) - - // make sure the customer was uploaded correctly. - printRequest("Calling GET /customer", customer) - var gr = sep12Client.getCustomer(pr!!.id) - printResponse(gr) - - assertEquals(pr.id, gr?.id) - - customer.emailAddress = "john.doe@stellar.org" - customer.type = "sep31-receiver" - - // Modify the customer - printRequest("Calling PUT /customer", customer) - pr = sep12Client.putCustomer(customer) - printResponse(pr) - - // Make sure the customer is modified correctly. - printRequest("Calling GET /customer", customer) - gr = sep12Client.getCustomer(pr!!.id) - printResponse(gr) - - assertEquals(pr.id, gr?.id) - assertEquals(Sep12Status.ACCEPTED, gr?.status) - - // Delete the customer - printRequest("Calling DELETE /customer/$CLIENT_WALLET_ACCOUNT") - val code = sep12Client.deleteCustomer(CLIENT_WALLET_ACCOUNT) - printResponse(code) - // currently, not implemented - assertEquals(200, code) - - val id = pr.id - val ex: SepNotFoundException = assertThrows { sep12Client.getCustomer(id) } - assertEquals("customer for 'id' '$id' not found", ex.message) - println(ex) - } - - fun testAll() { - println("Performing Sep12 tests...") - `test put, get customers`() - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyEnd2EndTests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyEnd2EndTests.kt deleted file mode 100644 index 43c5d6853f..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyEnd2EndTests.kt +++ /dev/null @@ -1,347 +0,0 @@ -package org.stellar.anchor.platform.test - -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.fail -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Assertions.assertNotNull -import org.springframework.web.util.UriComponentsBuilder -import org.stellar.anchor.api.callback.SendEventRequest -import org.stellar.anchor.api.callback.SendEventRequestPayload -import org.stellar.anchor.api.event.AnchorEvent -import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_CREATED -import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_STATUS_CHANGED -import org.stellar.anchor.api.sep.SepTransactionStatus -import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse -import org.stellar.anchor.auth.JwtService -import org.stellar.anchor.auth.Sep24InteractiveUrlJwt -import org.stellar.anchor.platform.CLIENT_WALLET_SECRET -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.util.Log.info -import org.stellar.reference.client.AnchorReferenceServerClient -import org.stellar.reference.wallet.WalletServerClient -import org.stellar.walletsdk.ApplicationConfiguration -import org.stellar.walletsdk.InteractiveFlowResponse -import org.stellar.walletsdk.StellarConfiguration -import org.stellar.walletsdk.Wallet -import org.stellar.walletsdk.anchor.* -import org.stellar.walletsdk.anchor.TransactionStatus.* -import org.stellar.walletsdk.asset.IssuedAssetId -import org.stellar.walletsdk.asset.StellarAssetId -import org.stellar.walletsdk.asset.XLM -import org.stellar.walletsdk.auth.AuthToken -import org.stellar.walletsdk.horizon.SigningKeyPair -import org.stellar.walletsdk.horizon.sign -import org.stellar.walletsdk.horizon.transaction.transferWithdrawalTransaction - -class Sep24CustodyEnd2EndTests(config: TestConfig, val jwt: String) { - private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET - private val keypair = SigningKeyPair.fromSecret(walletSecretKey) - private val wallet = - Wallet( - StellarConfiguration.Testnet, - ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } - ) - private val client = HttpClient { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } - private val anchor = - wallet.anchor(config.env["anchor.domain"]!!) { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } - private val maxTries = 90 - private val anchorReferenceServerClient = - AnchorReferenceServerClient(Url(config.env["reference.server.url"]!!)) - private val walletServerClient = WalletServerClient(Url(config.env["wallet.server.url"]!!)) - private val jwtService: JwtService = - JwtService( - config.env["secret.sep10.jwt_secret"]!!, - config.env["secret.sep24.interactive_url.jwt_secret"]!!, - config.env["secret.sep24.more_info_url.jwt_secret"]!!, - config.env["secret.callback_api.auth_secret"]!!, - config.env["secret.platform_api.auth_secret"]!!, - config.env["secret.custody_server.auth_secret"]!! - ) - - private fun `test typical deposit end-to-end flow`(asset: StellarAssetId, amount: String) = - runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val token = anchor.auth().authenticate(keypair) - val response = makeDeposit(asset, amount, token) - - // Assert the interactive URL JWT is valid - val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams - val cipher = params["token"]!![0] - val interactiveJwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) - assertEquals("referenceCustodial", interactiveJwt.claims[JwtService.CLIENT_NAME]) - - // Wait for the status to change to COMPLETED - waitForTxnStatus(response.id, COMPLETED, token) - - // Check if the transaction can be listed by stellar transaction id - val fetchedTxn = anchor.interactive().getTransaction(response.id, token) as DepositTransaction - val transactionByStellarId = - anchor - .interactive() - .getTransactionBy(token, stellarTransactionId = fetchedTxn.stellarTransactionId) - assertEquals(fetchedTxn.id, transactionByStellarId.id) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(response.id, 5) - assertEvents(actualEvents, expectedStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(response.id, 5) - assertCallbacks(actualCallbacks, expectedStatuses) - } - - private suspend fun makeDeposit( - asset: StellarAssetId, - amount: String, - token: AuthToken - ): InteractiveFlowResponse { - // Start interactive deposit - val deposit = anchor.interactive().deposit(asset, token, mapOf("amount" to amount)) - - // Get transaction status and make sure it is INCOMPLETE - val transaction = anchor.interactive().getTransaction(deposit.id, token) - assertEquals(INCOMPLETE, transaction.status) - // Make sure the interactive url is valid. This will also start the reference server's - // withdrawal process. - val resp = client.get(deposit.url) - info("accessing ${deposit.url}...") - assertEquals(200, resp.status.value) - - return deposit - } - - private fun assertEvents( - actualEvents: List?, - expectedStatuses: List> - ) { - assertNotNull(actualEvents) - actualEvents?.let { - assertEquals(expectedStatuses.size, actualEvents.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualEvents[index].let { actualEvent -> - assertNotNull(actualEvent.id) - assertNotNull(actualEvent.timestamp) - assertEquals(expectedStatus.first.type, actualEvent.type) - org.junit.jupiter.api.Assertions.assertTrue( - actualEvent.payload is SendEventRequestPayload - ) - assertEquals(expectedStatus.second, actualEvent.payload.transaction.status) - } - } - } - } - - private fun assertCallbacks( - actualCallbacks: List?, - expectedStatuses: List> - ) { - assertNotNull(actualCallbacks) - actualCallbacks?.let { - assertEquals(expectedStatuses.size, actualCallbacks.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualCallbacks[index].let { actualCallback -> - assertNotNull(actualCallback.transaction.id) - assertEquals(expectedStatus.second.status, actualCallback.transaction.status) - } - } - } - } - - private fun `test typical withdraw end-to-end flow`(asset: StellarAssetId, amount: String) { - `test typical withdraw end-to-end flow`(asset, mapOf("amount" to amount)) - } - - private fun `test typical withdraw end-to-end flow`( - asset: StellarAssetId, - extraFields: Map - ) = runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val token = anchor.auth().authenticate(keypair) - val withdrawTxn = anchor.interactive().withdraw(asset, token, extraFields) - - // Get transaction status and make sure it is INCOMPLETE - val transaction = anchor.interactive().getTransaction(withdrawTxn.id, token) - assertEquals(INCOMPLETE, transaction.status) - // Make sure the interactive url is valid. This will also start the reference server's - // withdrawal process. - val resp = client.get(withdrawTxn.url) - info("accessing ${withdrawTxn.url}...") - assertEquals(200, resp.status.value) - // Wait for the status to change to PENDING_USER_TRANSFER_START - waitForTxnStatus(withdrawTxn.id, PENDING_USER_TRANSFER_START, token) - // Submit transfer transaction - val walletTxn = - (anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction) - val transfer = - wallet - .stellar() - .transaction(walletTxn.from!!) - .transferWithdrawalTransaction(walletTxn, asset) - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) - // Wait for the status to change to PENDING_USER_TRANSFER_END - waitForTxnStatus(withdrawTxn.id, COMPLETED, token) - - // Check if the transaction can be listed by stellar transaction id - val fetchTxn = - anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction - val transactionByStellarId = - anchor - .interactive() - .getTransactionBy(token, stellarTransactionId = fetchTxn.stellarTransactionId) - assertEquals(fetchTxn.id, transactionByStellarId.id) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(withdrawTxn.id, 5) - assertEvents(actualEvents, expectedStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(withdrawTxn.id, 5) - assertCallbacks(actualCallbacks, expectedStatuses) - } - - private suspend fun waitForWalletServerCallbacks( - txnId: String, - count: Int - ): List? { - var retries = 5 - var callbacks: List? = null - while (retries > 0) { - callbacks = walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java) - if (callbacks.size == count) { - return callbacks - } - delay(1.seconds) - retries-- - } - return callbacks - } - - private suspend fun waitForBusinessServerEvents( - txnId: String, - count: Int - ): List? { - var retries = 5 - var events: List? = null - while (retries > 0) { - events = anchorReferenceServerClient.getEvents(txnId) - if (events.size == count) { - return events - } - delay(1.seconds) - retries-- - } - return events - } - - private suspend fun waitForTxnStatus( - id: String, - expectedStatus: TransactionStatus, - token: AuthToken - ) { - var status: TransactionStatus? = null - - for (i in 0..maxTries) { - // Get transaction info - val transaction = anchor.interactive().getTransaction(id, token) - - if (status != transaction.status) { - status = transaction.status - - info( - "Transaction(id=${transaction.id}) status changed to $status. Message: ${transaction.message}" - ) - } - - delay(1.seconds) - - if (transaction.status == expectedStatus) { - return - } - } - - fail("Transaction wasn't $expectedStatus in $maxTries tries, last status: $status") - } - - private fun `test created transactions show up in the get history call`( - asset: StellarAssetId, - amount: String - ) = runBlocking { - val newAcc = wallet.stellar().account().createKeyPair() - - val tx = - wallet - .stellar() - .transaction(keypair) - .sponsoring(keypair, newAcc) { - createAccount(newAcc) - addAssetSupport(USDC) - } - .build() - .sign(keypair) - .sign(newAcc) - - wallet.stellar().submitTransaction(tx) - - val token = anchor.auth().authenticate(newAcc) - val deposits = - (0..1).map { - val txnId = makeDeposit(asset, amount, token).id - waitForTxnStatus(txnId, COMPLETED, token) - txnId - } - val history = anchor.interactive().getTransactionsForAsset(asset, token) - - Assertions.assertThat(history).allMatch { deposits.contains(it.id) } - } - - fun testAll() { - info("Running SEP-24 USDC end-to-end tests...") - `test typical deposit end-to-end flow`(USDC, "1.1") - `test typical withdraw end-to-end flow`(USDC, "1.1") - `test created transactions show up in the get history call`(USDC, "1.1") - info("Running SEP-24 XLM end-to-end tests...") - `test typical deposit end-to-end flow`(XLM, "0.00001") - `test typical withdraw end-to-end flow`(XLM, "0.00001") - `test created transactions show up in the get history call`(XLM, "0.00001") - } - - companion object { - private val USDC = - IssuedAssetId("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") - private val expectedStatuses = - listOf( - TRANSACTION_CREATED to SepTransactionStatus.INCOMPLETE, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_USR_TRANSFER_START, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_ANCHOR, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_STELLAR, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.COMPLETED - ) - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyRpcEnd2EndTests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyRpcEnd2EndTests.kt deleted file mode 100644 index 55a4966f52..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep24CustodyRpcEnd2EndTests.kt +++ /dev/null @@ -1,346 +0,0 @@ -package org.stellar.anchor.platform.test - -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.fail -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Assertions.assertNotNull -import org.springframework.web.util.UriComponentsBuilder -import org.stellar.anchor.api.callback.SendEventRequest -import org.stellar.anchor.api.callback.SendEventRequestPayload -import org.stellar.anchor.api.event.AnchorEvent -import org.stellar.anchor.api.event.AnchorEvent.Type.* -import org.stellar.anchor.api.sep.SepTransactionStatus -import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse -import org.stellar.anchor.auth.JwtService -import org.stellar.anchor.auth.Sep24InteractiveUrlJwt -import org.stellar.anchor.platform.CLIENT_WALLET_SECRET -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.util.Log.info -import org.stellar.reference.client.AnchorReferenceServerClient -import org.stellar.reference.wallet.WalletServerClient -import org.stellar.walletsdk.ApplicationConfiguration -import org.stellar.walletsdk.InteractiveFlowResponse -import org.stellar.walletsdk.StellarConfiguration -import org.stellar.walletsdk.Wallet -import org.stellar.walletsdk.anchor.* -import org.stellar.walletsdk.anchor.TransactionStatus.* -import org.stellar.walletsdk.asset.IssuedAssetId -import org.stellar.walletsdk.asset.StellarAssetId -import org.stellar.walletsdk.asset.XLM -import org.stellar.walletsdk.auth.AuthToken -import org.stellar.walletsdk.horizon.SigningKeyPair -import org.stellar.walletsdk.horizon.sign -import org.stellar.walletsdk.horizon.transaction.transferWithdrawalTransaction - -class Sep24CustodyRpcEnd2EndTests(config: TestConfig, val jwt: String) { - private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET - private val keypair = SigningKeyPair.fromSecret(walletSecretKey) - private val wallet = - Wallet( - StellarConfiguration.Testnet, - ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } - ) - private val client = HttpClient { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } - private val anchor = - wallet.anchor(config.env["anchor.domain"]!!) { - install(HttpTimeout) { - requestTimeoutMillis = 300000 - connectTimeoutMillis = 300000 - socketTimeoutMillis = 300000 - } - } - private val maxTries = 90 - private val anchorReferenceServerClient = - AnchorReferenceServerClient(Url(config.env["reference.server.url"]!!)) - private val walletServerClient = WalletServerClient(Url(config.env["wallet.server.url"]!!)) - private val jwtService: JwtService = - JwtService( - config.env["secret.sep10.jwt_secret"]!!, - config.env["secret.sep24.interactive_url.jwt_secret"]!!, - config.env["secret.sep24.more_info_url.jwt_secret"]!!, - config.env["secret.callback_api.auth_secret"]!!, - config.env["secret.platform_api.auth_secret"]!!, - config.env["secret.custody_server.auth_secret"]!! - ) - - private fun `test typical deposit end-to-end flow`(asset: StellarAssetId, amount: String) = - runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val token = anchor.auth().authenticate(keypair) - val response = makeDeposit(asset, amount, token) - - // Assert the interactive URL JWT is valid - val params = UriComponentsBuilder.fromUriString(response.url).build().queryParams - val cipher = params["token"]!![0] - val interactiveJwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) - assertEquals("referenceCustodial", interactiveJwt.claims[JwtService.CLIENT_NAME]) - - // Wait for the status to change to COMPLETED - waitForTxnStatus(response.id, COMPLETED, token) - - // Check if the transaction can be listed by stellar transaction id - val fetchedTxn = anchor.interactive().getTransaction(response.id, token) as DepositTransaction - val transactionByStellarId = - anchor - .interactive() - .getTransactionBy(token, stellarTransactionId = fetchedTxn.stellarTransactionId) - assertEquals(fetchedTxn.id, transactionByStellarId.id) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(response.id, 5) - assertEvents(actualEvents, expectedStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(response.id, 5) - assertCallbacks(actualCallbacks, expectedStatuses) - } - - private suspend fun makeDeposit( - asset: StellarAssetId, - amount: String, - token: AuthToken - ): InteractiveFlowResponse { - // Start interactive deposit - val deposit = anchor.interactive().deposit(asset, token, mapOf("amount" to amount)) - - // Get transaction status and make sure it is INCOMPLETE - val transaction = anchor.interactive().getTransaction(deposit.id, token) - assertEquals(INCOMPLETE, transaction.status) - // Make sure the interactive url is valid. This will also start the reference server's - // withdrawal process. - val resp = client.get(deposit.url) - info("accessing ${deposit.url}...") - assertEquals(200, resp.status.value) - - return deposit - } - - private fun assertEvents( - actualEvents: List?, - expectedStatuses: List> - ) { - assertNotNull(actualEvents) - actualEvents?.let { - assertEquals(expectedStatuses.size, actualEvents.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualEvents[index].let { actualEvent -> - assertNotNull(actualEvent.id) - assertNotNull(actualEvent.timestamp) - assertEquals(expectedStatus.first.type, actualEvent.type) - org.junit.jupiter.api.Assertions.assertTrue( - actualEvent.payload is SendEventRequestPayload - ) - assertEquals(expectedStatus.second, actualEvent.payload.transaction.status) - } - } - } - } - - private fun assertCallbacks( - actualCallbacks: List?, - expectedStatuses: List> - ) { - assertNotNull(actualCallbacks) - actualCallbacks?.let { - assertEquals(expectedStatuses.size, actualCallbacks.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualCallbacks[index].let { actualCallback -> - assertNotNull(actualCallback.transaction.id) - assertEquals(expectedStatus.second.status, actualCallback.transaction.status) - } - } - } - } - - private fun `test typical withdraw end-to-end flow`(asset: StellarAssetId, amount: String) { - `test typical withdraw end-to-end flow`(asset, mapOf("amount" to amount)) - } - - private fun `test typical withdraw end-to-end flow`( - asset: StellarAssetId, - extraFields: Map - ) = runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val token = anchor.auth().authenticate(keypair) - val withdrawTxn = anchor.interactive().withdraw(asset, token, extraFields) - - // Get transaction status and make sure it is INCOMPLETE - val transaction = anchor.interactive().getTransaction(withdrawTxn.id, token) - assertEquals(INCOMPLETE, transaction.status) - // Make sure the interactive url is valid. This will also start the reference server's - // withdrawal process. - val resp = client.get(withdrawTxn.url) - info("accessing ${withdrawTxn.url}...") - assertEquals(200, resp.status.value) - // Wait for the status to change to PENDING_USER_TRANSFER_START - waitForTxnStatus(withdrawTxn.id, PENDING_USER_TRANSFER_START, token) - // Submit transfer transaction - val walletTxn = - (anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction) - val transfer = - wallet - .stellar() - .transaction(walletTxn.from!!) - .transferWithdrawalTransaction(walletTxn, asset) - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) - // Wait for the status to change to PENDING_USER_TRANSFER_END - waitForTxnStatus(withdrawTxn.id, COMPLETED, token) - - // Check if the transaction can be listed by stellar transaction id - val fetchTxn = - anchor.interactive().getTransaction(withdrawTxn.id, token) as WithdrawalTransaction - val transactionByStellarId = - anchor - .interactive() - .getTransactionBy(token, stellarTransactionId = fetchTxn.stellarTransactionId) - assertEquals(fetchTxn.id, transactionByStellarId.id) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(withdrawTxn.id, 4) - assertEvents(actualEvents, expectedStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(withdrawTxn.id, 4) - assertCallbacks(actualCallbacks, expectedStatuses) - } - - private suspend fun waitForWalletServerCallbacks( - txnId: String, - count: Int - ): List? { - var retries = 5 - var callbacks: List? = null - while (retries > 0) { - callbacks = walletServerClient.getCallbacks(txnId, Sep24GetTransactionResponse::class.java) - if (callbacks.size == count) { - return callbacks - } - delay(1.seconds) - retries-- - } - return callbacks - } - - private suspend fun waitForBusinessServerEvents( - txnId: String, - count: Int - ): List? { - var retries = 5 - var events: List? = null - while (retries > 0) { - events = anchorReferenceServerClient.getEvents(txnId) - if (events.size == count) { - return events - } - delay(1.seconds) - retries-- - } - return events - } - - private suspend fun waitForTxnStatus( - id: String, - expectedStatus: TransactionStatus, - token: AuthToken - ) { - var status: TransactionStatus? = null - - for (i in 0..maxTries) { - // Get transaction info - val transaction = anchor.interactive().getTransaction(id, token) - - if (status != transaction.status) { - status = transaction.status - - info( - "Transaction(id=${transaction.id}) status changed to $status. Message: ${transaction.message}" - ) - } - - delay(1.seconds) - - if (transaction.status == expectedStatus) { - return - } - } - - fail("Transaction wasn't $expectedStatus in $maxTries tries, last status: $status") - } - - private fun `test created transactions show up in the get history call`( - asset: StellarAssetId, - amount: String - ) = runBlocking { - val newAcc = wallet.stellar().account().createKeyPair() - - val tx = - wallet - .stellar() - .transaction(keypair) - .sponsoring(keypair, newAcc) { - createAccount(newAcc) - addAssetSupport(USDC) - } - .build() - .sign(keypair) - .sign(newAcc) - - wallet.stellar().submitTransaction(tx) - - val token = anchor.auth().authenticate(newAcc) - val deposits = - (0..1).map { - val txnId = makeDeposit(asset, amount, token).id - waitForTxnStatus(txnId, COMPLETED, token) - txnId - } - val history = anchor.interactive().getHistory(asset, token) - - Assertions.assertThat(history).allMatch { deposits.contains(it.id) } - } - - fun testAll() { - info("Running SEP-24 USDC end-to-end tests...") - `test typical deposit end-to-end flow`(USDC, "1.1") - `test typical withdraw end-to-end flow`(USDC, "1.1") - `test created transactions show up in the get history call`(USDC, "1.1") - info("Running SEP-24 XLM end-to-end tests...") - `test typical deposit end-to-end flow`(XLM, "0.00001") - `test typical withdraw end-to-end flow`(XLM, "0.00001") - `test created transactions show up in the get history call`(XLM, "0.00001") - } - - companion object { - private val USDC = - IssuedAssetId("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") - private val expectedStatuses = - listOf( - TRANSACTION_CREATED to SepTransactionStatus.INCOMPLETE, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_USR_TRANSFER_START, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_ANCHOR, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_STELLAR, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.COMPLETED - ) - } -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31CustodyRpcEnd2EndTests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31CustodyRpcEnd2EndTests.kt deleted file mode 100644 index eafd07db01..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep31CustodyRpcEnd2EndTests.kt +++ /dev/null @@ -1,253 +0,0 @@ -package org.stellar.anchor.platform.test - -import io.ktor.client.plugins.* -import io.ktor.http.* -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.fail -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Assertions.assertNotNull -import org.stellar.anchor.api.callback.SendEventRequest -import org.stellar.anchor.api.callback.SendEventRequestPayload -import org.stellar.anchor.api.event.AnchorEvent -import org.stellar.anchor.api.event.AnchorEvent.Type.* -import org.stellar.anchor.api.sep.SepTransactionStatus -import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest -import org.stellar.anchor.api.sep.sep31.Sep31GetTransactionResponse -import org.stellar.anchor.api.sep.sep31.Sep31PostTransactionRequest -import org.stellar.anchor.apiclient.PlatformApiClient -import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.* -import org.stellar.anchor.util.GsonUtils -import org.stellar.anchor.util.Log.info -import org.stellar.anchor.util.MemoHelper -import org.stellar.anchor.util.Sep1Helper -import org.stellar.reference.client.AnchorReferenceServerClient -import org.stellar.reference.wallet.WalletServerClient -import org.stellar.walletsdk.ApplicationConfiguration -import org.stellar.walletsdk.StellarConfiguration -import org.stellar.walletsdk.Wallet -import org.stellar.walletsdk.anchor.MemoType -import org.stellar.walletsdk.asset.IssuedAssetId -import org.stellar.walletsdk.asset.StellarAssetId -import org.stellar.walletsdk.horizon.SigningKeyPair -import org.stellar.walletsdk.horizon.sign - -class Sep31CustodyRpcEnd2EndTests( - config: TestConfig, - val toml: Sep1Helper.TomlContent, - val jwt: String -) { - private val gson = GsonUtils.getInstance() - private val walletSecretKey = System.getenv("WALLET_SECRET_KEY") ?: CLIENT_WALLET_SECRET - private val keypair = SigningKeyPair.fromSecret(walletSecretKey) - private val wallet = - Wallet( - StellarConfiguration.Testnet, - ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } - ) - private val maxTries = 90 - private val anchorReferenceServerClient = - AnchorReferenceServerClient(Url(config.env["reference.server.url"]!!)) - private val walletServerClient = WalletServerClient(Url(config.env["wallet.server.url"]!!)) - private val sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt) - private val sep31Client = Sep31Client(toml.getString("DIRECT_PAYMENT_SERVER"), jwt) - private val sep38Client = Sep38Client(toml.getString("ANCHOR_QUOTE_SERVER"), jwt) - private val platformApiClient = - PlatformApiClient(AuthHelper.forNone(), config.env["platform.server.url"]!!) - - private fun assertEvents( - actualEvents: List?, - expectedStatuses: List> - ) { - assertNotNull(actualEvents) - actualEvents?.let { - assertEquals(expectedStatuses.size, actualEvents.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualEvents[index].let { actualEvent -> - assertNotNull(actualEvent.id) - assertNotNull(actualEvent.timestamp) - assertEquals(expectedStatus.first.type, actualEvent.type) - Assertions.assertTrue(actualEvent.payload is SendEventRequestPayload) - assertEquals(expectedStatus.second, actualEvent.payload.transaction.status) - } - } - } - } - - private fun assertCallbacks( - actualCallbacks: List?, - expectedStatuses: List> - ) { - assertNotNull(actualCallbacks) - actualCallbacks?.let { - assertEquals(expectedStatuses.size, actualCallbacks.size) - - expectedStatuses.forEachIndexed { index, expectedStatus -> - actualCallbacks[index].let { actualCallback -> - assertNotNull(actualCallback.transaction.id) - assertEquals(expectedStatus.second.status, actualCallback.transaction.status) - } - } - } - } - - private fun `test typical receive end-to-end flow`(asset: StellarAssetId, amount: String) = - runBlocking { - walletServerClient.clearCallbacks() - anchorReferenceServerClient.clearEvents() - - val senderCustomerRequest = - gson.fromJson(testCustomer1Json, Sep12PutCustomerRequest::class.java) - val senderCustomer = sep12Client.putCustomer(senderCustomerRequest) - - // Create receiver customer - val receiverCustomerRequest = - gson.fromJson(testCustomer2Json, Sep12PutCustomerRequest::class.java) - val receiverCustomer = sep12Client.putCustomer(receiverCustomerRequest) - - val quote = sep38Client.postQuote(asset.sep38, amount, FIAT_USD) - - // POST Sep31 transaction - val txnRequest = gson.fromJson(postSep31TxnRequest, Sep31PostTransactionRequest::class.java) - txnRequest.senderId = senderCustomer!!.id - txnRequest.receiverId = receiverCustomer!!.id - txnRequest.quoteId = quote.id - val postTxResponse = sep31Client.postTransaction(txnRequest) - - anchorReferenceServerClient.processSep31Receive(postTxResponse.id) - - // Get transaction status and make sure it is PENDING_SENDER - val transaction = platformApiClient.getTransaction(postTxResponse.id) - assertEquals(SepTransactionStatus.PENDING_SENDER, transaction.status) - - val memoType: MemoType = - when (postTxResponse.stellarMemoType) { - MemoHelper.memoTypeAsString(org.stellar.sdk.xdr.MemoType.MEMO_ID) -> { - MemoType.ID - } - MemoHelper.memoTypeAsString(org.stellar.sdk.xdr.MemoType.MEMO_HASH) -> { - MemoType.HASH - } - else -> { - MemoType.TEXT - } - } - - // Submit transfer transaction - val transfer = - wallet - .stellar() - .transaction(keypair) - .transfer(postTxResponse.stellarAccountId, asset, amount) - .setMemo(Pair(memoType, postTxResponse.stellarMemo)) - .build() - transfer.sign(keypair) - wallet.stellar().submitTransaction(transfer) - - // Wait for the status to change to COMPLETED - waitStatus(postTxResponse.id, SepTransactionStatus.COMPLETED) - - // Check the events sent to the reference server are recorded correctly - val actualEvents = waitForBusinessServerEvents(postTxResponse.id, 3) - assertEvents(actualEvents, expectedStatuses) - - // Check the callbacks sent to the wallet reference server are recorded correctly - val actualCallbacks = waitForWalletServerCallbacks(postTxResponse.id, 3) - assertCallbacks(actualCallbacks, expectedStatuses) - } - - private suspend fun waitForWalletServerCallbacks( - txnId: String, - count: Int - ): List? { - var retries = 5 - var callbacks: List? = null - while (retries > 0) { - callbacks = walletServerClient.getCallbacks(txnId, Sep31GetTransactionResponse::class.java) - if (callbacks.size == count) { - return callbacks - } - delay(1.seconds) - retries-- - } - return callbacks - } - - private suspend fun waitForBusinessServerEvents( - txnId: String, - count: Int - ): List? { - var retries = 5 - var events: List? = null - while (retries > 0) { - events = anchorReferenceServerClient.getEvents(txnId) - if (events.size == count) { - return events - } - delay(1.seconds) - retries-- - } - return events - } - - private suspend fun waitStatus(id: String, expectedStatus: SepTransactionStatus) { - var status: SepTransactionStatus? = null - - for (i in 0..maxTries) { - // Get transaction info - val transaction = platformApiClient.getTransaction(id) - - val current = transaction.status - info("Expected: $expectedStatus. Current: $current") - if (status != transaction.status) { - status = transaction.status - info("Deposit transaction status changed to $status. Message: ${transaction.message}") - } - - delay(1.seconds) - - if (transaction.status == expectedStatus) { - return - } - } - - fail("Transaction wasn't $expectedStatus in $maxTries tries, last status: $status") - } - - fun testAll() { - info("Running SEP-31 USDC end-to-end tests...") - `test typical receive end-to-end flow`(USDC, "5") - } - - companion object { - private val USDC = - IssuedAssetId("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") - private const val FIAT_USD = "iso4217:USD" - private val expectedStatuses = - listOf( - TRANSACTION_CREATED to SepTransactionStatus.PENDING_SENDER, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.PENDING_RECEIVER, - TRANSACTION_STATUS_CHANGED to SepTransactionStatus.COMPLETED - ) - } - - private val postSep31TxnRequest = - """{ - "amount": "5", - "asset_code": "USDC", - "asset_issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - "receiver_id": "MOCK_RECEIVER_ID", - "sender_id": "MOCK_SENDER_ID", - "fields": { - "transaction": { - "receiver_routing_number": "r0123", - "receiver_account_number": "a0456", - "type": "SWIFT" - } - } -}""" -} diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/SepHealthTests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/SepHealthTests.kt deleted file mode 100644 index 76f70e862f..0000000000 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/SepHealthTests.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.stellar.anchor.platform.test - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.stellar.anchor.apiclient.PlatformApiClient -import org.stellar.anchor.auth.AuthHelper -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.util.Sep1Helper.TomlContent - -class SepHealthTests(config: TestConfig, toml: TomlContent, jwt: String) { - private val platformApiClient = - PlatformApiClient(AuthHelper.forNone(), config.env["sep.server.url"]!!) - - private fun testHealth() { - val response = platformApiClient.health(listOf("all")) - assertEquals(5, response.size) - assertEquals(1L, response["number_of_checks"]) - assertNotNull(response["checks"]) - assertNotNull(response["started_at"]) - assertNotNull(response["elapsed_time_ms"]) - assertNotNull(response["number_of_checks"]) - assertNotNull(response["version"]) - } - fun testAll() { - println("Performing Sep Sever Health tests...") - testHealth() - } -} diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/Mutex.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/Mutex.kt new file mode 100644 index 0000000000..845940fd01 --- /dev/null +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/Mutex.kt @@ -0,0 +1,27 @@ +package org.stellar.reference + +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +// This is to make sure transaction submission is mutual exclusive to avoid failures +internal val submissionLock = Mutex() + +suspend fun transactionWithRetry( + maxAttempts: Int = 5, + delay: Int = 5, + transactionLogic: suspend () -> Unit +) = + flow { submissionLock.withLock { transactionLogic() } } + .retryWhen { _, attempt -> + if (attempt < maxAttempts) { + delay((delay + (1..5).random()).seconds) + return@retryWhen true + } else { + return@retryWhen false + } + } + .collect {} diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/ReferenceServer.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/ReferenceServer.kt index 10d134dc0e..9e03e11761 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/ReferenceServer.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/ReferenceServer.kt @@ -1,6 +1,8 @@ package org.stellar.reference import io.ktor.server.netty.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import mu.KotlinLogging import org.stellar.reference.di.ConfigContainer import org.stellar.reference.di.EventConsumerContainer @@ -8,6 +10,8 @@ import org.stellar.reference.di.ReferenceServerContainer import org.stellar.reference.event.EventConsumer val log = KotlinLogging.logger {} +lateinit var eventConsumingExecutor: ExecutorService + lateinit var referenceKotlinServer: NettyApplicationEngine lateinit var eventConsumer: EventConsumer @@ -18,21 +22,23 @@ fun main(args: Array) { fun startServer(envMap: Map?, wait: Boolean) { // read config ConfigContainer.init(envMap) - - Thread { - log.info("Starting event consumer") - eventConsumer = EventConsumerContainer.eventConsumer.start() - } - .start() + eventConsumingExecutor = Executors.newFixedThreadPool(1) + eventConsumingExecutor.submit { + log.info("Starting event consumer") + eventConsumer = EventConsumerContainer.eventConsumer.start() + } // start server log.info { "Starting Kotlin reference server" } - referenceKotlinServer = ReferenceServerContainer.server.start(wait) + referenceKotlinServer = ReferenceServerContainer.startServer(wait) } fun stopServer() { log.info("Stopping Kotlin business reference server...") if (::referenceKotlinServer.isInitialized) (referenceKotlinServer).stop(5000, 30000) if (::eventConsumer.isInitialized) eventConsumer.stop() + eventConsumingExecutor.shutdown() + eventConsumingExecutor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS) + log.info("Kotlin reference server stopped...") } diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/di/ReferenceServerContainer.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/di/ReferenceServerContainer.kt index e1be7323b2..40cd36c897 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/di/ReferenceServerContainer.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/di/ReferenceServerContainer.kt @@ -30,20 +30,27 @@ import org.stellar.reference.sep24.testSep24 const val AUTH_CONFIG_ENDPOINT = "endpoint-auth" object ReferenceServerContainer { - private val config = ConfigContainer.getInstance().config - val server = - embeddedServer(Netty, port = config.appSettings.port) { - install(ContentNegotiation) { json() } - configureAuth() - configureRouting() - install(CORS) { - anyHost() - allowHeader(HttpHeaders.Authorization) - allowHeader(HttpHeaders.ContentType) + lateinit var server: NettyApplicationEngine + + fun startServer(wait: Boolean): NettyApplicationEngine { + server = + embeddedServer(Netty, port = config.appSettings.port) { + install(ContentNegotiation) { json() } + configureAuth() + configureRouting() + install(CORS) { + anyHost() + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.ContentType) + } + install(RequestLoggerPlugin) + install(RequestExceptionHandlerPlugin) } - install(RequestLoggerPlugin) - install(RequestExceptionHandlerPlugin) - } + + return server.start(wait = wait) + } + + private val config = ConfigContainer.getInstance().config private fun Application.configureRouting() = routing { sep24( diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/event/processor/Sep6EventProcessor.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/event/processor/Sep6EventProcessor.kt index 1a3ccf0c80..0696d72bfc 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/event/processor/Sep6EventProcessor.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/event/processor/Sep6EventProcessor.kt @@ -8,12 +8,14 @@ import org.stellar.anchor.api.event.AnchorEvent import org.stellar.anchor.api.platform.* import org.stellar.anchor.api.platform.PatchTransactionsRequest import org.stellar.anchor.api.platform.PlatformTransactionData.Kind +import org.stellar.anchor.api.rpc.method.RpcMethod import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.reference.callbacks.customer.CustomerService import org.stellar.reference.client.PlatformClient import org.stellar.reference.data.* import org.stellar.reference.log import org.stellar.reference.service.SepHelper +import org.stellar.reference.transactionWithRetry import org.stellar.sdk.* class Sep6EventProcessor( @@ -42,7 +44,7 @@ class Sep6EventProcessor( override fun onTransactionCreated(event: AnchorEvent) { when (val kind = event.transaction.kind) { Kind.DEPOSIT, - Kind.WITHDRAWAL -> updateAmounts(event) + Kind.WITHDRAWAL -> requestKyc(event) else -> { log.warn("Received transaction created event with unsupported kind: $kind") } @@ -74,28 +76,37 @@ class Sep6EventProcessor( } runBlocking { val keypair = KeyPair.fromSecretSeed(config.appSettings.secret) - val txnId = - submitStellarTransaction( - keypair.accountId, - transaction.destinationAccount, - Asset.create(transaction.amountExpected.asset.toAssetId()), - transaction.amountExpected.amount + lateinit var stellarTxnId: String + if (config.appSettings.custodyEnabled) { + sepHelper.rpcAction( + RpcMethod.DO_STELLAR_PAYMENT.toString(), + DoStellarPaymentRequest(transactionId = transaction.id) ) - onchainPayments[transaction.id] = txnId - // TODO: manually submit the transaction until custody service is implemented - patchTransaction( - PlatformTransactionData.builder() - .id(transaction.id) - .status(PENDING_STELLAR) - .updatedAt(Instant.now()) - .build() - ) + } else { + transactionWithRetry { + stellarTxnId = + submitStellarTransaction( + keypair.accountId, + transaction.destinationAccount, + Asset.create(transaction.amountExpected.asset.toAssetId()), + transaction.amountExpected.amount + ) + } + onchainPayments[transaction.id] = stellarTxnId + patchTransaction( + PlatformTransactionData.builder() + .id(transaction.id) + .status(PENDING_STELLAR) + .updatedAt(Instant.now()) + .build() + ) + } } } PENDING_USR_TRANSFER_START -> runBlocking { sepHelper.rpcAction( - "notify_offchain_funds_received", + RpcMethod.NOTIFY_OFFCHAIN_FUNDS_RECEIVED.toString(), NotifyOffchainFundsReceivedRequest( transactionId = transaction.id, message = "Funds received from user", @@ -105,7 +116,7 @@ class Sep6EventProcessor( PENDING_STELLAR -> runBlocking { sepHelper.rpcAction( - "notify_onchain_funds_sent", + RpcMethod.NOTIFY_ONCHAIN_FUNDS_SENT.toString(), NotifyOnchainFundsSentRequest( transactionId = transaction.id, message = "Funds sent to user", @@ -136,7 +147,7 @@ class Sep6EventProcessor( val externalTxnId = UUID.randomUUID() offchainPayments[transaction.id] = externalTxnId.toString() sepHelper.rpcAction( - "notify_offchain_funds_pending", + RpcMethod.NOTIFY_OFFCHAIN_FUNDS_PENDING.toString(), NotifyOffchainFundsPendingRequest( transactionId = transaction.id, message = "Funds sent to user", @@ -145,7 +156,7 @@ class Sep6EventProcessor( ) } else { sepHelper.rpcAction( - "notify_offchain_funds_available", + RpcMethod.NOTIFY_OFFCHAIN_FUNDS_AVAILABLE.toString(), NotifyOffchainFundsAvailableRequest( transactionId = transaction.id, message = "Funds available for withdrawal", @@ -158,7 +169,7 @@ class Sep6EventProcessor( PENDING_EXTERNAL -> runBlocking { sepHelper.rpcAction( - "notify_offchain_funds_sent", + RpcMethod.NOTIFY_OFFCHAIN_FUNDS_SENT.toString(), NotifyOffchainFundsSentRequest( transactionId = transaction.id, message = "Funds sent to user", @@ -191,67 +202,65 @@ class Sep6EventProcessor( val customer = transaction.customers.sender when (transaction.kind) { Kind.DEPOSIT -> { - if (verifyKyc(customer.account, customer.memo, Kind.DEPOSIT).isNotEmpty()) { - return - } - runBlocking { - sepHelper.rpcAction( - "request_offchain_funds", - RequestOffchainFundsRequest( - transactionId = transaction.id, - message = "Please deposit the amount to the following bank account", - amountIn = - AmountAssetRequest( - asset = "iso4217:USD", - amount = transaction.amountExpected.amount - ), - amountOut = - AmountAssetRequest( - asset = transaction.amountOut.asset, - amount = transaction.amountOut.amount - ), - amountFee = AmountAssetRequest(asset = "iso4217:USD", amount = "0"), - instructions = - mapOf( - "organization.bank_number" to - InstructionField( - value = "121122676", - description = "US Bank routing number" - ), - "organization.bank_account_number" to - InstructionField( - value = "13719713158835300", - description = "US Bank account number" - ), - ) + if (verifyKyc(customer.account, customer.memo, Kind.DEPOSIT).isEmpty()) { + runBlocking { + sepHelper.rpcAction( + RpcMethod.REQUEST_OFFCHAIN_FUNDS.toString(), + RequestOffchainFundsRequest( + transactionId = transaction.id, + message = "Please deposit the amount to the following bank account", + amountIn = + AmountAssetRequest( + asset = "iso4217:USD", + amount = transaction.amountExpected.amount + ), + amountOut = + AmountAssetRequest( + asset = transaction.amountExpected.asset, + amount = transaction.amountExpected.amount + ), + amountFee = AmountAssetRequest(asset = "iso4217:USD", amount = "0"), + instructions = + mapOf( + "organization.bank_number" to + InstructionField( + value = "121122676", + description = "US Bank routing number" + ), + "organization.bank_account_number" to + InstructionField( + value = "13719713158835300", + description = "US Bank account number" + ), + ) + ) ) - ) + } } } Kind.WITHDRAWAL -> { - if (verifyKyc(customer.account, customer.memo, Kind.WITHDRAWAL).isNotEmpty()) { - return - } - runBlocking { - sepHelper.rpcAction( - "request_onchain_funds", - RequestOnchainFundsRequest( - transactionId = transaction.id, - message = "Please deposit the amount to the following address", - amountIn = - AmountAssetRequest( - asset = transaction.amountExpected.asset, - amount = transaction.amountExpected.amount - ), - amountOut = - AmountAssetRequest( - asset = "iso4217:USD", - amount = transaction.amountExpected.amount - ), - amountFee = - AmountAssetRequest(asset = transaction.amountExpected.asset, amount = "0") + if (verifyKyc(customer.account, customer.memo, Kind.WITHDRAWAL).isEmpty()) { + runBlocking { + sepHelper.rpcAction( + RpcMethod.REQUEST_ONCHAIN_FUNDS.toString(), + RequestOnchainFundsRequest( + transactionId = transaction.id, + message = "Please deposit the amount to the following address", + amountIn = + AmountAssetRequest( + asset = transaction.amountExpected.asset, + amount = transaction.amountExpected.amount + ), + amountOut = + AmountAssetRequest( + asset = "iso4217:USD", + amount = transaction.amountExpected.amount + ), + amountFee = + AmountAssetRequest(asset = transaction.amountExpected.asset, amount = "0") + ) ) - ) + } } } else -> { @@ -266,7 +275,11 @@ class Sep6EventProcessor( private fun verifyKyc(sep10Account: String, sep10AccountMemo: String?, kind: Kind): List { val customer = customerService.getCustomer( - GetCustomerRequest.builder().account(sep10Account).memo(sep10AccountMemo).build() + GetCustomerRequest.builder() + .account(sep10Account) + .memo(sep10AccountMemo) + .memoType(if (sep10AccountMemo != null) "id" else null) + .build() ) val providedFields = customer.providedFields.keys return requiredKyc @@ -274,25 +287,6 @@ class Sep6EventProcessor( .filter { !providedFields.contains(it) } } - private fun updateAmounts(event: AnchorEvent) { - val asset = - when (event.transaction.kind) { - Kind.DEPOSIT -> event.transaction.amountExpected.asset - Kind.WITHDRAWAL -> "iso4217:USD" - else -> throw RuntimeException("Unsupported kind: ${event.transaction.kind}") - } - runBlocking { - sepHelper.rpcAction( - "notify_amounts_updated", - NotifyAmountsUpdatedRequest( - transactionId = event.transaction.id, - amountOut = AmountAssetRequest(asset, amount = event.transaction.amountExpected.amount), - amountFee = AmountAssetRequest(asset, amount = "0") - ) - ) - } - } - private fun requestKyc(event: AnchorEvent) { val kind = event.transaction.kind val customer = event.transaction.customers.sender @@ -300,7 +294,7 @@ class Sep6EventProcessor( runBlocking { if (missingFields.isNotEmpty()) { sepHelper.rpcAction( - "request_customer_info_update", + RpcMethod.REQUEST_CUSTOMER_INFO_UPDATE.toString(), RequestCustomerInfoUpdateHandler( transactionId = event.transaction.id, message = "Please update your info", @@ -331,7 +325,9 @@ class Sep6EventProcessor( val transaction = TransactionBuilder(account, Network.TESTNET) .setBaseFee(100) - .setTimeout(60L) + .addPreconditions( + TransactionPreconditions.builder().timeBounds(TimeBounds.expiresAfter(60)).build() + ) .addOperation(PaymentOperation.Builder(destination, asset, amount).build()) .build() transaction.sign(KeyPair.fromSecretSeed(config.appSettings.secret)) diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/DepositService.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/DepositService.kt index 4681807297..d60d238ee0 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/DepositService.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/DepositService.kt @@ -5,11 +5,13 @@ import java.math.RoundingMode import mu.KotlinLogging import org.stellar.reference.data.* import org.stellar.reference.service.SepHelper +import org.stellar.reference.transactionWithRetry import org.stellar.sdk.responses.operations.PaymentOperationResponse private val log = KotlinLogging.logger {} class DepositService(private val cfg: Config) { + val sep24 = SepHelper(cfg) suspend fun processDeposit( @@ -46,11 +48,13 @@ class DepositService(private val cfg: Config) { // 7. Finalize custody Stellar anchor transaction finalizeCustodyStellarTransaction(transactionId) } else { - // 5. Sign and send transaction - val txHash = sep24.sendStellarTransaction(account, asset, amount, memo, memoType) + transactionWithRetry { + // 5. Sign and send transaction + val txHash = sep24.sendStellarTransaction(account, asset, amount, memo, memoType) - // 6. Finalize Stellar anchor transaction - finalizeStellarTransaction(transactionId, txHash, asset, amount) + // 6. Finalize Stellar anchor transaction + finalizeStellarTransaction(transactionId, txHash, asset, amount) + } } log.info { "Transaction completed: $transactionId" } diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/Sep24TestRoute.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/Sep24TestRoute.kt index cba883f30d..72a7e2cf78 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/Sep24TestRoute.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/sep24/Sep24TestRoute.kt @@ -7,6 +7,7 @@ import io.ktor.server.routing.* import io.ktor.util.logging.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import mu.KotlinLogging import org.stellar.reference.ClientException @@ -66,7 +67,7 @@ fun Route.testSep24( val stellarAsset = asset.replace("stellar:", "") // Run deposit processing asynchronously - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Job()).launch { depositService.processDeposit( transactionId, amountExpected, diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/service/SepHelper.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/service/SepHelper.kt index 86031f80ba..397c5bdaf6 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/service/SepHelper.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/service/SepHelper.kt @@ -10,6 +10,7 @@ import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import java.math.BigDecimal import java.util.* +import java.util.Base64 import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.serialization.json.Json @@ -79,7 +80,7 @@ class SepHelper(private val cfg: Config) { return client.get("$baseUrl/transactions/$transactionId").body() } - internal suspend fun sendStellarTransaction( + internal fun sendStellarTransaction( destinationAddress: String, assetString: String, amount: BigDecimal, @@ -92,7 +93,9 @@ class SepHelper(private val cfg: Config) { val transactionBuilder = TransactionBuilder(myAccount, Network.TESTNET) .setBaseFee(100) - .setTimeout(60) + .addPreconditions( + TransactionPreconditions.builder().timeBounds(TimeBounds.expiresAfter(60)).build() + ) .addOperation( PaymentOperation.Builder(destinationAddress, asset, amount.toPlainString()).build() ) diff --git a/test-lib/build.gradle.kts b/lib-util/build.gradle.kts similarity index 64% rename from test-lib/build.gradle.kts rename to lib-util/build.gradle.kts index b97d9f7634..f2edb3251e 100644 --- a/test-lib/build.gradle.kts +++ b/lib-util/build.gradle.kts @@ -7,7 +7,10 @@ plugins { dependencies { implementation(libs.bundles.junit) + implementation(libs.commons.text) implementation(libs.coroutines.core) - + implementation(libs.google.gson) + implementation(variantOf(libs.java.stellar.sdk) { classifier("uber") }) + implementation(libs.okhttp3) implementation(project(":api-schema")) } diff --git a/core/src/main/java/org/stellar/anchor/util/OkHttpUtil.java b/lib-util/src/main/java/org/stellar/anchor/util/OkHttpUtil.java similarity index 100% rename from core/src/main/java/org/stellar/anchor/util/OkHttpUtil.java rename to lib-util/src/main/java/org/stellar/anchor/util/OkHttpUtil.java diff --git a/core/src/main/java/org/stellar/anchor/util/StringHelper.java b/lib-util/src/main/java/org/stellar/anchor/util/StringHelper.java similarity index 100% rename from core/src/main/java/org/stellar/anchor/util/StringHelper.java rename to lib-util/src/main/java/org/stellar/anchor/util/StringHelper.java diff --git a/test-lib/src/main/kotlin/org/stellar/anchor/LockAndMockExtension.kt b/lib-util/src/main/kotlin/org/stellar/anchor/LockAndMockExtension.kt similarity index 100% rename from test-lib/src/main/kotlin/org/stellar/anchor/LockAndMockExtension.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/LockAndMockExtension.kt diff --git a/test-lib/src/main/kotlin/org/stellar/anchor/TestMutex.kt b/lib-util/src/main/kotlin/org/stellar/anchor/TestMutex.kt similarity index 100% rename from test-lib/src/main/kotlin/org/stellar/anchor/TestMutex.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/TestMutex.kt diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/CustodyApiClient.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/CustodyApiClient.kt similarity index 97% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/CustodyApiClient.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/CustodyApiClient.kt index daa25a86e7..28e4dbefa4 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/CustodyApiClient.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/CustodyApiClient.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import com.google.gson.reflect.TypeToken import org.stellar.anchor.api.custody.CreateCustodyTransactionRequest diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/ReferenceServerClient.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/ReferenceServerClient.kt similarity index 95% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/ReferenceServerClient.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/ReferenceServerClient.kt index 260fd5fcf8..5cbac4e599 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/ReferenceServerClient.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/ReferenceServerClient.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep10Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep10Client.kt similarity index 98% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep10Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep10Client.kt index cf0101b11b..4b500450c9 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep10Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep10Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import java.net.URL import org.stellar.anchor.api.exception.SepNotAuthorizedException diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep12Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep12Client.kt similarity index 93% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep12Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep12Client.kt index 9e684f6729..9d3bfbe616 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep12Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep12Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import com.google.common.reflect.TypeToken import okhttp3.MediaType @@ -7,7 +7,7 @@ import okhttp3.MultipartBody import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import org.springframework.http.HttpStatus +import org.apache.http.HttpStatus.* import org.stellar.anchor.api.exception.SepNotAuthorizedException import org.stellar.anchor.api.sep.sep12.Sep12DeleteCustomerRequest import org.stellar.anchor.api.sep.sep12.Sep12GetCustomerResponse @@ -61,11 +61,11 @@ class Sep12Client(private val endpoint: String, private val jwt: String) : SepCl .put(body) .build() val response = client.newCall(request).execute() - if (response.code == HttpStatus.FORBIDDEN.value()) { + if (response.code == SC_FORBIDDEN) { throw SepNotAuthorizedException("Forbidden") } - assert(response.code == HttpStatus.ACCEPTED.value()) + assert(response.code == SC_ACCEPTED) return gson.fromJson(response.body!!.string(), Sep12PutCustomerResponse::class.java) } @@ -80,7 +80,7 @@ class Sep12Client(private val endpoint: String, private val jwt: String) : SepCl .delete(gson.toJson(deleteCustomerRequest).toRequestBody(TYPE_JSON)) .build() val response = client.newCall(request).execute() - if (response.code == HttpStatus.FORBIDDEN.value()) { + if (response.code == SC_FORBIDDEN) { throw SepNotAuthorizedException("Forbidden") } return response.code diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep24Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep24Client.kt similarity index 97% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep24Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep24Client.kt index bd5a1bbc9b..95b6eb3f04 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep24Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep24Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import org.stellar.anchor.api.sep.sep24.InfoResponse import org.stellar.anchor.api.sep.sep24.InteractiveTransactionResponse diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep31Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep31Client.kt similarity index 97% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep31Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep31Client.kt index a129ae0e85..c8e02cced0 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep31Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep31Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import com.google.gson.reflect.TypeToken import org.stellar.anchor.api.sep.sep31.Sep31GetTransactionResponse diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep38Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep38Client.kt similarity index 98% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep38Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep38Client.kt index 877fb6ff21..093ea04f9a 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep38Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep38Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import java.time.Instant import java.time.format.DateTimeFormatter diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep6Client.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep6Client.kt similarity index 97% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep6Client.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/Sep6Client.kt index 81d9f08141..de357e98c5 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/Sep6Client.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/Sep6Client.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import org.stellar.anchor.api.sep.sep6.GetTransactionResponse import org.stellar.anchor.api.sep.sep6.InfoResponse diff --git a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/SepClient.kt b/lib-util/src/main/kotlin/org/stellar/anchor/client/SepClient.kt similarity index 84% rename from integration-tests/src/main/kotlin/org/stellar/anchor/platform/SepClient.kt rename to lib-util/src/main/kotlin/org/stellar/anchor/client/SepClient.kt index 1f88e148dc..9750175a9f 100644 --- a/integration-tests/src/main/kotlin/org/stellar/anchor/platform/SepClient.kt +++ b/lib-util/src/main/kotlin/org/stellar/anchor/client/SepClient.kt @@ -1,4 +1,4 @@ -package org.stellar.anchor.platform +package org.stellar.anchor.client import com.google.gson.Gson import java.util.concurrent.TimeUnit @@ -6,7 +6,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import org.springframework.http.HttpStatus +import org.apache.http.HttpStatus import org.stellar.anchor.api.exception.SepException import org.stellar.anchor.api.exception.SepNotAuthorizedException import org.stellar.anchor.api.exception.SepNotFoundException @@ -59,14 +59,12 @@ open class SepClient { fun handleResponse(response: Response): String? { val responseBody = response.body?.string() - println("statusCode: " + response.code) - println("responseBody: $responseBody") when (response.code) { - HttpStatus.OK.value(), - HttpStatus.CREATED.value(), - HttpStatus.ACCEPTED.value() -> return responseBody - HttpStatus.FORBIDDEN.value() -> throw SepNotAuthorizedException("Forbidden") - HttpStatus.NOT_FOUND.value() -> { + HttpStatus.SC_OK, + HttpStatus.SC_CREATED, + HttpStatus.SC_ACCEPTED -> return responseBody + HttpStatus.SC_FORBIDDEN -> throw SepNotAuthorizedException("Forbidden") + HttpStatus.SC_NOT_FOUND -> { val sepException = gson.fromJson(responseBody, SepExceptionResponse::class.java) throw SepNotFoundException(sepException.error) } diff --git a/platform/build.gradle.kts b/platform/build.gradle.kts index 284b6f1e9c..ae6076d17d 100644 --- a/platform/build.gradle.kts +++ b/platform/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { // From projects implementation(project(":api-schema")) - implementation(project(":test-lib")) + implementation(project(":lib-util")) implementation(project(":core")) testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/platform/src/main/java/org/stellar/anchor/platform/callback/RestCustomerIntegration.java b/platform/src/main/java/org/stellar/anchor/platform/callback/RestCustomerIntegration.java index dbdd5799ee..e7d1700a68 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/callback/RestCustomerIntegration.java +++ b/platform/src/main/java/org/stellar/anchor/platform/callback/RestCustomerIntegration.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.callback; import static okhttp3.HttpUrl.get; -import static org.stellar.anchor.platform.callback.PlatformIntegrationHelper.*; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -56,11 +55,14 @@ public GetCustomerResponse getCustomer(GetCustomerRequest customerRequest) HttpUrl url = urlBuilder.build(); // Make request - Response response = call(httpClient, getRequestBuilder(authHelper).url(url).get().build()); - String responseContent = getContent(response); + Response response = + PlatformIntegrationHelper.call( + httpClient, + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).get().build()); + String responseContent = PlatformIntegrationHelper.getContent(response); if (response.code() != HttpStatus.OK.value()) { - throw httpError(responseContent, response.code(), gson); + throw PlatformIntegrationHelper.httpError(responseContent, response.code(), gson); } GetCustomerResponse getCustomerResponse; @@ -85,15 +87,16 @@ public PutCustomerResponse putCustomer(PutCustomerRequest putCustomerRequest) RequestBody requestBody = RequestBody.create(gson.toJson(putCustomerRequest), MediaType.get("application/json")); - Request callbackRequest = getRequestBuilder(authHelper).url(url).put(requestBody).build(); + Request callbackRequest = + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).put(requestBody).build(); // Call anchor - Response response = call(httpClient, callbackRequest); - String responseContent = getContent(response); + Response response = PlatformIntegrationHelper.call(httpClient, callbackRequest); + String responseContent = PlatformIntegrationHelper.getContent(response); if (!List.of(HttpStatus.OK.value(), HttpStatus.CREATED.value(), HttpStatus.ACCEPTED.value()) .contains(response.code())) { - throw httpError(responseContent, response.code(), gson); + throw PlatformIntegrationHelper.httpError(responseContent, response.code(), gson); } try { @@ -106,14 +109,15 @@ public PutCustomerResponse putCustomer(PutCustomerRequest putCustomerRequest) @Override public void deleteCustomer(String id) throws AnchorException { HttpUrl url = getCustomerUrlBuilder().addPathSegment(id).build(); - Request callbackRequest = getRequestBuilder(authHelper).url(url).delete().build(); + Request callbackRequest = + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).delete().build(); // Call anchor - Response response = call(httpClient, callbackRequest); - String responseContent = getContent(response); + Response response = PlatformIntegrationHelper.call(httpClient, callbackRequest); + String responseContent = PlatformIntegrationHelper.getContent(response); if (!List.of(HttpStatus.OK.value(), HttpStatus.NO_CONTENT.value()).contains(response.code())) { - throw httpError(responseContent, response.code(), gson); + throw PlatformIntegrationHelper.httpError(responseContent, response.code(), gson); } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/callback/RestFeeIntegration.java b/platform/src/main/java/org/stellar/anchor/platform/callback/RestFeeIntegration.java index 89f325c76d..54715d528e 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/callback/RestFeeIntegration.java +++ b/platform/src/main/java/org/stellar/anchor/platform/callback/RestFeeIntegration.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.callback; import static okhttp3.HttpUrl.get; -import static org.stellar.anchor.platform.callback.PlatformIntegrationHelper.*; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -54,12 +53,13 @@ public GetFeeResponse getFee(GetFeeRequest request) throws AnchorException { }); HttpUrl url = urlBuilder.build(); - Request httpRequest = getRequestBuilder(authHelper).url(url).get().build(); - Response response = call(httpClient, httpRequest); - String responseContent = getContent(response); + Request httpRequest = + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).get().build(); + Response response = PlatformIntegrationHelper.call(httpClient, httpRequest); + String responseContent = PlatformIntegrationHelper.getContent(response); if (response.code() != HttpStatus.OK.value()) { - throw httpError(responseContent, response.code(), gson); + throw PlatformIntegrationHelper.httpError(responseContent, response.code(), gson); } GetFeeResponse feeResponse; diff --git a/platform/src/main/java/org/stellar/anchor/platform/callback/RestRateIntegration.java b/platform/src/main/java/org/stellar/anchor/platform/callback/RestRateIntegration.java index 9b434169b9..cab5ad6758 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/callback/RestRateIntegration.java +++ b/platform/src/main/java/org/stellar/anchor/platform/callback/RestRateIntegration.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.callback; import static okhttp3.HttpUrl.get; -import static org.stellar.anchor.platform.callback.PlatformIntegrationHelper.*; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -56,12 +55,13 @@ public GetRateResponse getRate(GetRateRequest request) throws AnchorException { }); HttpUrl url = urlBuilder.build(); - Request httpRequest = getRequestBuilder(authHelper).url(url).get().build(); - Response response = call(httpClient, httpRequest); - String responseContent = getContent(response); + Request httpRequest = + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).get().build(); + Response response = PlatformIntegrationHelper.call(httpClient, httpRequest); + String responseContent = PlatformIntegrationHelper.getContent(response); if (response.code() != HttpStatus.OK.value()) { - throw httpError(responseContent, response.code(), gson); + throw PlatformIntegrationHelper.httpError(responseContent, response.code(), gson); } GetRateResponse getRateResponse; diff --git a/platform/src/main/java/org/stellar/anchor/platform/callback/RestUniqueAddressIntegration.java b/platform/src/main/java/org/stellar/anchor/platform/callback/RestUniqueAddressIntegration.java index 7b56eeed83..8590d99393 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/callback/RestUniqueAddressIntegration.java +++ b/platform/src/main/java/org/stellar/anchor/platform/callback/RestUniqueAddressIntegration.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.callback; -import static org.stellar.anchor.platform.callback.PlatformIntegrationHelper.*; - import com.google.gson.Gson; import java.net.URI; import java.net.URISyntaxException; @@ -44,12 +42,13 @@ public GetUniqueAddressResponse getUniqueAddress(String transactionId) throws An .addQueryParameter("transaction_id", transactionId) .addPathSegment("unique_address") .build(); - Request request = getRequestBuilder(authHelper).url(url).get().build(); - Response response = call(httpClient, request); - String content = getContent(response); + Request request = + PlatformIntegrationHelper.getRequestBuilder(authHelper).url(url).get().build(); + Response response = PlatformIntegrationHelper.call(httpClient, request); + String content = PlatformIntegrationHelper.getContent(response); if (!List.of(HttpStatus.OK.value(), HttpStatus.NO_CONTENT.value()).contains(response.code())) { - throw httpError(content, response.code(), gson); + throw PlatformIntegrationHelper.httpError(content, response.code(), gson); } return gson.fromJson(content, GetUniqueAddressResponse.class); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/custody/CustodyBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/custody/CustodyBeans.java index 6252684e56..fda533c4bd 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/custody/CustodyBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/custody/CustodyBeans.java @@ -17,10 +17,7 @@ import org.stellar.anchor.platform.config.CustodyApiConfig; import org.stellar.anchor.platform.config.PropertyCustodyConfig; import org.stellar.anchor.platform.config.RpcConfig; -import org.stellar.anchor.platform.custody.CustodyPaymentService; -import org.stellar.anchor.platform.custody.CustodyTransactionService; -import org.stellar.anchor.platform.custody.Sep24CustodyPaymentHandler; -import org.stellar.anchor.platform.custody.Sep31CustodyPaymentHandler; +import org.stellar.anchor.platform.custody.*; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; @Configuration @@ -58,6 +55,16 @@ public FilterRegistrationBean platformToCustodyTokenFilter( return registrationBean; } + @Bean + Sep6CustodyPaymentHandler sep6CustodyPaymentHandler( + JdbcCustodyTransactionRepo custodyTransactionRepo, + PlatformApiClient platformApiClient, + RpcConfig rpcConfig, + MetricsService metricsService) { + return new Sep6CustodyPaymentHandler( + custodyTransactionRepo, platformApiClient, rpcConfig, metricsService); + } + @Bean Sep24CustodyPaymentHandler sep24CustodyPaymentHandler( JdbcCustodyTransactionRepo custodyTransactionRepo, diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/custody/FireblocksBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/custody/FireblocksBeans.java index 02466b33f9..881e3aff9b 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/custody/FireblocksBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/custody/FireblocksBeans.java @@ -11,10 +11,7 @@ import org.stellar.anchor.config.CustodySecretConfig; import org.stellar.anchor.horizon.Horizon; import org.stellar.anchor.platform.config.FireblocksConfig; -import org.stellar.anchor.platform.custody.CustodyPaymentService; -import org.stellar.anchor.platform.custody.CustodyTransactionService; -import org.stellar.anchor.platform.custody.Sep24CustodyPaymentHandler; -import org.stellar.anchor.platform.custody.Sep31CustodyPaymentHandler; +import org.stellar.anchor.platform.custody.*; import org.stellar.anchor.platform.custody.fireblocks.FireblocksApiClient; import org.stellar.anchor.platform.custody.fireblocks.FireblocksEventService; import org.stellar.anchor.platform.custody.fireblocks.FireblocksPaymentService; @@ -51,6 +48,7 @@ FireblocksApiClient fireblocksApiClient( @Bean FireblocksEventService fireblocksEventService( JdbcCustodyTransactionRepo custodyTransactionRepo, + Sep6CustodyPaymentHandler sep6CustodyPaymentHandler, Sep24CustodyPaymentHandler sep24CustodyPaymentHandler, Sep31CustodyPaymentHandler sep31CustodyPaymentHandler, Horizon horizon, @@ -58,6 +56,7 @@ FireblocksEventService fireblocksEventService( throws InvalidConfigException { return new FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/observer/PaymentObserverBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/observer/PaymentObserverBeans.java index c559ab2ac3..79404b1c4a 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/observer/PaymentObserverBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/observer/PaymentObserverBeans.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.component.observer; import java.util.List; -import java.util.stream.Collectors; import lombok.SneakyThrows; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,10 +35,7 @@ public StellarPaymentObserver stellarPaymentObserver( if (assetService == null || assetService.listAllAssets() == null) { throw new ServerErrorException("Asset service cannot be empty."); } - List stellarAssets = - assetService.listAllAssets().stream() - .filter(asset -> asset.getSchema().equals(AssetInfo.Schema.stellar)) - .collect(Collectors.toList()); + List stellarAssets = assetService.listStellarAssets(); if (stellarAssets.size() == 0) { throw new ServerErrorException("Asset service should contain at least one Stellar asset."); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index 9a1ea1ba9d..43f5d0b932 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -73,8 +73,8 @@ Sep12Config sep12Config(CallbackApiConfig callbackApiConfig) { @Bean @ConfigurationProperties(prefix = "sep31") - Sep31Config sep31Config(CustodyConfig custodyConfig) { - return new PropertySep31Config(custodyConfig); + Sep31Config sep31Config(CustodyConfig custodyConfig, AssetService assetService) { + return new PropertySep31Config(custodyConfig, assetService); } @Bean @@ -133,6 +133,7 @@ ClientFinder clientFinder(Sep10Config sep10Config, ClientsConfig clientsConfig) Sep6Service sep6Service( Sep6Config sep6Config, AssetService assetService, + ClientFinder clientFinder, Sep6TransactionStore txnStore, EventService eventService, Sep38QuoteStore sep38QuoteStore) { @@ -143,6 +144,7 @@ Sep6Service sep6Service( sep6Config, assetService, requestValidator, + clientFinder, txnStore, exchangeAmountsCalculator, eventService); @@ -176,22 +178,28 @@ Sep24Service sep24Service( ClientsConfig clientsConfig, AssetService assetService, JwtService jwtService, + ClientFinder clientFinder, Sep24TransactionStore sep24TransactionStore, EventService eventService, InteractiveUrlConstructor interactiveUrlConstructor, MoreInfoUrlConstructor moreInfoUrlConstructor, - CustodyConfig custodyConfig) { + CustodyConfig custodyConfig, + Sep38QuoteStore sep38QuoteStore) { + ExchangeAmountsCalculator exchangeAmountsCalculator = + new ExchangeAmountsCalculator(sep38QuoteStore); return new Sep24Service( appConfig, sep24Config, clientsConfig, assetService, jwtService, + clientFinder, sep24TransactionStore, eventService, interactiveUrlConstructor, moreInfoUrlConstructor, - custodyConfig); + custodyConfig, + exchangeAmountsCalculator); } @Bean diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/share/EventBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/share/EventBeans.java index 26cced500a..4adf60ad03 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/share/EventBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/share/EventBeans.java @@ -5,7 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.stellar.anchor.event.EventService; import org.stellar.anchor.platform.config.PropertyEventConfig; -import org.stellar.anchor.platform.event.*; +import org.stellar.anchor.platform.event.DefaultEventService; @Configuration public class EventBeans { diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java index 16696fb538..1ea7c5b9d3 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.stellar.anchor.api.exception.NotSupportedException; +import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.config.*; import org.stellar.anchor.healthcheck.HealthCheckable; @@ -47,14 +48,15 @@ MoreInfoUrlConstructor moreInfoUrlConstructor( @Bean @ConfigurationProperties(prefix = "sep24") - PropertySep24Config sep24Config(SecretConfig secretConfig, CustodyConfig custodyConfig) { - return new PropertySep24Config(secretConfig, custodyConfig); + PropertySep24Config sep24Config( + SecretConfig secretConfig, CustodyConfig custodyConfig, AssetService assetService) { + return new PropertySep24Config(secretConfig, custodyConfig, assetService); } @Bean @ConfigurationProperties(prefix = "sep6") - PropertySep6Config sep6Config(CustodyConfig custodyConfig) { - return new PropertySep6Config(custodyConfig); + PropertySep6Config sep6Config(CustodyConfig custodyConfig, AssetService assetService) { + return new PropertySep6Config(custodyConfig, assetService); } /********************************** diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertyEventConfig.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertyEventConfig.java index c021a1e32c..28ecd623db 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertyEventConfig.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertyEventConfig.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.config; -import static org.stellar.anchor.config.event.QueueConfig.*; import static org.stellar.anchor.util.StringHelper.isEmpty; import lombok.Data; diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep24Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep24Config.java index f9f7b4dfc6..234ace38cb 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep24Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep24Config.java @@ -15,6 +15,8 @@ import org.jetbrains.annotations.NotNull; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.stellar.anchor.api.sep.AssetInfo; +import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.SecretConfig; import org.stellar.anchor.config.Sep24Config; @@ -38,10 +40,13 @@ public class PropertySep24Config implements Sep24Config, Validator { DepositInfoGeneratorType depositInfoGeneratorType; CustodyConfig custodyConfig; KycFieldsForwarding kycFieldsForwarding; + AssetService assetService; - public PropertySep24Config(SecretConfig secretConfig, CustodyConfig custodyConfig) { + public PropertySep24Config( + SecretConfig secretConfig, CustodyConfig custodyConfig, AssetService assetService) { this.secretConfig = secretConfig; this.custodyConfig = custodyConfig; + this.assetService = assetService; } @Getter @@ -204,5 +209,16 @@ void validateDepositInfoGeneratorType(Errors errors) { "sep24-deposit-info-generator-type", "[custody] deposit info generator type is not supported when custody integration is disabled"); } + + if (SELF == depositInfoGeneratorType) { + for (AssetInfo asset : assetService.listStellarAssets()) { + if (!asset.getCode().equals("native") && isEmpty(asset.getDistributionAccount())) { + errors.rejectValue( + "depositInfoGeneratorType", + "sep24-deposit-info-generator-type", + "[self] deposit info generator type is not supported when distribution account is not set"); + } + } + } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep31Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep31Config.java index be1a17063a..ffc9c1eabc 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep31Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep31Config.java @@ -1,12 +1,16 @@ package org.stellar.anchor.platform.config; import static org.stellar.anchor.config.Sep31Config.DepositInfoGeneratorType.CUSTODY; +import static org.stellar.anchor.config.Sep31Config.DepositInfoGeneratorType.SELF; import static org.stellar.anchor.config.Sep31Config.PaymentType.STRICT_SEND; +import static org.stellar.anchor.util.StringHelper.isEmpty; import lombok.Data; import org.jetbrains.annotations.NotNull; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.stellar.anchor.api.sep.AssetInfo; +import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.Sep31Config; @@ -16,9 +20,11 @@ public class PropertySep31Config implements Sep31Config, Validator { PaymentType paymentType = STRICT_SEND; DepositInfoGeneratorType depositInfoGeneratorType; CustodyConfig custodyConfig; + AssetService assetService; - public PropertySep31Config(CustodyConfig custodyConfig) { + public PropertySep31Config(CustodyConfig custodyConfig, AssetService assetService) { this.custodyConfig = custodyConfig; + this.assetService = assetService; } @Override @@ -48,5 +54,16 @@ void validateDepositInfoGeneratorType(Errors errors) { "sep31-deposit-info-generator-type", "[custody] deposit info generator type is not supported when custody integration is disabled"); } + + if (SELF == depositInfoGeneratorType) { + for (AssetInfo asset : assetService.listStellarAssets()) { + if (!asset.getCode().equals("native") && isEmpty(asset.getDistributionAccount())) { + errors.rejectValue( + "depositInfoGeneratorType", + "sep31-deposit-info-generator-type", + "[self] deposit info generator type is not supported when distribution account is not set"); + } + } + } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java index ea2db86a95..26d6703c28 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java @@ -1,10 +1,14 @@ package org.stellar.anchor.platform.config; import static org.stellar.anchor.config.Sep6Config.DepositInfoGeneratorType.CUSTODY; +import static org.stellar.anchor.config.Sep6Config.DepositInfoGeneratorType.SELF; +import static org.stellar.anchor.util.StringHelper.isEmpty; import lombok.*; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.stellar.anchor.api.sep.AssetInfo; +import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.Sep6Config; @@ -14,9 +18,11 @@ public class PropertySep6Config implements Sep6Config, Validator { Features features; DepositInfoGeneratorType depositInfoGeneratorType; CustodyConfig custodyConfig; + AssetService assetService; - public PropertySep6Config(CustodyConfig custodyConfig) { + public PropertySep6Config(CustodyConfig custodyConfig, AssetService assetService) { this.custodyConfig = custodyConfig; + this.assetService = assetService; } @Override @@ -59,5 +65,16 @@ void validateDepositInfoGeneratorType(Errors errors) { "sep6-deposit-info-generator-type", "[custody] deposit info generator type is not supported when custody integration is disabled"); } + + if (SELF == depositInfoGeneratorType) { + for (AssetInfo asset : assetService.listStellarAssets()) { + if (!asset.getCode().equals("native") && isEmpty(asset.getDistributionAccount())) { + errors.rejectValue( + "depositInfoGeneratorType", + "sep6-deposit-info-generator-type", + "[self] deposit info generator type is not supported when distribution account is not set"); + } + } + } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigManager.java b/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigManager.java index 9f4778b917..507568a2de 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigManager.java +++ b/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigManager.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.configurator; import static org.stellar.anchor.api.platform.HealthCheckStatus.GREEN; -import static org.stellar.anchor.platform.configurator.ConfigHelper.*; import static org.stellar.anchor.platform.configurator.ConfigMap.ConfigSource.FILE; import static org.stellar.anchor.util.Log.*; @@ -21,7 +20,6 @@ import org.stellar.anchor.api.platform.HealthCheckResult; import org.stellar.anchor.api.platform.HealthCheckStatus; import org.stellar.anchor.healthcheck.HealthCheckable; -import org.stellar.anchor.util.Log; public abstract class ConfigManager implements ApplicationContextInitializer, HealthCheckable { @@ -53,7 +51,7 @@ void sanitize(ConfigMap configMap) { .forEach( var -> { if (configMap.get(var) != null) { - Log.warnF( + warnF( "Possible secret leak of config[{}]. Please remove the secret from configuration.", var); configMap.remove(var); @@ -97,7 +95,7 @@ ConfigMap processConfigurations(ConfigurableApplicationContext applicationContex throws IOException, InvalidConfigException { info("reading default configuration values"); // Load default values - ConfigMap latestConfig = loadDefaultConfig(); + ConfigMap latestConfig = ConfigHelper.loadDefaultConfig(); infoF("default configuration version={}", latestConfig.getVersion()); // Check if default config is consistent with the definition @@ -110,12 +108,12 @@ ConfigMap processConfigurations(ConfigurableApplicationContext applicationContex Resource configFileResource = getConfigFileAsResource(applicationContext); if (configFileResource != null) { infoF("reading configuration file from {}", configFileResource.getURL()); - ConfigMap yamlConfig = loadConfig(configFileResource, FILE); + ConfigMap yamlConfig = ConfigHelper.loadConfig(configFileResource, FILE); latestConfig.merge(updateToLatestConfig(latestConfig, yamlConfig)); } // Read and process the environment variable - ConfigMap envConfig = loadConfigFromEnv(latestConfig.getVersion()); + ConfigMap envConfig = ConfigHelper.loadConfigFromEnv(latestConfig.getVersion()); if (envConfig != null) { info("Processing system environment variables"); latestConfig.merge(updateToLatestConfig(latestConfig, envConfig)); diff --git a/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigReader.java b/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigReader.java index e94160ad89..907a04fd70 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigReader.java +++ b/platform/src/main/java/org/stellar/anchor/platform/configurator/ConfigReader.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.configurator; -import static org.stellar.anchor.platform.configurator.ConfigHelper.loadConfig; -import static org.stellar.anchor.platform.configurator.ConfigMap.ConfigSource.VERSION_SCHEMA; import static org.stellar.anchor.util.StringHelper.isEmpty; import java.io.IOException; @@ -11,7 +9,6 @@ import java.util.regex.Pattern; import org.springframework.core.io.ClassPathResource; import org.stellar.anchor.api.exception.InvalidConfigException; -import org.stellar.anchor.platform.configurator.ConfigMap.ConfigEntry; import org.stellar.anchor.util.Log; public class ConfigReader { @@ -21,7 +18,9 @@ public class ConfigReader { public ConfigReader(int version) throws InvalidConfigException { try { configSchema = - loadConfig(new ClassPathResource(getVersionSchemaFile(version)), VERSION_SCHEMA); + ConfigHelper.loadConfig( + new ClassPathResource(getVersionSchemaFile(version)), + ConfigMap.ConfigSource.VERSION_SCHEMA); this.version = version; } catch (IOException e) { throw new InvalidConfigException(String.format("version:%s is not a defined", version)); @@ -82,7 +81,7 @@ public ConfigMap readFrom(ConfigMap configMap) { .names() .forEach( key -> { - ConfigEntry entry = configMap.get(key); + ConfigMap.ConfigEntry entry = configMap.get(key); String value = entry.getValue(); String configLocation = configSchema.getString(key); if (configLocation != null) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/configurator/DataConfigAdapter.java b/platform/src/main/java/org/stellar/anchor/platform/configurator/DataConfigAdapter.java index d94e921171..ee4236bdd5 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/configurator/DataConfigAdapter.java +++ b/platform/src/main/java/org/stellar/anchor/platform/configurator/DataConfigAdapter.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.configurator; -import static org.stellar.anchor.platform.config.PropertySecretConfig.SECRET_DATA_PASSWORD; -import static org.stellar.anchor.platform.config.PropertySecretConfig.SECRET_DATA_USERNAME; import static org.stellar.anchor.util.Log.error; import static org.stellar.anchor.util.StringHelper.isEmpty; @@ -11,6 +9,7 @@ import java.util.List; import java.util.Properties; import org.stellar.anchor.api.exception.InvalidConfigException; +import org.stellar.anchor.platform.config.PropertySecretConfig; import org.stellar.anchor.util.Log; /** @@ -112,8 +111,12 @@ void updateSpringEnv(ConfigMap config) throws InvalidConfigException { set("spring.jpa.generate-ddl", true); set("spring.jpa.hibernate.ddl-auto", "update"); set("spring.datasource.url", constructSQLiteUrl(config)); - set("spring.datasource.username", SecretManager.getInstance().get(SECRET_DATA_USERNAME)); - set("spring.datasource.password", SecretManager.getInstance().get(SECRET_DATA_PASSWORD)); + set( + "spring.datasource.username", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME)); + set( + "spring.datasource.password", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD)); break; case DATABASE_AURORA: set("spring.datasource.driver-class-name", "org.postgresql.Driver"); @@ -123,8 +126,12 @@ void updateSpringEnv(ConfigMap config) throws InvalidConfigException { "spring.datasource.hikari.max-lifetime", 840000); // 14 minutes because IAM tokens are valid for 15 min set("spring.datasource.url", constructPostgressUrl(config)); - set("spring.datasource.username", SecretManager.getInstance().get(SECRET_DATA_USERNAME)); - set("spring.datasource.password", SecretManager.getInstance().get(SECRET_DATA_PASSWORD)); + set( + "spring.datasource.username", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME)); + set( + "spring.datasource.password", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD)); configureFlyway(config); break; case DATABASE_POSTGRES: @@ -132,8 +139,12 @@ void updateSpringEnv(ConfigMap config) throws InvalidConfigException { set("spring.datasource.name", "anchor-platform"); set("spring.jpa.database-platform", "org.hibernate.dialect.PostgreSQL9Dialect"); set("spring.datasource.url", constructPostgressUrl(config)); - set("spring.datasource.username", SecretManager.getInstance().get(SECRET_DATA_USERNAME)); - set("spring.datasource.password", SecretManager.getInstance().get(SECRET_DATA_PASSWORD)); + set( + "spring.datasource.username", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME)); + set( + "spring.datasource.password", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD)); configureFlyway(config); break; default: @@ -148,8 +159,12 @@ private void configureFlyway(ConfigMap config) { if (config.getString("data.flyway_enabled", "").equalsIgnoreCase("true")) { set("spring.flyway.enabled", true); set("spring.flyway.locations", "classpath:/db/migration"); - set("spring.flyway.user", SecretManager.getInstance().get(SECRET_DATA_USERNAME)); - set("spring.flyway.password", SecretManager.getInstance().get(SECRET_DATA_PASSWORD)); + set( + "spring.flyway.user", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME)); + set( + "spring.flyway.password", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD)); set("spring.flyway.url", constructPostgressUrl(config)); boolean baselineOnMigrate = config.getString("data.flyway_baseline_on_migrate").equalsIgnoreCase("true"); @@ -195,8 +210,11 @@ void validateConnection(ConfigMap config) throws InvalidConfigException { String url = constructPostgressUrl(config); try { Properties props = new Properties(); - props.setProperty("user", SecretManager.getInstance().get(SECRET_DATA_USERNAME)); - props.setProperty("password", SecretManager.getInstance().get(SECRET_DATA_PASSWORD)); + props.setProperty( + "user", SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME)); + props.setProperty( + "password", + SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD)); DriverManager.getConnection(url, props); } catch (SQLException e) { error(e.getMessage()); @@ -215,15 +233,17 @@ void validateCredential(ConfigMap config) throws InvalidConfigException { case DATABASE_SQLITE: case DATABASE_AURORA: case DATABASE_POSTGRES: - if (isEmpty(SecretManager.getInstance().get(SECRET_DATA_USERNAME))) { + if (isEmpty(SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_USERNAME))) { String msg = - SECRET_DATA_USERNAME + " is not set. Please provide the datasource username."; + PropertySecretConfig.SECRET_DATA_USERNAME + + " is not set. Please provide the datasource username."; error(msg); throw new InvalidConfigException(msg); } - if (isEmpty(SecretManager.getInstance().get(SECRET_DATA_PASSWORD))) { + if (isEmpty(SecretManager.getInstance().get(PropertySecretConfig.SECRET_DATA_PASSWORD))) { String msg = - SECRET_DATA_PASSWORD + " is not set. Please provide the datasource username."; + PropertySecretConfig.SECRET_DATA_PASSWORD + + " is not set. Please provide the datasource username."; error(msg); throw new InvalidConfigException(msg); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/configurator/SecretManager.java b/platform/src/main/java/org/stellar/anchor/platform/configurator/SecretManager.java index 88919d18dc..441589376e 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/configurator/SecretManager.java +++ b/platform/src/main/java/org/stellar/anchor/platform/configurator/SecretManager.java @@ -1,9 +1,5 @@ package org.stellar.anchor.platform.configurator; -import static org.stellar.anchor.platform.config.PropertyCustodySecretConfig.SECRET_CUSTODY_SERVER_AUTH_SECRET; -import static org.stellar.anchor.platform.config.PropertyCustodySecretConfig.SECRET_FIREBLOCKS_API_KEY; -import static org.stellar.anchor.platform.config.PropertyCustodySecretConfig.SECRET_FIREBLOCKS_SECRET_KEY; -import static org.stellar.anchor.platform.config.PropertySecretConfig.*; import static org.stellar.anchor.util.Log.info; import static org.stellar.anchor.util.StringHelper.isNotEmpty; @@ -14,22 +10,24 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.PropertiesPropertySource; +import org.stellar.anchor.platform.config.PropertyCustodySecretConfig; +import org.stellar.anchor.platform.config.PropertySecretConfig; public class SecretManager implements ApplicationContextInitializer { final List secretVars = Arrays.asList( - SECRET_SEP_10_JWT_SECRET, - SECRET_SEP_10_SIGNING_SEED, - SECRET_SEP_24_INTERACTIVE_URL_JWT_SECRET, - SECRET_SEP_24_MORE_INFO_URL_JWT_SECRET, - SECRET_CALLBACK_API_AUTH_SECRET, - SECRET_PLATFORM_API_AUTH_SECRET, - SECRET_CUSTODY_SERVER_AUTH_SECRET, - SECRET_DATA_USERNAME, - SECRET_DATA_PASSWORD, - SECRET_FIREBLOCKS_SECRET_KEY, - SECRET_FIREBLOCKS_API_KEY); + PropertySecretConfig.SECRET_SEP_10_JWT_SECRET, + PropertySecretConfig.SECRET_SEP_10_SIGNING_SEED, + PropertySecretConfig.SECRET_SEP_24_INTERACTIVE_URL_JWT_SECRET, + PropertySecretConfig.SECRET_SEP_24_MORE_INFO_URL_JWT_SECRET, + PropertySecretConfig.SECRET_CALLBACK_API_AUTH_SECRET, + PropertySecretConfig.SECRET_PLATFORM_API_AUTH_SECRET, + PropertyCustodySecretConfig.SECRET_CUSTODY_SERVER_AUTH_SECRET, + PropertySecretConfig.SECRET_DATA_USERNAME, + PropertySecretConfig.SECRET_DATA_PASSWORD, + PropertyCustodySecretConfig.SECRET_FIREBLOCKS_SECRET_KEY, + PropertyCustodySecretConfig.SECRET_FIREBLOCKS_API_KEY); final Properties props = new Properties(); diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/custody/CustodyControllerConfig.java b/platform/src/main/java/org/stellar/anchor/platform/controller/custody/CustodyControllerConfig.java new file mode 100644 index 0000000000..0cebe616a2 --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/custody/CustodyControllerConfig.java @@ -0,0 +1,14 @@ +package org.stellar.anchor.platform.controller.custody; + +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +public class CustodyControllerConfig { + @InitBinder + void initBinder(final WebDataBinder binder) { + // Maps empty strings to null when a @RequestParam is being bound. + // This is required due to a bug in Spring. + binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/platform/PlatformControllerConfig.java b/platform/src/main/java/org/stellar/anchor/platform/controller/platform/PlatformControllerConfig.java new file mode 100644 index 0000000000..fec29e6e48 --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/platform/PlatformControllerConfig.java @@ -0,0 +1,14 @@ +package org.stellar.anchor.platform.controller.platform; + +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +public class PlatformControllerConfig { + @InitBinder + void initBinder(final WebDataBinder binder) { + // Maps empty strings to null when a @RequestParam is being bound. + // This is required due to a bug in Spring. + binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep12Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep12Controller.java index 71b102290f..bc65b06f53 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep12Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep12Controller.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.controller.sep; -import static org.stellar.anchor.platform.controller.sep.Sep10Helper.getSep10Token; import static org.stellar.anchor.util.Log.*; import com.google.gson.Gson; @@ -49,7 +48,7 @@ public Sep12GetCustomerResponse getCustomer( memo, memoType, lang); - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); Sep12GetCustomerRequest getCustomerRequest = Sep12GetCustomerRequest.builder() .type(type) @@ -73,7 +72,7 @@ public Sep12GetCustomerResponse getCustomer( public Sep12PutCustomerResponse putCustomer( HttpServletRequest request, @RequestBody Sep12PutCustomerRequest putCustomerRequest) { debug("PUT /customer details:", putCustomerRequest); - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); return sep12Service.putCustomer(sep10Jwt, putCustomerRequest); } @@ -93,7 +92,7 @@ public Sep12PutCustomerResponse putCustomerMultipart(HttpServletRequest request) } Sep12PutCustomerRequest putCustomerRequest = gson.fromJson(gson.toJson(requestData), Sep12PutCustomerRequest.class); - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); return sep12Service.putCustomer(sep10Jwt, putCustomerRequest); } @@ -108,7 +107,7 @@ public void deleteCustomer( HttpServletRequest request, @PathVariable String account, @RequestBody(required = false) Sep12DeleteCustomerRequest body) { - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); String memo = body != null ? body.getMemo() : null; String memoType = body != null ? body.getMemoType() : null; debugF( @@ -131,7 +130,7 @@ public void deleteCustomer( @PathVariable String account, @RequestParam(required = false) String memo, @RequestParam(required = false, name = "memo_type") String memoType) { - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); debugF("DELETE /customer requestURI={} account={}", request.getRequestURI(), account); sep12Service.deleteCustomer(sep10Jwt, account, memo, memoType); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep24Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep24Controller.java index d24e427f65..fa16233e89 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep24Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep24Controller.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.controller.sep; -import static org.stellar.anchor.platform.controller.sep.Sep10Helper.getSep10Token; import static org.stellar.anchor.util.Log.*; import java.io.IOException; @@ -49,7 +48,7 @@ public InteractiveTransactionResponse deposit( HttpServletRequest request, @RequestBody HashMap requestData) throws AnchorException, MalformedURLException, URISyntaxException { debug("/deposit", requestData); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); InteractiveTransactionResponse itr = sep24Service.deposit(token, requestData); info("interactive redirection:", itr); return itr; @@ -79,7 +78,7 @@ public InteractiveTransactionResponse withdraw( HttpServletRequest request, @RequestBody HashMap requestData) throws AnchorException, MalformedURLException, URISyntaxException { debug("/withdraw", requestData); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); InteractiveTransactionResponse itr = sep24Service.withdraw(token, requestData); info("interactive redirection:", itr); return itr; @@ -109,7 +108,7 @@ public GetTransactionsResponse getTransactions( HttpServletRequest request, @RequestBody GetTransactionsRequest tr) throws SepException, MalformedURLException, URISyntaxException { debug("/transactions", tr); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); return sep24Service.findTransactions(token, tr); } @@ -148,7 +147,7 @@ public Sep24GetTransactionResponse getTransaction( HttpServletRequest request, @RequestBody(required = false) GetTransactionRequest tr) throws SepException, IOException, URISyntaxException { debug("/transaction", tr); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); return sep24Service.findTransaction(token, tr); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep31Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep31Controller.java index 3d277d5836..f18391849a 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep31Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep31Controller.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.controller.sep; -import static org.stellar.anchor.platform.controller.sep.Sep10Helper.getSep10Token; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.errorEx; @@ -47,7 +46,7 @@ public Sep31InfoResponse getInfo() { public Sep31PostTransactionResponse postTransaction( HttpServletRequest servletRequest, @RequestBody Sep31PostTransactionRequest request) throws AnchorException { - Sep10Jwt sep10Jwt = getSep10Token(servletRequest); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(servletRequest); debugF("POST /transactions request={}", request); return sep31Service.postTransaction(sep10Jwt, request); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java index 3521925ac8..20cd37a039 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.controller.sep; -import static org.stellar.anchor.platform.controller.sep.Sep10Helper.getSep10Token; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.errorEx; @@ -77,7 +76,7 @@ public GetPriceResponse getPrice( gson.fromJson(gson.toJson(params), Sep38GetPriceRequest.class); Sep10Jwt sep10Jwt; try { - sep10Jwt = getSep10Token(request); + sep10Jwt = Sep10Helper.getSep10Token(request); } catch (SepValidationException svex) { sep10Jwt = null; } @@ -93,7 +92,7 @@ public GetPriceResponse getPrice( method = {RequestMethod.POST}) public Sep38QuoteResponse postQuote( HttpServletRequest request, @RequestBody Sep38PostQuoteRequest postQuoteRequest) { - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); debugF("POSTS /quote request={}", postQuoteRequest); return sep38Service.postQuote(sep10Jwt, postQuoteRequest); } @@ -106,7 +105,7 @@ public Sep38QuoteResponse postQuote( method = {RequestMethod.GET}) public Sep38QuoteResponse getQuote( HttpServletRequest request, @PathVariable(name = "quote_id") String quoteId) { - Sep10Jwt sep10Jwt = getSep10Token(request); + Sep10Jwt sep10Jwt = Sep10Helper.getSep10Token(request); debugF("GET /quote id={}", quoteId); return sep38Service.getQuote(sep10Jwt, quoteId); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep6Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep6Controller.java index eec8a3a1a6..8938c9f7e1 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep6Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep6Controller.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.controller.sep; -import static org.stellar.anchor.platform.controller.sep.Sep10Helper.getSep10Token; import static org.stellar.anchor.util.Log.debugF; import javax.servlet.http.HttpServletRequest; @@ -53,7 +52,7 @@ public StartDepositResponse deposit( Boolean claimableBalancesSupported) throws AnchorException { debugF("GET /deposit"); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); StartDepositRequest startDepositRequest = StartDepositRequest.builder() .assetCode(assetCode) @@ -92,7 +91,7 @@ public StartDepositResponse depositExchange( Boolean claimableBalancesSupported) throws AnchorException { debugF("GET /deposit-exchange"); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); StartDepositExchangeRequest startDepositExchangeRequest = StartDepositExchangeRequest.builder() .destinationAsset(destinationAsset) @@ -124,7 +123,7 @@ public StartWithdrawResponse withdraw( @RequestParam(value = "refundMemoType", required = false) String refundMemoType) throws AnchorException { debugF("GET /withdraw"); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); StartWithdrawRequest startWithdrawRequest = StartWithdrawRequest.builder() .assetCode(assetCode) @@ -153,7 +152,7 @@ public StartWithdrawResponse withdraw( @RequestParam(value = "refund_memo_type", required = false) String refundMemoType) throws AnchorException { debugF("GET /withdraw-exchange"); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); StartWithdrawExchangeRequest startWithdrawExchangeRequest = StartWithdrawExchangeRequest.builder() .sourceAsset(sourceAsset) @@ -190,7 +189,7 @@ public GetTransactionsResponse getTransactions( pagingId, noOlderThan, lang); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); GetTransactionsRequest getTransactionsRequest = GetTransactionsRequest.builder() .assetCode(assetCode) @@ -222,7 +221,7 @@ public GetTransactionResponse getTransaction( stellarTransactionId, externalTransactionId, lang); - Sep10Jwt token = getSep10Token(request); + Sep10Jwt token = Sep10Helper.getSep10Token(request); GetTransactionRequest getTransactionRequest = GetTransactionRequest.builder() .id(id) diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/SepControllerConfig.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/SepControllerConfig.java new file mode 100644 index 0000000000..9982fa2f29 --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/SepControllerConfig.java @@ -0,0 +1,16 @@ +package org.stellar.anchor.platform.controller.sep; + +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +@ControllerAdvice +public class SepControllerConfig { + @InitBinder + void initBinder(final WebDataBinder binder) { + // Maps empty strings to null when a @RequestParam is being bound. + // This is required due to a bug in Spring. + binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyEventService.java b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyEventService.java index 261e4fac38..2aa2f2a565 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyEventService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyEventService.java @@ -22,14 +22,17 @@ public abstract class CustodyEventService { private final JdbcCustodyTransactionRepo custodyTransactionRepo; + private final Sep6CustodyPaymentHandler sep6CustodyPaymentHandler; private final Sep24CustodyPaymentHandler sep24CustodyPaymentHandler; private final Sep31CustodyPaymentHandler sep31CustodyPaymentHandler; protected CustodyEventService( JdbcCustodyTransactionRepo custodyTransactionRepo, + Sep6CustodyPaymentHandler sep6CustodyPaymentHandler, Sep24CustodyPaymentHandler sep24CustodyPaymentHandler, Sep31CustodyPaymentHandler sep31CustodyPaymentHandler) { this.custodyTransactionRepo = custodyTransactionRepo; + this.sep6CustodyPaymentHandler = sep6CustodyPaymentHandler; this.sep24CustodyPaymentHandler = sep24CustodyPaymentHandler; this.sep31CustodyPaymentHandler = sep31CustodyPaymentHandler; } @@ -49,6 +52,17 @@ public void handlePayment(CustodyPayment payment) throws AnchorException, IOExce } switch (Sep.from(custodyTransaction.getProtocol())) { + case SEP_6: + switch (Kind.from(custodyTransaction.getKind())) { + case DEPOSIT: + case DEPOSIT_EXCHANGE: + sep6CustodyPaymentHandler.onSent(custodyTransaction, payment); + return; + case WITHDRAWAL: + case WITHDRAWAL_EXCHANGE: + sep6CustodyPaymentHandler.onReceived(custodyTransaction, payment); + return; + } case SEP_24: switch (Kind.from(custodyTransaction.getKind())) { case DEPOSIT: diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyPaymentHandler.java index 8249106bba..fced2887f3 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyPaymentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyPaymentHandler.java @@ -1,8 +1,5 @@ package org.stellar.anchor.platform.custody; -import static org.stellar.anchor.platform.custody.CustodyPayment.CustodyPaymentStatus.SUCCESS; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.COMPLETED; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.FAILED; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.warnF; import static org.stellar.anchor.util.MathHelper.decimal; @@ -12,7 +9,6 @@ import java.math.BigDecimal; import java.util.Set; import org.stellar.anchor.api.exception.AnchorException; -import org.stellar.anchor.platform.custody.CustodyPayment.CustodyPaymentStatus; import org.stellar.anchor.platform.data.CustodyTransactionStatus; import org.stellar.anchor.platform.data.JdbcCustodyTransaction; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; @@ -64,7 +60,7 @@ protected void updateTransaction(JdbcCustodyTransaction txn, CustodyPayment paym } protected void validatePayment(JdbcCustodyTransaction txn, CustodyPayment payment) { - if (SUCCESS != payment.getStatus()) { + if (CustodyPayment.CustodyPaymentStatus.SUCCESS != payment.getStatus()) { return; } @@ -102,12 +98,12 @@ protected void validatePayment(JdbcCustodyTransaction txn, CustodyPayment paymen } protected CustodyTransactionStatus getCustodyTransactionStatus( - CustodyPaymentStatus custodyPaymentStatus) { + CustodyPayment.CustodyPaymentStatus custodyPaymentStatus) { switch (custodyPaymentStatus) { case SUCCESS: - return COMPLETED; + return CustodyTransactionStatus.COMPLETED; case ERROR: - return FAILED; + return CustodyTransactionStatus.FAILED; default: throw new RuntimeException( String.format("Unsupported custody transaction status[%s]", custodyPaymentStatus)); diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyTransactionService.java b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyTransactionService.java index 2f1c8e586c..3806b0f3a2 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyTransactionService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/CustodyTransactionService.java @@ -2,10 +2,6 @@ import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.RECEIVE; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.CREATED; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.SUBMITTED; -import static org.stellar.anchor.platform.data.JdbcCustodyTransaction.PaymentType.PAYMENT; -import static org.stellar.anchor.platform.data.JdbcCustodyTransaction.PaymentType.REFUND; import static org.stellar.anchor.util.Log.debugF; import java.time.Instant; @@ -25,7 +21,6 @@ import org.stellar.anchor.api.exception.custody.CustodyTooManyRequestsException; import org.stellar.anchor.platform.data.CustodyTransactionStatus; import org.stellar.anchor.platform.data.JdbcCustodyTransaction; -import org.stellar.anchor.platform.data.JdbcCustodyTransaction.PaymentType; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; public class CustodyTransactionService { @@ -46,7 +41,8 @@ public CustodyTransactionService( * @param request custody transaction info * @return {@link JdbcCustodyTransaction} object */ - public JdbcCustodyTransaction create(CreateCustodyTransactionRequest request, PaymentType type) + public JdbcCustodyTransaction create( + CreateCustodyTransactionRequest request, JdbcCustodyTransaction.PaymentType type) throws CustodyBadRequestException { return custodyTransactionRepo.save( JdbcCustodyTransaction.builder() @@ -81,7 +77,7 @@ public CreateTransactionPaymentResponse createPayment(String txnId, String reque throws AnchorException { JdbcCustodyTransaction txn = custodyTransactionRepo.findFirstBySepTxIdAndTypeOrderByCreatedAtAsc( - txnId, PAYMENT.getType()); + txnId, JdbcCustodyTransaction.PaymentType.PAYMENT.getType()); if (txn == null) { throw new CustodyNotFoundException(String.format("Transaction (id=%s) is not found", txnId)); } @@ -89,7 +85,7 @@ public CreateTransactionPaymentResponse createPayment(String txnId, String reque CreateTransactionPaymentResponse response; try { response = custodyPaymentService.createTransactionPayment(txn, requestBody); - updateCustodyTransaction(txn, response.getId(), SUBMITTED); + updateCustodyTransaction(txn, response.getId(), CustodyTransactionStatus.SUBMITTED); } catch (FireblocksException e) { updateCustodyTransaction(txn, StringUtils.EMPTY, CustodyTransactionStatus.FAILED); throw (getResponseException(e)); @@ -110,7 +106,7 @@ public CreateTransactionPaymentResponse createRefund( String txnId, CreateTransactionRefundRequest refundRequest) throws AnchorException { JdbcCustodyTransaction txn = custodyTransactionRepo.findFirstBySepTxIdAndTypeOrderByCreatedAtAsc( - txnId, PAYMENT.getType()); + txnId, JdbcCustodyTransaction.PaymentType.PAYMENT.getType()); if (txn == null) { throw new CustodyNotFoundException(String.format("Transaction (id=%s) is not found", txnId)); } @@ -120,7 +116,7 @@ public CreateTransactionPaymentResponse createRefund( CreateTransactionPaymentResponse response; try { response = custodyPaymentService.createTransactionPayment(refundTxn, null); - updateCustodyTransaction(refundTxn, response.getId(), SUBMITTED); + updateCustodyTransaction(refundTxn, response.getId(), CustodyTransactionStatus.SUBMITTED); } catch (FireblocksException e) { custodyTransactionRepo.deleteById(refundTxn.getId()); throw (getResponseException(e)); @@ -144,7 +140,7 @@ private JdbcCustodyTransaction createTransactionRefundRecord( .asset(txn.getAsset()) .kind(txn.getKind()) .build(), - REFUND); + JdbcCustodyTransaction.PaymentType.REFUND); } private void updateCustodyTransaction( @@ -160,12 +156,14 @@ public void updateCustodyTransaction(JdbcCustodyTransaction txn) { } public List getOutboundTransactionsEligibleForReconciliation() { - return custodyTransactionRepo.findAllByStatusAndExternalTxIdNotNull(SUBMITTED.toString()); + return custodyTransactionRepo.findAllByStatusAndExternalTxIdNotNull( + CustodyTransactionStatus.SUBMITTED.toString()); } public List getInboundTransactionsEligibleForReconciliation() { return custodyTransactionRepo.findAllByStatusAndKindIn( - CREATED.toString(), Set.of(RECEIVE.getKind(), WITHDRAWAL.getKind())); + CustodyTransactionStatus.CREATED.toString(), + Set.of(RECEIVE.getKind(), WITHDRAWAL.getKind())); } private AnchorException getResponseException(FireblocksException e) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/Sep24CustodyPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep24CustodyPaymentHandler.java index aef5f0d648..c16b972d64 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/Sep24CustodyPaymentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep24CustodyPaymentHandler.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.custody; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.FAILED; import static org.stellar.anchor.util.Log.infoF; import java.io.IOException; @@ -10,7 +9,6 @@ import org.stellar.anchor.platform.config.RpcConfig; import org.stellar.anchor.platform.data.CustodyTransactionStatus; import org.stellar.anchor.platform.data.JdbcCustodyTransaction; -import org.stellar.anchor.platform.data.JdbcCustodyTransaction.PaymentType; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; import org.stellar.anchor.platform.service.AnchorMetrics; @@ -47,11 +45,11 @@ public void onReceived(JdbcCustodyTransaction txn, CustodyPayment payment) validatePayment(txn, payment); updateTransaction(txn, payment); - if (FAILED == CustodyTransactionStatus.from(txn.getStatus())) { + if (CustodyTransactionStatus.FAILED == CustodyTransactionStatus.from(txn.getStatus())) { platformApiClient.notifyTransactionError( txn.getSepTxId(), rpcConfig.getCustomMessages().getCustodyTransactionFailed()); } else { - switch (PaymentType.from(txn.getType())) { + switch (JdbcCustodyTransaction.PaymentType.from(txn.getType())) { case PAYMENT: platformApiClient.notifyOnchainFundsReceived( txn.getSepTxId(), @@ -91,7 +89,7 @@ public void onSent(JdbcCustodyTransaction txn, CustodyPayment payment) validatePayment(txn, payment); updateTransaction(txn, payment); - if (FAILED == CustodyTransactionStatus.from(txn.getStatus())) { + if (CustodyTransactionStatus.FAILED == CustodyTransactionStatus.from(txn.getStatus())) { platformApiClient.notifyTransactionError( txn.getSepTxId(), rpcConfig.getCustomMessages().getCustodyTransactionFailed()); } else { diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/Sep31CustodyPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep31CustodyPaymentHandler.java index a646a349d6..268814e0e6 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/Sep31CustodyPaymentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep31CustodyPaymentHandler.java @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.custody; -import static org.stellar.anchor.platform.data.CustodyTransactionStatus.FAILED; import static org.stellar.anchor.util.Log.infoF; import static org.stellar.anchor.util.Log.warn; @@ -11,7 +10,6 @@ import org.stellar.anchor.platform.config.RpcConfig; import org.stellar.anchor.platform.data.CustodyTransactionStatus; import org.stellar.anchor.platform.data.JdbcCustodyTransaction; -import org.stellar.anchor.platform.data.JdbcCustodyTransaction.PaymentType; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; import org.stellar.anchor.platform.service.AnchorMetrics; @@ -45,11 +43,11 @@ public void onReceived(JdbcCustodyTransaction txn, CustodyPayment payment) validatePayment(txn, payment); updateTransaction(txn, payment); - if (FAILED == CustodyTransactionStatus.from(txn.getStatus())) { + if (CustodyTransactionStatus.FAILED == CustodyTransactionStatus.from(txn.getStatus())) { platformApiClient.notifyTransactionError( txn.getSepTxId(), rpcConfig.getCustomMessages().getCustodyTransactionFailed()); } else { - switch (PaymentType.from(txn.getType())) { + switch (JdbcCustodyTransaction.PaymentType.from(txn.getType())) { case PAYMENT: platformApiClient.notifyOnchainFundsReceived( txn.getSepTxId(), diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandler.java new file mode 100644 index 0000000000..644c73e9b3 --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandler.java @@ -0,0 +1,97 @@ +package org.stellar.anchor.platform.custody; + +import static org.stellar.anchor.util.Log.infoF; + +import java.io.IOException; +import org.stellar.anchor.api.exception.AnchorException; +import org.stellar.anchor.apiclient.PlatformApiClient; +import org.stellar.anchor.metrics.MetricsService; +import org.stellar.anchor.platform.config.RpcConfig; +import org.stellar.anchor.platform.data.CustodyTransactionStatus; +import org.stellar.anchor.platform.data.JdbcCustodyTransaction; +import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; +import org.stellar.anchor.platform.service.AnchorMetrics; + +/** Custody payment handler for SEP6 transactions */ +public class Sep6CustodyPaymentHandler extends CustodyPaymentHandler { + private final PlatformApiClient platformApiClient; + private final RpcConfig rpcConfig; + private final MetricsService metricsService; + + public Sep6CustodyPaymentHandler( + JdbcCustodyTransactionRepo custodyTransactionRepo, + PlatformApiClient platformApiClient, + RpcConfig rpcConfig, + MetricsService metricsService) { + super(custodyTransactionRepo); + this.platformApiClient = platformApiClient; + this.rpcConfig = rpcConfig; + this.metricsService = metricsService; + } + + @Override + public void onReceived(JdbcCustodyTransaction txn, CustodyPayment payment) + throws AnchorException, IOException { + infoF( + "Incoming inbound payment for SEP-6 transaction. Payment: id[{}], externalTxId[{}], type[{}]", + payment.getId(), + payment.getExternalTxId(), + txn.getType()); + + validatePayment(txn, payment); + updateTransaction(txn, payment); + + if (CustodyTransactionStatus.FAILED == CustodyTransactionStatus.from(txn.getStatus())) { + platformApiClient.notifyTransactionError( + txn.getSepTxId(), rpcConfig.getCustomMessages().getCustodyTransactionFailed()); + } else { + switch (JdbcCustodyTransaction.PaymentType.from(txn.getType())) { + case PAYMENT: + platformApiClient.notifyOnchainFundsReceived( + txn.getSepTxId(), + payment.getTransactionHash(), + payment.getAmount(), + rpcConfig.getCustomMessages().getIncomingPaymentReceived()); + + metricsService + .counter(AnchorMetrics.PAYMENT_RECEIVED, "asset", payment.getAssetName()) + .increment(Double.parseDouble(payment.getAmount())); + case REFUND: + platformApiClient.notifyRefundSent( + txn.getSepTxId(), + payment.getTransactionHash(), + payment.getAmount(), + txn.getAmountFee(), + txn.getAsset()); + break; + } + } + } + + @Override + public void onSent(JdbcCustodyTransaction txn, CustodyPayment payment) + throws AnchorException, IOException { + infoF( + "Incoming outbound payment for SEP-6 transaction. Payment: id[{}], externalTxId[{}], type[{}]", + payment.getId(), + payment.getExternalTxId(), + txn.getType()); + + validatePayment(txn, payment); + updateTransaction(txn, payment); + + if (CustodyTransactionStatus.FAILED == CustodyTransactionStatus.from(txn.getStatus())) { + platformApiClient.notifyTransactionError( + txn.getSepTxId(), rpcConfig.getCustomMessages().getCustodyTransactionFailed()); + } else { + platformApiClient.notifyOnchainFundsSent( + txn.getSepTxId(), + payment.getTransactionHash(), + rpcConfig.getCustomMessages().getOutgoingPaymentSent()); + + metricsService + .counter(AnchorMetrics.PAYMENT_SENT, "asset", payment.getAssetName()) + .increment(Double.parseDouble(payment.getAmount())); + } + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventService.java b/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventService.java index aa4bc31b7e..77cdc53245 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventService.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.custody.fireblocks; -import static org.stellar.anchor.platform.utils.RSAUtil.SHA512_WITH_RSA_ALGORITHM; -import static org.stellar.anchor.platform.utils.RSAUtil.isValidSignature; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.error; import static org.stellar.anchor.util.Log.errorEx; @@ -26,12 +24,9 @@ import org.stellar.anchor.api.exception.SepException; import org.stellar.anchor.horizon.Horizon; import org.stellar.anchor.platform.config.FireblocksConfig; -import org.stellar.anchor.platform.custody.CustodyEventService; -import org.stellar.anchor.platform.custody.CustodyPayment; -import org.stellar.anchor.platform.custody.CustodyPayment.CustodyPaymentStatus; -import org.stellar.anchor.platform.custody.Sep24CustodyPaymentHandler; -import org.stellar.anchor.platform.custody.Sep31CustodyPaymentHandler; +import org.stellar.anchor.platform.custody.*; import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo; +import org.stellar.anchor.platform.utils.RSAUtil; import org.stellar.anchor.util.GsonUtils; import org.stellar.sdk.responses.operations.OperationResponse; import org.stellar.sdk.responses.operations.PathPaymentBaseOperationResponse; @@ -49,12 +44,17 @@ public class FireblocksEventService extends CustodyEventService { public FireblocksEventService( JdbcCustodyTransactionRepo custodyTransactionRepo, + Sep6CustodyPaymentHandler sep6CustodyPaymentHandler, Sep24CustodyPaymentHandler sep24CustodyPaymentHandler, Sep31CustodyPaymentHandler sep31CustodyPaymentHandler, Horizon horizon, FireblocksConfig fireblocksConfig) throws InvalidConfigException { - super(custodyTransactionRepo, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler); + super( + custodyTransactionRepo, + sep6CustodyPaymentHandler, + sep24CustodyPaymentHandler, + sep31CustodyPaymentHandler); this.horizon = horizon; this.publicKey = fireblocksConfig.getFireblocksPublicKey(); } @@ -82,7 +82,8 @@ public void handleEvent(String event, Map headers) throws BadReq debugF("Fireblocks /webhook endpoint called with data '{}'", event); try { - if (isValidSignature(signature, event, publicKey, SHA512_WITH_RSA_ALGORITHM)) { + if (RSAUtil.isValidSignature( + signature, event, publicKey, RSAUtil.SHA512_WITH_RSA_ALGORITHM)) { FireblocksEventObject fireblocksEventObject = GsonUtils.getInstance().fromJson(event, FireblocksEventObject.class); @@ -115,11 +116,13 @@ public void handleEvent(String event, Map headers) throws BadReq public Optional convert(TransactionDetails td) throws IOException { Optional operation = Optional.empty(); - CustodyPaymentStatus status = - td.getStatus().isCompleted() ? CustodyPaymentStatus.SUCCESS : CustodyPaymentStatus.ERROR; + CustodyPayment.CustodyPaymentStatus status = + td.getStatus().isCompleted() + ? CustodyPayment.CustodyPaymentStatus.SUCCESS + : CustodyPayment.CustodyPaymentStatus.ERROR; String message = null; - if (CustodyPaymentStatus.ERROR == status && td.getSubStatus() != null) { + if (CustodyPayment.CustodyPaymentStatus.ERROR == status && td.getSubStatus() != null) { message = td.getSubStatus().name(); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep24Transaction.java b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep24Transaction.java index 3f74f80e70..7d046d4885 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep24Transaction.java +++ b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep24Transaction.java @@ -120,6 +120,10 @@ public void setRefunds(Sep24Refunds refunds) { @Column(name = "client_domain") String clientDomain; + @SerializedName("client_name") + @Column(name = "client_name") + String clientName; + @SerializedName("claimable_balance_supported") @Column(name = "claimable_balance_supported") Boolean claimableBalanceSupported; @@ -135,4 +139,16 @@ public void setRefunds(Sep24Refunds refunds) { @SerializedName("refund_memo_type") @Column(name = "refund_memo_type") String refundMemoType; + + @SerializedName("quote_id") + @Column(name = "quote_id") + String quoteId; + + @SerializedName("source_asset") + @Column(name = "source_asset") + String sourceAsset; + + @SerializedName("destination_asset") + @Column(name = "destination_asset") + String destinationAsset; } diff --git a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep31Transaction.java b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep31Transaction.java index c355359d06..21104bbd9d 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep31Transaction.java +++ b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep31Transaction.java @@ -54,6 +54,10 @@ public String getProtocol() { @Column(name = "client_domain") String clientDomain; + @SerializedName("client_name") + @Column(name = "client_name") + String clientName; + @SerializedName("sender_id") @Column(name = "sender_id") String senderId; diff --git a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6Transaction.java b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6Transaction.java index 6823677ccd..7a4f58178e 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6Transaction.java +++ b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6Transaction.java @@ -87,6 +87,14 @@ public String getProtocol() { @Column(name = "memo_type") String memoType; + @SerializedName("client_domain") + @Column(name = "client_domain") + String clientDomain; + + @SerializedName("client_name") + @Column(name = "client_name") + String clientName; + @SerializedName("quote_id") @Column(name = "quote_id") String quoteId; diff --git a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionRepo.java b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionRepo.java index 288c970d20..fbeaa8e2bc 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionRepo.java +++ b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionRepo.java @@ -24,5 +24,9 @@ JdbcSep6Transaction findOneByWithdrawAnchorAccountAndMemoAndStatus( String withdrawAnchorAccount, String memo, String status); List findBySep10AccountAndRequestAssetCodeOrderByStartedAtDesc( - String stellarAccount, String assetCode); + String sep10Account, String requestAssetCode); + + List + findBySep10AccountAndSep10AccountMemoAndRequestAssetCodeOrderByStartedAtDesc( + String sep10Account, String sep10AccountMemo, String requestAssetCode); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionStore.java b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionStore.java index 666e99c9cc..24abcc86dd 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionStore.java +++ b/platform/src/main/java/org/stellar/anchor/platform/data/JdbcSep6TransactionStore.java @@ -54,10 +54,17 @@ public Sep6Transaction findByExternalTransactionId(String externalTransactionId) public List findTransactions( String accountId, String accountMemo, GetTransactionsRequest request) throws SepValidationException { - if (accountMemo != null) accountId = accountId + ":" + accountMemo; - List txns = - transactionRepo.findBySep10AccountAndRequestAssetCodeOrderByStartedAtDesc( - accountId, request.getAssetCode()); + List txns; + if (accountMemo == null) { + txns = + transactionRepo.findBySep10AccountAndRequestAssetCodeOrderByStartedAtDesc( + accountId, request.getAssetCode()); + } else { + txns = + transactionRepo + .findBySep10AccountAndSep10AccountMemoAndRequestAssetCodeOrderByStartedAtDesc( + accountId, accountMemo, request.getAssetCode()); + } int limit = Integer.MAX_VALUE; if (request.getLimit() != null && request.getLimit() > 0) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java b/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java index 1d06a21c84..1a111acdc3 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java +++ b/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java @@ -18,6 +18,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; import org.stellar.anchor.api.event.AnchorEvent; import org.stellar.anchor.api.exception.AnchorException; import org.stellar.anchor.api.exception.InternalServerErrorException; @@ -39,7 +40,7 @@ public class EventProcessorManager { public static final String CLIENT_STATUS_CALLBACK_EVENT_PROCESSOR_NAME_PREFIX = "client-status-callback-"; - public static final String CALLBACK_API_EVENT_PROCESSOR_NAME = "callback-api-"; + public static final String CALLBACK_API_EVENT_PROCESSOR_NAME = "callback-api"; private final SecretConfig secretConfig; private final EventProcessorConfig eventProcessorConfig; private final CallbackApiConfig callbackApiConfig; @@ -92,8 +93,7 @@ public void start() { // clientsConfig if (eventProcessorConfig.getClientStatusCallback().isEnabled()) { for (PropertyClientsConfig.ClientConfig clientConfig : clientsConfig.getClients()) { - if (clientConfig.getCallbackUrl().isEmpty()) { - + if (StringUtils.isEmpty(clientConfig.getCallbackUrl())) { Log.info(String.format("Client status callback skipped: %s", json(clientConfig))); continue; } diff --git a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/JdbcStellarPaymentStreamerCursorStore.java b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/JdbcStellarPaymentStreamerCursorStore.java index b43b518eaa..e95d3cec46 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/JdbcStellarPaymentStreamerCursorStore.java +++ b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/JdbcStellarPaymentStreamerCursorStore.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.observer.stellar; -import static org.stellar.anchor.platform.data.PaymentStreamerCursor.SINGLETON_ID; - import java.util.Optional; import org.stellar.anchor.platform.data.PaymentStreamerCursor; import org.stellar.anchor.platform.data.PaymentStreamerCursorRepo; @@ -15,12 +13,13 @@ public JdbcStellarPaymentStreamerCursorStore(PaymentStreamerCursorRepo repo) { @Override public void save(String cursor) { - PaymentStreamerCursor paymentStreamerCursor = this.repo.findById(SINGLETON_ID).orElse(null); + PaymentStreamerCursor paymentStreamerCursor = + this.repo.findById(PaymentStreamerCursor.SINGLETON_ID).orElse(null); if (paymentStreamerCursor == null) { paymentStreamerCursor = new PaymentStreamerCursor(); } - paymentStreamerCursor.setId(SINGLETON_ID); + paymentStreamerCursor.setId(PaymentStreamerCursor.SINGLETON_ID); paymentStreamerCursor.setCursor(cursor); this.repo.save(paymentStreamerCursor); @@ -28,7 +27,7 @@ public void save(String cursor) { @Override public String load() { - Optional pageToken = repo.findById(SINGLETON_ID); + Optional pageToken = repo.findById(PaymentStreamerCursor.SINGLETON_ID); return pageToken.map(PaymentStreamerCursor::getCursor).orElse(null); } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarPaymentObserver.java b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarPaymentObserver.java index 6b0d527bc7..8e17e69011 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarPaymentObserver.java +++ b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarPaymentObserver.java @@ -30,7 +30,7 @@ import org.stellar.anchor.api.platform.HealthCheckResult; import org.stellar.anchor.api.platform.HealthCheckStatus; import org.stellar.anchor.healthcheck.HealthCheckable; -import org.stellar.anchor.platform.config.PaymentObserverConfig.StellarPaymentObserverConfig; +import org.stellar.anchor.platform.config.PaymentObserverConfig; import org.stellar.anchor.platform.observer.ObservedPayment; import org.stellar.anchor.platform.observer.PaymentListener; import org.stellar.anchor.platform.utils.DaemonExecutors; @@ -54,7 +54,7 @@ public class StellarPaymentObserver implements HealthCheckable { private static final int MIN_RESULTS = 1; final Server server; - final StellarPaymentObserverConfig config; + final PaymentObserverConfig.StellarPaymentObserverConfig config; final List paymentListeners; final StellarPaymentStreamerCursorStore paymentStreamerCursorStore; final Map, String> mapStreamToAccount = new HashMap<>(); @@ -76,7 +76,7 @@ public class StellarPaymentObserver implements HealthCheckable { public StellarPaymentObserver( String horizonServer, - StellarPaymentObserverConfig config, + PaymentObserverConfig.StellarPaymentObserverConfig config, List paymentListeners, PaymentObservingAccountsManager paymentObservingAccountsManager, StellarPaymentStreamerCursorStore paymentStreamerCursorStore) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java index cfcbabf348..90024e3f08 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java @@ -3,8 +3,6 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.DO_STELLAR_PAYMENT; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_STELLAR; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandler.java index 447ee9b383..14070a7233 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandler.java @@ -2,9 +2,11 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL_EXCHANGE; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_AMOUNTS_UPDATED; import static org.stellar.anchor.api.sep.SepTransactionStatus.*; +import com.google.common.collect.ImmutableSet; import java.util.Set; import org.stellar.anchor.api.exception.BadRequestException; import org.stellar.anchor.api.exception.rpc.InvalidParamsException; @@ -19,6 +21,7 @@ import org.stellar.anchor.event.EventService; import org.stellar.anchor.metrics.MetricsService; import org.stellar.anchor.platform.data.JdbcSep24Transaction; +import org.stellar.anchor.platform.data.JdbcSep6Transaction; import org.stellar.anchor.platform.data.JdbcSepTransaction; import org.stellar.anchor.platform.utils.AssetValidationUtils; import org.stellar.anchor.platform.validator.RequestValidator; @@ -84,7 +87,13 @@ protected SepTransactionStatus getNextStatus( protected Set getSupportedStatuses(JdbcSepTransaction txn) { switch (Sep.from(txn.getProtocol())) { case SEP_6: - return Set.of(INCOMPLETE, PENDING_ANCHOR, PENDING_CUSTOMER_INFO_UPDATE); + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (ImmutableSet.of(WITHDRAWAL, WITHDRAWAL_EXCHANGE).contains(Kind.from(txn6.getKind()))) { + if (areFundsReceived(txn6)) { + return Set.of(PENDING_ANCHOR); + } + } + return emptySet(); case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; if (WITHDRAWAL == Kind.from(txn24.getKind())) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsAvailableHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsAvailableHandler.java index 894e123a6f..208f4b6ff8 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsAvailableHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsAvailableHandler.java @@ -3,8 +3,6 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_OFFCHAIN_FUNDS_AVAILABLE; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_USR_TRANSFER_COMPLETE; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java index 205b1ae27f..e88d6e0ad2 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java @@ -2,8 +2,6 @@ import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_OFFCHAIN_FUNDS_RECEIVED; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_EXTERNAL; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsSentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsSentHandler.java index ff05ab6786..08c7dc0460 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsSentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsSentHandler.java @@ -2,8 +2,6 @@ import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_OFFCHAIN_FUNDS_SENT; import static org.stellar.anchor.api.sep.SepTransactionStatus.COMPLETED; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOnchainFundsSentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOnchainFundsSentHandler.java index 4d3a644de9..994660cd69 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOnchainFundsSentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOnchainFundsSentHandler.java @@ -2,8 +2,6 @@ import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_ONCHAIN_FUNDS_SENT; import static org.stellar.anchor.api.sep.SepTransactionStatus.COMPLETED; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandler.java index fe74cb33d2..fe92bbd7dc 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandler.java @@ -2,20 +2,25 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_REFUND_PENDING; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_EXTERNAL; import static org.stellar.anchor.util.AssetHelper.getAssetCode; -import static org.stellar.anchor.util.MathHelper.decimal; -import static org.stellar.anchor.util.MathHelper.sum; +import static org.stellar.anchor.util.MathHelper.*; +import com.google.common.collect.ImmutableSet; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; import org.stellar.anchor.api.exception.BadRequestException; import org.stellar.anchor.api.exception.rpc.InvalidParamsException; import org.stellar.anchor.api.exception.rpc.InvalidRequestException; +import org.stellar.anchor.api.platform.PlatformTransactionData; import org.stellar.anchor.api.platform.PlatformTransactionData.Kind; import org.stellar.anchor.api.platform.PlatformTransactionData.Sep; import org.stellar.anchor.api.rpc.method.AmountAssetRequest; @@ -23,13 +28,13 @@ import org.stellar.anchor.api.rpc.method.RpcMethod; import org.stellar.anchor.api.sep.AssetInfo; import org.stellar.anchor.api.sep.SepTransactionStatus; +import org.stellar.anchor.api.shared.Amount; +import org.stellar.anchor.api.shared.RefundPayment; +import org.stellar.anchor.api.shared.Refunds; import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.event.EventService; import org.stellar.anchor.metrics.MetricsService; -import org.stellar.anchor.platform.data.JdbcSep24RefundPayment; -import org.stellar.anchor.platform.data.JdbcSep24Refunds; -import org.stellar.anchor.platform.data.JdbcSep24Transaction; -import org.stellar.anchor.platform.data.JdbcSepTransaction; +import org.stellar.anchor.platform.data.*; import org.stellar.anchor.platform.utils.AssetValidationUtils; import org.stellar.anchor.platform.validator.RequestValidator; import org.stellar.anchor.sep24.Sep24RefundPayment; @@ -97,31 +102,70 @@ public RpcMethod getRpcMethod() { @Override protected SepTransactionStatus getNextStatus( - JdbcSepTransaction txn, NotifyRefundPendingRequest request) throws InvalidParamsException { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - AssetInfo assetInfo = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); + JdbcSepTransaction txn, NotifyRefundPendingRequest request) + throws InvalidParamsException, InvalidRequestException { + String amount; + String amountFee; + switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + AssetInfo assetInfo6 = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); - Sep24Refunds sep24Refunds = txn24.getRefunds(); - String amount = request.getRefund().getAmount().getAmount(); - String amountFee = request.getRefund().getAmountFee().getAmount(); + Refunds refunds = txn6.getRefunds(); + amount = request.getRefund().getAmount().getAmount(); + amountFee = request.getRefund().getAmountFee().getAmount(); - BigDecimal totalRefunded; - if (sep24Refunds == null || sep24Refunds.getRefundPayments() == null) { - totalRefunded = sum(assetInfo, amount, amountFee); - } else { - totalRefunded = sum(assetInfo, sep24Refunds.getAmountRefunded(), amount, amountFee); - } + BigDecimal totalRefunded6; + if (refunds == null || refunds.getPayments() == null) { + totalRefunded6 = sum(assetInfo6, amount, amountFee); + } else { + totalRefunded6 = + sum(assetInfo6, refunds.getAmountRefunded().getAmount(), amount, amountFee); + } - BigDecimal amountIn = decimal(txn.getAmountIn(), assetInfo); - if (totalRefunded.compareTo(amountIn) > 0) { - throw new InvalidParamsException("Refund amount exceeds amount_in"); - } + BigDecimal amountIn6 = decimal(txn.getAmountIn(), assetInfo6); + if (totalRefunded6.compareTo(amountIn6) > 0) { + throw new InvalidParamsException("Refund amount exceeds amount_in"); + } + + return PENDING_EXTERNAL; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + AssetInfo assetInfo = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); + + Sep24Refunds sep24Refunds = txn24.getRefunds(); + amount = request.getRefund().getAmount().getAmount(); + amountFee = request.getRefund().getAmountFee().getAmount(); + + BigDecimal totalRefunded; + if (sep24Refunds == null || sep24Refunds.getRefundPayments() == null) { + totalRefunded = sum(assetInfo, amount, amountFee); + } else { + totalRefunded = sum(assetInfo, sep24Refunds.getAmountRefunded(), amount, amountFee); + } + + BigDecimal amountIn = decimal(txn.getAmountIn(), assetInfo); + if (totalRefunded.compareTo(amountIn) > 0) { + throw new InvalidParamsException("Refund amount exceeds amount_in"); + } - return PENDING_EXTERNAL; + return PENDING_EXTERNAL; + } + throw new InvalidRequestException( + String.format( + "RPC method[%s] is not supported for protocol[%s]", getRpcMethod(), txn.getProtocol())); } @Override protected Set getSupportedStatuses(JdbcSepTransaction txn) { + if (SEP_6 == Sep.from(txn.getProtocol())) { + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(Kind.from(txn6.getKind()))) { + return Set.of(PENDING_ANCHOR); + } + return emptySet(); + } + if (SEP_24 == Sep.from(txn.getProtocol())) { JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; if (DEPOSIT == Kind.from(txn24.getKind())) { @@ -134,30 +178,93 @@ protected Set getSupportedStatuses(JdbcSepTransaction txn) @Override protected void updateTransactionWithRpcRequest( JdbcSepTransaction txn, NotifyRefundPendingRequest request) { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - - NotifyRefundPendingRequest.Refund refund = request.getRefund(); - Sep24RefundPayment refundPayment = - JdbcSep24RefundPayment.builder() - .id(refund.getId()) - .amount(refund.getAmount().getAmount()) - .fee(refund.getAmountFee().getAmount()) - .build(); - - Sep24Refunds sep24Refunds = txn24.getRefunds(); - if (sep24Refunds == null) { - sep24Refunds = new JdbcSep24Refunds(); - } + NotifyRefundPendingRequest.Refund requestRefund = request.getRefund(); + AssetInfo assetInfo = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); - if (sep24Refunds.getRefundPayments() == null) { - sep24Refunds.setRefundPayments(List.of()); - } - List refundPayments = sep24Refunds.getRefundPayments(); - refundPayments.add(refundPayment); - sep24Refunds.setRefundPayments(refundPayments); + switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; - AssetInfo assetInfo = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); - sep24Refunds.recalculateAmounts(assetInfo); - txn24.setRefunds(sep24Refunds); + RefundPayment requestPayment = + RefundPayment.builder() + .id(requestRefund.getId()) + .idType(RefundPayment.IdType.EXTERNAL) + .amount( + Amount.builder() + .amount(requestRefund.getAmount().getAmount()) + .asset(requestRefund.getAmount().getAsset()) + .build()) + .fee( + Amount.builder() + .amount(requestRefund.getAmountFee().getAmount()) + .asset(requestRefund.getAmountFee().getAsset()) + .build()) + .build(); + + Refunds refunds = txn6.getRefunds(); + if (refunds == null) { + refunds = new Refunds(); + } + + if (refunds.getPayments() == null) { + refunds.setPayments(new RefundPayment[] {}); + } + List sep6RefundPayments = + new ArrayList<>(Arrays.asList(refunds.getPayments())); + sep6RefundPayments.add(requestPayment); + refunds.setPayments(sep6RefundPayments.toArray(new RefundPayment[0])); + + // Calculate the total fee amount by summing together fees from all refund payments. + refunds.setAmountFee( + new Amount( + formatAmount( + sep6RefundPayments.stream() + .map(RefundPayment::getFee) + .map(amount -> decimal(amount.getAmount(), assetInfo)) + .reduce(BigDecimal.ZERO, BigDecimal::add)), + requestRefund.getAmountFee().getAsset())); + + // Calculate the total refunded amount by summing together amounts from all refund payments. + refunds.setAmountRefunded( + new Amount( + formatAmount( + sum( + assetInfo, + refunds.getAmountFee().getAmount(), + formatAmount( + sep6RefundPayments.stream() + .map(RefundPayment::getAmount) + .map(amount -> decimal(amount.getAmount(), assetInfo)) + .reduce(BigDecimal.ZERO, BigDecimal::add)))), + requestRefund.getAmount().getAsset())); + + txn6.setRefunds(refunds); + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + + Sep24RefundPayment refundPayment = + JdbcSep24RefundPayment.builder() + .id(requestRefund.getId()) + .amount(requestRefund.getAmount().getAmount()) + .fee(requestRefund.getAmountFee().getAmount()) + .build(); + + Sep24Refunds sep24Refunds = txn24.getRefunds(); + if (sep24Refunds == null) { + sep24Refunds = new JdbcSep24Refunds(); + } + + if (sep24Refunds.getRefundPayments() == null) { + sep24Refunds.setRefundPayments(List.of()); + } + List sep24RefundPayments = sep24Refunds.getRefundPayments(); + sep24RefundPayments.add(refundPayment); + sep24Refunds.setRefundPayments(sep24RefundPayments); + + sep24Refunds.recalculateAmounts(assetInfo); + txn24.setRefunds(sep24Refunds); + break; + } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundSentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundSentHandler.java index e2e744ee30..e743a7d220 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundSentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyRefundSentHandler.java @@ -1,20 +1,20 @@ package org.stellar.anchor.platform.rpc; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.RECEIVE; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.*; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_REFUND_SENT; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_EXTERNAL; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_RECEIVER; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_STELLAR; import static org.stellar.anchor.api.sep.SepTransactionStatus.REFUNDED; +import static org.stellar.anchor.api.shared.RefundPayment.IdType.*; import static org.stellar.anchor.util.AssetHelper.getAssetCode; -import static org.stellar.anchor.util.MathHelper.decimal; -import static org.stellar.anchor.util.MathHelper.sum; +import static org.stellar.anchor.util.MathHelper.*; +import static org.stellar.anchor.util.MathHelper.formatAmount; +import com.google.common.collect.ImmutableSet; import java.math.BigDecimal; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import org.stellar.anchor.api.exception.BadRequestException; import org.stellar.anchor.api.exception.rpc.InvalidParamsException; import org.stellar.anchor.api.exception.rpc.InvalidRequestException; @@ -25,16 +25,12 @@ import org.stellar.anchor.api.rpc.method.RpcMethod; import org.stellar.anchor.api.sep.AssetInfo; import org.stellar.anchor.api.sep.SepTransactionStatus; +import org.stellar.anchor.api.shared.Amount; +import org.stellar.anchor.api.shared.Refunds; import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.event.EventService; import org.stellar.anchor.metrics.MetricsService; -import org.stellar.anchor.platform.data.JdbcSep24RefundPayment; -import org.stellar.anchor.platform.data.JdbcSep24Refunds; -import org.stellar.anchor.platform.data.JdbcSep24Transaction; -import org.stellar.anchor.platform.data.JdbcSep31RefundPayment; -import org.stellar.anchor.platform.data.JdbcSep31Refunds; -import org.stellar.anchor.platform.data.JdbcSep31Transaction; -import org.stellar.anchor.platform.data.JdbcSepTransaction; +import org.stellar.anchor.platform.data.*; import org.stellar.anchor.platform.utils.AssetValidationUtils; import org.stellar.anchor.platform.validator.RequestValidator; import org.stellar.anchor.sep24.Sep24RefundPayment; @@ -73,6 +69,7 @@ protected void validate(JdbcSepTransaction txn, NotifyRefundSentRequest request) SepTransactionStatus currentStatus = SepTransactionStatus.from(txn.getStatus()); switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: case SEP_24: if (request.getRefund() == null && PENDING_ANCHOR == currentStatus) { throw new InvalidParamsException("refund is required"); @@ -139,6 +136,55 @@ protected SepTransactionStatus getNextStatus( BigDecimal totalRefunded; switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + Refunds refunds = txn6.getRefunds(); + + if (refunds == null || refunds.getPayments() == null) { + totalRefunded = + sum(assetInfo, refund.getAmount().getAmount(), refund.getAmountFee().getAmount()); + } else { + if (PENDING_ANCHOR == SepTransactionStatus.from(txn.getStatus())) { + totalRefunded = + sum( + assetInfo, + refunds.getAmountRefunded().getAmount(), + refund.getAmount().getAmount(), + refund.getAmountFee().getAmount()); + } else { + if (refund == null) { + totalRefunded = decimal(refunds.getAmountRefunded().getAmount(), assetInfo); + } else { + org.stellar.anchor.api.shared.RefundPayment[] payments = refunds.getPayments(); + + // make sure refund, provided in request, was sent on refund_pending + Arrays.stream(payments) + .map(org.stellar.anchor.api.shared.RefundPayment::getId) + .filter(id -> id.equals(refund.getId())) + .findFirst() + .orElseThrow(() -> new InvalidParamsException("Invalid refund id")); + + totalRefunded = + Arrays.stream(payments) + .map( + payment -> { + if (payment.getId().equals(request.getRefund().getId())) { + return sum( + assetInfo, + refund.getAmount().getAmount(), + refund.getAmountFee().getAmount()); + } else { + return sum( + assetInfo, + payment.getAmount().getAmount(), + payment.getFee().getAmount()); + } + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + } + } + break; case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; Sep24Refunds sep24Refunds = txn24.getRefunds(); @@ -211,6 +257,25 @@ protected Set getSupportedStatuses(JdbcSepTransaction txn) Set supportedStatuses = new HashSet<>(); switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + switch (Kind.from(txn6.getKind())) { + case DEPOSIT: + case DEPOSIT_EXCHANGE: + if (areFundsReceived(txn6)) { + supportedStatuses.add(PENDING_EXTERNAL); + supportedStatuses.add(PENDING_ANCHOR); + } + break; + case WITHDRAWAL: + case WITHDRAWAL_EXCHANGE: + supportedStatuses.add(PENDING_STELLAR); + if (areFundsReceived(txn6)) { + supportedStatuses.add(PENDING_ANCHOR); + } + break; + } + break; case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; switch (Kind.from(txn24.getKind())) { @@ -240,15 +305,81 @@ protected Set getSupportedStatuses(JdbcSepTransaction txn) protected void updateTransactionWithRpcRequest( JdbcSepTransaction txn, NotifyRefundSentRequest request) { AssetInfo assetInfo = assetService.getAsset(getAssetCode(txn.getAmountInAsset())); - NotifyRefundSentRequest.Refund refund = request.getRefund(); - if (refund != null) { + NotifyRefundSentRequest.Refund requestRefund = request.getRefund(); + if (requestRefund != null) { switch (PlatformTransactionData.Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + boolean isDeposit = + ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(Kind.from(txn6.getKind())); + org.stellar.anchor.api.shared.RefundPayment refundPayment = + org.stellar.anchor.api.shared.RefundPayment.builder() + .id(requestRefund.getId()) + .idType(isDeposit ? EXTERNAL : STELLAR) + .amount( + Amount.builder() + .asset(requestRefund.getAmount().getAsset()) + .amount(requestRefund.getAmount().getAmount()) + .build()) + .fee( + Amount.builder() + .asset(requestRefund.getAmountFee().getAsset()) + .amount(requestRefund.getAmountFee().getAmount()) + .build()) + .build(); + + Refunds refunds = txn6.getRefunds(); + if (refunds == null) { + refunds = new Refunds(); + } + + if (refunds.getPayments() == null) { + refunds.setPayments(new org.stellar.anchor.api.shared.RefundPayment[] {refundPayment}); + } else { + List payments = + new ArrayList<>(Arrays.asList(refunds.getPayments())); + payments.removeIf(payment -> payment.getId().equals(request.getRefund().getId())); + payments.add(refundPayment); + refunds.setPayments( + payments.toArray(new org.stellar.anchor.api.shared.RefundPayment[0])); + } + + List refundPayments = + new ArrayList<>(Arrays.asList(refunds.getPayments())); + + // Calculate the total fee amount by summing together fees from all refund payments. + refunds.setAmountFee( + new Amount( + formatAmount( + refundPayments.stream() + .map(org.stellar.anchor.api.shared.RefundPayment::getFee) + .map(amount -> decimal(amount.getAmount(), assetInfo)) + .reduce(BigDecimal.ZERO, BigDecimal::add)), + requestRefund.getAmountFee().getAsset())); + + // Calculate the total refunded amount by summing together amounts from all refund + // payments. + refunds.setAmountRefunded( + new Amount( + formatAmount( + sum( + assetInfo, + refunds.getAmountFee().getAmount(), + formatAmount( + refundPayments.stream() + .map(org.stellar.anchor.api.shared.RefundPayment::getAmount) + .map(amount -> decimal(amount.getAmount(), assetInfo)) + .reduce(BigDecimal.ZERO, BigDecimal::add)))), + requestRefund.getAmount().getAsset())); + + txn6.setRefunds(refunds); + break; case SEP_24: Sep24RefundPayment sep24RefundPayment = JdbcSep24RefundPayment.builder() - .id(refund.getId()) - .amount(refund.getAmount().getAmount()) - .fee(refund.getAmountFee().getAmount()) + .id(requestRefund.getId()) + .amount(requestRefund.getAmount().getAmount()) + .fee(requestRefund.getAmountFee().getAmount()) .build(); JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; @@ -272,9 +403,9 @@ protected void updateTransactionWithRpcRequest( case SEP_31: RefundPayment sep31RefundPayment = JdbcSep31RefundPayment.builder() - .id(refund.getId()) - .amount(refund.getAmount().getAmount()) - .fee(refund.getAmountFee().getAmount()) + .id(requestRefund.getId()) + .amount(requestRefund.getAmount().getAmount()) + .fee(requestRefund.getAmountFee().getAmount()) .build(); JdbcSep31Transaction txn31 = (JdbcSep31Transaction) txn; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java index 836b9aae55..5143c26f44 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java @@ -3,7 +3,6 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_TRUST_SET; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_STELLAR; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestCustomerInfoUpdateHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestCustomerInfoUpdateHandler.java index f97144c938..08fdadb4d1 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestCustomerInfoUpdateHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestCustomerInfoUpdateHandler.java @@ -1,7 +1,6 @@ package org.stellar.anchor.platform.rpc; import static java.util.Collections.emptySet; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_31; import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.REQUEST_CUSTOMER_INFO_UPDATE; import static org.stellar.anchor.api.sep.SepTransactionStatus.*; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOffchainFundsHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOffchainFundsHandler.java index 80abb7c382..00f246117d 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOffchainFundsHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOffchainFundsHandler.java @@ -2,8 +2,6 @@ import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.REQUEST_OFFCHAIN_FUNDS; import static org.stellar.anchor.api.sep.SepTransactionStatus.*; diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestTrustlineHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestTrustlineHandler.java index 64e3cba6e3..9074ee3672 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestTrustlineHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestTrustlineHandler.java @@ -3,7 +3,6 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; import static org.stellar.anchor.api.rpc.method.RpcMethod.REQUEST_TRUST; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_TRUST; diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/AnchorMetrics.java b/platform/src/main/java/org/stellar/anchor/platform/service/AnchorMetrics.java index cdffd70860..2067e88b4f 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/AnchorMetrics.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/AnchorMetrics.java @@ -1,10 +1,12 @@ package org.stellar.anchor.platform.service; public enum AnchorMetrics { - SEP24_TRANSACTION("sep24.transaction"), - SEP31_TRANSACTION("sep31.transaction"), + SEP6_TRANSACTION_OBSERVED("sep6.transaction.observed"), + SEP24_TRANSACTION_OBSERVED("sep24.transaction.observed"), + SEP31_TRANSACTION_OBSERVED("sep31.transaction.observed"), SEP31_TRANSACTION_DB("sep31.transaction.db"), SEP24_TRANSACTION_DB("sep24.transaction.db"), + SEP6_TRANSACTION_DB("sep6.transaction.db"), PAYMENT_RECEIVED("payment.received"), PAYMENT_SENT("payment.sent"), LOGGER("logger"), diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/PaymentOperationToEventListener.java b/platform/src/main/java/org/stellar/anchor/platform/service/PaymentOperationToEventListener.java index 2606c2a5cf..49d9913861 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/PaymentOperationToEventListener.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/PaymentOperationToEventListener.java @@ -95,18 +95,18 @@ public void onReceived(ObservedPayment payment) throws IOException { } // Find a transaction matching the memo, assumes transactions are unique to account+memo - JdbcSep24Transaction sep24Txn; + JdbcSep24Transaction sep24Txn = null; try { sep24Txn = sep24TransactionStore.findOneByToAccountAndMemoAndStatus( payment.getTo(), memo, SepTransactionStatus.PENDING_USR_TRANSFER_START.toString()); } catch (Exception ex) { errorEx(ex); - return; } if (sep24Txn != null) { try { handleSep24Transaction(payment, sep24Txn); + return; } catch (AnchorException aex) { warnF("Error handling the SEP24 transaction id={}.", sep24Txn.getId()); errorEx(aex); @@ -114,18 +114,18 @@ public void onReceived(ObservedPayment payment) throws IOException { } // Find a transaction matching the memo, assumes transactions are unique to account+memo - JdbcSep6Transaction sep6Txn; + JdbcSep6Transaction sep6Txn = null; try { sep6Txn = sep6TransactionStore.findOneByWithdrawAnchorAccountAndMemoAndStatus( payment.getTo(), memo, SepTransactionStatus.PENDING_USR_TRANSFER_START.toString()); } catch (Exception ex) { errorEx(ex); - return; } if (sep6Txn != null) { try { handleSep6Transaction(payment, sep6Txn); + return; } catch (AnchorException aex) { warnF("Error handling the SEP6 transaction id={}.", sep6Txn.getId()); errorEx(aex); @@ -171,7 +171,7 @@ void handleSep31Transaction(ObservedPayment payment, JdbcSep31Transaction txn) // Update metrics Metrics.counter( - AnchorMetrics.SEP31_TRANSACTION.toString(), + AnchorMetrics.SEP31_TRANSACTION_OBSERVED.toString(), "status", SepTransactionStatus.PENDING_RECEIVER.toString()) .increment(); @@ -219,9 +219,8 @@ void handleSep24Transaction(ObservedPayment payment, JdbcSep24Transaction txn) rpcConfig.getCustomMessages().getIncomingPaymentReceived()); } - // Update metrics Metrics.counter( - AnchorMetrics.SEP24_TRANSACTION.toString(), + AnchorMetrics.SEP24_TRANSACTION_OBSERVED.toString(), "status", SepTransactionStatus.PENDING_ANCHOR.toString()) .increment(); @@ -258,5 +257,13 @@ void handleSep6Transaction(ObservedPayment payment, JdbcSep6Transaction txn) payment.getTransactionHash(), payment.getAmount(), rpcConfig.getCustomMessages().getIncomingPaymentReceived()); + + Metrics.counter( + AnchorMetrics.SEP6_TRANSACTION_OBSERVED.toString(), + "status", + SepTransactionStatus.PENDING_ANCHOR.toString()) + .increment(); + Metrics.counter(AnchorMetrics.PAYMENT_RECEIVED.toString(), "asset", payment.getAssetName()) + .increment(Double.parseDouble(payment.getAmount())); } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/RpcService.java b/platform/src/main/java/org/stellar/anchor/platform/service/RpcService.java index c18fc67603..0ee0f8c2cd 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/RpcService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/RpcService.java @@ -3,10 +3,6 @@ import static java.util.function.Function.identity; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; -import static org.stellar.anchor.platform.utils.RpcUtil.getRpcBatchLimitErrorResponse; -import static org.stellar.anchor.platform.utils.RpcUtil.getRpcErrorResponse; -import static org.stellar.anchor.platform.utils.RpcUtil.getRpcSuccessResponse; -import static org.stellar.anchor.platform.utils.RpcUtil.validateRpcRequest; import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.Log.errorEx; @@ -22,6 +18,7 @@ import org.stellar.anchor.api.rpc.method.RpcMethod; import org.stellar.anchor.platform.config.RpcConfig; import org.stellar.anchor.platform.rpc.RpcMethodHandler; +import org.stellar.anchor.platform.utils.RpcUtil; public class RpcService { @@ -36,7 +33,7 @@ public RpcService(List> rpcMethodHandlers, RpcConfig rpcConf public List handle(List rpcRequests) { if (rpcRequests.size() > rpcConfig.getBatchSizeLimit()) { - return List.of(getRpcBatchLimitErrorResponse(rpcConfig.getBatchSizeLimit())); + return List.of(RpcUtil.getRpcBatchLimitErrorResponse(rpcConfig.getBatchSizeLimit())); } return rpcRequests.stream() @@ -44,24 +41,24 @@ public List handle(List rpcRequests) { rc -> { final Object rpcId = rc.getId(); try { - validateRpcRequest(rc); - return getRpcSuccessResponse(rpcId, processRpcCall(rc)); + RpcUtil.validateRpcRequest(rc); + return RpcUtil.getRpcSuccessResponse(rpcId, processRpcCall(rc)); } catch (RpcException ex) { errorEx( String.format( "An RPC error occurred while processing an RPC request with method[%s] and id[%s]", rc.getMethod(), rpcId), ex); - return getRpcErrorResponse(rc, ex); + return RpcUtil.getRpcErrorResponse(rc, ex); } catch (BadRequestException ex) { - return getRpcErrorResponse(rc, ex); + return RpcUtil.getRpcErrorResponse(rc, ex); } catch (Exception ex) { errorEx( String.format( "An internal error occurred while processing an RPC request with method[%s] and id[%s]", rc.getMethod(), rpcId), ex); - return getRpcErrorResponse(rc, new InternalErrorException(ex.getMessage())); + return RpcUtil.getRpcErrorResponse(rc, new InternalErrorException(ex.getMessage())); } }) .collect(toList()); diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java index e90f1f615e..d803dc362a 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.service; -import static org.stellar.anchor.platform.config.PropertySep24Config.MoreInfoUrlConfig; - import java.net.URI; import java.time.Instant; import java.util.HashMap; @@ -11,17 +9,20 @@ import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.auth.Sep24MoreInfoUrlJwt; import org.stellar.anchor.platform.config.PropertyClientsConfig; +import org.stellar.anchor.platform.config.PropertySep24Config; import org.stellar.anchor.sep24.MoreInfoUrlConstructor; import org.stellar.anchor.sep24.Sep24Transaction; import org.stellar.anchor.util.ConfigHelper; public class SimpleMoreInfoUrlConstructor extends MoreInfoUrlConstructor { private final PropertyClientsConfig clientsConfig; - private final MoreInfoUrlConfig config; + private final PropertySep24Config.MoreInfoUrlConfig config; private final JwtService jwtService; public SimpleMoreInfoUrlConstructor( - PropertyClientsConfig clientsConfig, MoreInfoUrlConfig config, JwtService jwtService) { + PropertyClientsConfig clientsConfig, + PropertySep24Config.MoreInfoUrlConfig config, + JwtService jwtService) { this.clientsConfig = clientsConfig; this.config = config; this.jwtService = jwtService; diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java b/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java index c8fafecd94..66a84ca743 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java @@ -7,7 +7,6 @@ import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_CUSTOMER_INFO_UPDATE; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_USR_TRANSFER_START; import static org.stellar.anchor.event.EventService.EventQueue.TRANSACTION; -import static org.stellar.anchor.platform.utils.PlatformTransactionHelper.toGetTransactionResponse; import static org.stellar.anchor.sep31.Sep31Helper.allAmountAvailable; import static org.stellar.anchor.util.BeanHelper.updateField; import static org.stellar.anchor.util.MathHelper.decimal; @@ -30,7 +29,6 @@ import org.stellar.anchor.api.exception.BadRequestException; import org.stellar.anchor.api.exception.InternalServerErrorException; import org.stellar.anchor.api.exception.NotFoundException; -import org.stellar.anchor.api.platform.*; import org.stellar.anchor.api.platform.GetTransactionResponse; import org.stellar.anchor.api.platform.GetTransactionsResponse; import org.stellar.anchor.api.platform.PatchTransactionRequest; @@ -52,6 +50,7 @@ import org.stellar.anchor.platform.data.JdbcSep31Transaction; import org.stellar.anchor.platform.data.JdbcSep6Transaction; import org.stellar.anchor.platform.data.JdbcSepTransaction; +import org.stellar.anchor.platform.utils.PlatformTransactionHelper; import org.stellar.anchor.sep24.Sep24DepositInfoGenerator; import org.stellar.anchor.sep24.Sep24Refunds; import org.stellar.anchor.sep24.Sep24TransactionStore; @@ -167,7 +166,7 @@ public GetTransactionResponse findTransaction(String txnId) throws AnchorExcepti findUnknownTransactionCounter.increment(); } - return toGetTransactionResponse(txn, assetService); + return PlatformTransactionHelper.toGetTransactionResponse(txn, assetService); } else { throw new NotFoundException(String.format("transaction (id=%s) is not found", txnId)); } @@ -196,7 +195,10 @@ public GetTransactionsResponse findTransactions(TransactionsSeps sep, Transactio return new GetTransactionsResponse( txn.stream() - .map(t -> toGetTransactionResponse((JdbcSepTransaction) t, assetService)) + .map( + t -> + PlatformTransactionHelper.toGetTransactionResponse( + (JdbcSepTransaction) t, assetService)) .collect(Collectors.toList())); } @@ -335,7 +337,7 @@ private GetTransactionResponse patchTransaction(PatchTransactionRequest patch) break; } - return toGetTransactionResponse(txn, assetService); + return PlatformTransactionHelper.toGetTransactionResponse(txn, assetService); } void updateSepTransaction(PlatformTransactionData patch, JdbcSepTransaction txn) diff --git a/platform/src/main/resources/config/anchor-config-default-values.yaml b/platform/src/main/resources/config/anchor-config-default-values.yaml index e54b31a581..f66be56c1a 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -307,6 +307,7 @@ sep6: ## deposit address, memo and memo type. ## @supported_values: ## self: the memo and memo type are generated in the local code, and the distribution account is used for the deposit address. + ## When using 'self', make sure all the stellar assets has distribution account configured. ## custody: the memo and memo type are generated through Custody API, for example Fireblocks, as well as the deposit address. ## none: deposit address, memo and memo type should be provided by the business in PATCH/RPC request. # diff --git a/platform/src/main/resources/db/migration/V11__sep24_field_updates.sql b/platform/src/main/resources/db/migration/V11__sep24_field_updates.sql new file mode 100644 index 0000000000..ea5faf4342 --- /dev/null +++ b/platform/src/main/resources/db/migration/V11__sep24_field_updates.sql @@ -0,0 +1,5 @@ +ALTER TABLE sep24_transaction ADD quote_id VARCHAR(255); + +ALTER TABLE sep24_transaction ADD source_asset VARCHAR(255); + +ALTER TABLE sep24_transaction ADD destination_asset VARCHAR(255); \ No newline at end of file diff --git a/platform/src/main/resources/db/migration/V12__client_domain_name.sql b/platform/src/main/resources/db/migration/V12__client_domain_name.sql new file mode 100644 index 0000000000..6ef076bd97 --- /dev/null +++ b/platform/src/main/resources/db/migration/V12__client_domain_name.sql @@ -0,0 +1,5 @@ +ALTER TABLE sep6_transaction ADD client_domain VARCHAR(255); +ALTER TABLE sep6_transaction ADD client_name VARCHAR(255); + +ALTER TABLE sep24_transaction ADD client_name VARCHAR(255); +ALTER TABLE sep31_transaction ADD client_name VARCHAR(255); diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt index 2270c4bc6c..2ae9470a88 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt @@ -72,7 +72,10 @@ class LogAppenderTest { // Verify that the append method was called, which ensures that the event was captured verify { appender.append(any()) } - assertEquals(LogAppenderTest::class.qualifiedName, capturedLogEvent.captured.loggerName) + assertEquals( + org.stellar.anchor.platform.LogAppenderTest::class.qualifiedName, + capturedLogEvent.captured.loggerName + ) assertEquals(wantLevelName, capturedLogEvent.captured.level.toString()) assertEquals(wantMessage, capturedLogEvent.captured.message.toString()) capturedLogEvent.clear() diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/PaymentObservingAccountsBeansTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/PaymentObservingAccountsBeansTest.kt index 0119f04cde..9ef1abb2ac 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/PaymentObservingAccountsBeansTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/PaymentObservingAccountsBeansTest.kt @@ -63,6 +63,7 @@ class PaymentObservingAccountsBeansTest { // assetService.listAllAssets() doesn't contain stellar assets val mockStellarLessAssetService = mockk() every { mockStellarLessAssetService.listAllAssets() } returns listOf() + every { mockStellarLessAssetService.listStellarAssets() } returns listOf() ex = assertThrows { paymentObserverBeans.stellarPaymentObserver( mockStellarLessAssetService, diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/CustodyConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/CustodyConfigTest.kt index 37e6fe8c2d..a55f1874a3 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/CustodyConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/CustodyConfigTest.kt @@ -2,9 +2,9 @@ package org.stellar.anchor.platform.config import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource -import org.junit.jupiter.params.provider.NullSource import org.junit.jupiter.params.provider.ValueSource import org.springframework.validation.BindException import org.springframework.validation.Errors @@ -35,10 +35,8 @@ class PropertyCustodyConfigTest { assertFalse(errors.hasErrors()) } - @ParameterizedTest - @NullSource - @ValueSource(strings = [""]) - fun `test empty type`(type: String?) { + @Test + fun `test empty type`() { config.type = null config.validate(config, errors) assertErrorCode(errors, "custody-type-empty") diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/EventConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/EventConfigTest.kt index fb7b9c788b..54e6ecd086 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/EventConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/EventConfigTest.kt @@ -11,6 +11,7 @@ import org.springframework.validation.BindException import org.springframework.validation.Errors import org.springframework.validation.ValidationUtils import org.stellar.anchor.config.event.QueueConfig.QueueType.* +import org.stellar.anchor.platform.config.* class EventConfigTest { lateinit var config: PropertyEventConfig diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep1ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep1ConfigTest.kt index 18e49177c3..8ac4d07924 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep1ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep1ConfigTest.kt @@ -3,7 +3,6 @@ package org.stellar.anchor.platform.config import java.net.URL import java.nio.file.Paths import kotlin.test.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.NullSource diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep24ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep24ConfigTest.kt index 9a9e1dc52d..0c3747216a 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep24ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep24ConfigTest.kt @@ -11,6 +11,8 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.springframework.validation.BindException import org.springframework.validation.Errors +import org.stellar.anchor.asset.AssetService +import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.config.CustodyConfig import org.stellar.anchor.config.SecretConfig import org.stellar.anchor.config.Sep24Config.DepositInfoGeneratorType @@ -23,16 +25,18 @@ class Sep24ConfigTest { lateinit var errors: Errors lateinit var secretConfig: SecretConfig lateinit var custodyConfig: CustodyConfig + lateinit var assetService: AssetService @BeforeEach fun setUp() { secretConfig = mockk() custodyConfig = mockk() + assetService = DefaultAssetService.fromJsonResource("test_assets.json") every { secretConfig.sep24MoreInfoUrlJwtSecret } returns "more_info url jwt secret" every { secretConfig.sep24InteractiveUrlJwtSecret } returns "interactive url jwt secret" every { custodyConfig.isCustodyIntegrationEnabled } returns false - config = PropertySep24Config(secretConfig, custodyConfig) + config = PropertySep24Config(secretConfig, custodyConfig, assetService) config.enabled = true errors = BindException(config, "config") config.interactiveUrl = InteractiveUrlConfig("https://www.stellar.org", 600, listOf("")) @@ -155,4 +159,19 @@ class Sep24ConfigTest { config.validate(config, errors) assertFalse(errors.hasErrors()) } + + @Test + fun `test validation rejecting self deposit generator if distribution_account missing in asset`() { + assetService = + DefaultAssetService.fromJsonResource("test_assets_missing_distribution_account.json") + config = + PropertySep24Config(secretConfig, custodyConfig, assetService).apply { + enabled = true + interactiveUrl = InteractiveUrlConfig("https://www.stellar.org", 600, listOf("")) + moreInfoUrl = MoreInfoUrlConfig("https://www.stellar.org", 600, listOf("")) + depositInfoGeneratorType = DepositInfoGeneratorType.SELF + } + config.validate(config, errors) + assertEquals("sep24-deposit-info-generator-type", errors.allErrors[0].code) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep31ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep31ConfigTest.kt index bbe046e9cf..61d1b3297b 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep31ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep31ConfigTest.kt @@ -8,6 +8,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.validation.BindException import org.springframework.validation.Errors +import org.stellar.anchor.asset.AssetService +import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.config.CustodyConfig import org.stellar.anchor.config.Sep31Config.DepositInfoGeneratorType @@ -15,20 +17,22 @@ class Sep31ConfigTest { lateinit var config: PropertySep31Config lateinit var errors: Errors lateinit var custodyConfig: CustodyConfig + lateinit var assetService: AssetService @BeforeEach fun setUp() { custodyConfig = mockk() + assetService = DefaultAssetService.fromJsonResource("test_assets.json") every { custodyConfig.isCustodyIntegrationEnabled } returns false - config = PropertySep31Config(custodyConfig) + config = PropertySep31Config(custodyConfig, assetService) config.enabled = true errors = BindException(config, "config") config.depositInfoGeneratorType = DepositInfoGeneratorType.SELF } @Test - fun `test valid sep24 configuration`() { + fun `test valid sep31 configuration`() { config.validate(config, errors) assertFalse(errors.hasErrors()) } @@ -41,7 +45,7 @@ class Sep31ConfigTest { } @Test - fun `test valid sep24 configuration with custody integration`() { + fun `test valid sep31 configuration with custody integration`() { every { custodyConfig.isCustodyIntegrationEnabled } returns true config.depositInfoGeneratorType = DepositInfoGeneratorType.CUSTODY config.validate(config, errors) @@ -54,4 +58,17 @@ class Sep31ConfigTest { config.validate(config, errors) assertEquals("sep31-deposit-info-generator-type", errors.allErrors[0].code) } + + @Test + fun `test validation rejecting self deposit generator if distribution_account missing in asset`() { + assetService = + DefaultAssetService.fromJsonResource("test_assets_missing_distribution_account.json") + config = + PropertySep31Config(custodyConfig, assetService).apply { + enabled = true + depositInfoGeneratorType = DepositInfoGeneratorType.SELF + } + config.validate(config, errors) + assertEquals("sep31-deposit-info-generator-type", errors.allErrors[0].code) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt index cddcab78eb..f807a06d6b 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt @@ -10,20 +10,24 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.springframework.validation.BindException import org.springframework.validation.Errors +import org.stellar.anchor.asset.AssetService +import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.config.CustodyConfig import org.stellar.anchor.config.Sep6Config class Sep6ConfigTest { @MockK(relaxed = true) lateinit var custodyConfig: CustodyConfig + @MockK(relaxed = true) lateinit var assetService: AssetService lateinit var config: PropertySep6Config lateinit var errors: Errors @BeforeEach fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) + assetService = DefaultAssetService.fromJsonResource("test_assets.json") every { custodyConfig.isCustodyIntegrationEnabled } returns true config = - PropertySep6Config(custodyConfig).apply { + PropertySep6Config(custodyConfig, assetService).apply { enabled = true features = Sep6Config.Features(false, false) depositInfoGeneratorType = Sep6Config.DepositInfoGeneratorType.CUSTODY @@ -88,4 +92,20 @@ class Sep6ConfigTest { config.validate(config, errors) Assertions.assertEquals("sep6-deposit-info-generator-type", errors.allErrors[0].code) } + + @Test + fun `test validation rejecting self deposit generator if distribution_account missing in asset`() { + assetService = + DefaultAssetService.fromJsonResource("test_assets_missing_distribution_account.json") + config = + PropertySep6Config(custodyConfig, assetService).apply { + enabled = true + features = Sep6Config.Features(false, false) + depositInfoGeneratorType = Sep6Config.DepositInfoGeneratorType.SELF + } + every { custodyConfig.isCustodyIntegrationEnabled } returns false + + config.validate(config, errors) + Assertions.assertEquals("sep6-deposit-info-generator-type", errors.allErrors[0].code) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigHelperTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigHelperTest.kt index 9c8e994689..b679b65f78 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigHelperTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigHelperTest.kt @@ -11,17 +11,16 @@ class ConfigHelperTest { @CsvSource( value = [ - "clients[0]_name,clients,[0],name", - "clients[1]_name,clients,[1],name", - "clients[2]_name,clients,[2],name", - "clients[0]_type,clients,[0],type", - "clients[0]_callback_url,clients,[0],callback_url" + "clients[0]_name,clients,name", + "clients[1]_name,clients,name", + "clients[2]_name,clients,name", + "clients[0]_type,clients,type", + "clients[0]_callback_url,clients,callback_url" ] ) fun `test the list name extraction if the name is a list`( name: String, listName: String, - index: String, elementName: String ) { val result = ConfigHelper.extractListNameIfAny(name) diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigMapTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigMapTest.kt index 4d5505cc3d..c971fa2fda 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigMapTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/configurator/ConfigMapTest.kt @@ -1,7 +1,5 @@ package org.stellar.anchor.platform.configurator -import javax.xml.bind.annotation.XmlType.DEFAULT -import kotlin.test.assertEquals import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyEventServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyEventServiceTest.kt index 6cf57335fc..9ff0a9e28f 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyEventServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyEventServiceTest.kt @@ -6,6 +6,8 @@ import io.mockk.impl.annotations.MockK import io.mockk.verify import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.stellar.anchor.platform.data.JdbcCustodyTransaction import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo @@ -14,11 +16,13 @@ class CustodyEventServiceTest { // test implementation class CustodyEventServiceTestImpl( custodyTransactionRepo: JdbcCustodyTransactionRepo, + sep6CustodyPaymentHandler: Sep6CustodyPaymentHandler, sep24CustodyPaymentHandler: Sep24CustodyPaymentHandler, sep31CustodyPaymentHandler: Sep31CustodyPaymentHandler ) : CustodyEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler ) { @@ -28,6 +32,7 @@ class CustodyEventServiceTest { } @MockK(relaxed = true) private lateinit var custodyTransactionRepo: JdbcCustodyTransactionRepo + @MockK(relaxed = true) private lateinit var sep6CustodyPaymentHandler: Sep6CustodyPaymentHandler @MockK(relaxed = true) private lateinit var sep24CustodyPaymentHandler: Sep24CustodyPaymentHandler @MockK(relaxed = true) private lateinit var sep31CustodyPaymentHandler: Sep31CustodyPaymentHandler @@ -39,6 +44,7 @@ class CustodyEventServiceTest { custodyEventService = CustodyEventServiceTestImpl( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler ) @@ -61,6 +67,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } @@ -84,12 +92,94 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onSent(any(), any()) } } + @Test + fun test_handleEvent_sep6_receive() { + val payment = + CustodyPayment.builder() + .externalTxId("testExternalTxId") + .to("testTo") + .transactionMemo("testMemo") + .assetType("credit_alphanum4") + .build() + val txn = JdbcCustodyTransaction.builder().kind("receive").protocol("6").build() + + every { custodyTransactionRepo.findByExternalTxId("testExternalTxId") } returns txn + every { + custodyTransactionRepo.findFirstByToAccountAndMemoOrderByCreatedAtDesc(any(), any()) + } returns null + + custodyEventService.handlePayment(payment) + + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onSent(any(), any()) } + } + + @ValueSource(strings = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handleEvent_sep6_deposit(kind: String) { + val payment = + CustodyPayment.builder() + .externalTxId("testExternalTxId") + .to("testTo") + .transactionMemo("testMemo") + .assetType("credit_alphanum4") + .build() + val txn = JdbcCustodyTransaction.builder().kind(kind).protocol("6").build() + + every { custodyTransactionRepo.findByExternalTxId("testExternalTxId") } returns txn + every { + custodyTransactionRepo.findFirstByToAccountAndMemoOrderByCreatedAtDesc(any(), any()) + } returns null + + custodyEventService.handlePayment(payment) + + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 1) { sep6CustodyPaymentHandler.onSent(any(), any()) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(txn, payment) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onSent(any(), any()) } + } + + @ValueSource(strings = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handleEvent_sep6_withdrawal(kind: String) { + val payment = + CustodyPayment.builder() + .externalTxId("testExternalTxId") + .to("testTo") + .transactionMemo("testMemo") + .assetType("credit_alphanum4") + .build() + val txn = JdbcCustodyTransaction.builder().kind(kind).protocol("6").build() + + every { custodyTransactionRepo.findByExternalTxId(any()) } returns null + every { + custodyTransactionRepo.findFirstByToAccountAndMemoOrderByCreatedAtDesc("testTo", "testMemo") + } returns txn + + custodyEventService.handlePayment(payment) + + verify(exactly = 1) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(txn, payment) } + verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep31CustodyPaymentHandler.onSent(any(), any()) } + } + @Test fun test_handleEvent_sep24_receive() { val payment = @@ -108,6 +198,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } @@ -132,6 +224,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 1) { sep24CustodyPaymentHandler.onSent(txn, payment) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } @@ -156,6 +250,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 1) { sep24CustodyPaymentHandler.onReceived(txn, payment) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } @@ -180,6 +276,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep31CustodyPaymentHandler.onReceived(any(), any()) } @@ -204,6 +302,8 @@ class CustodyEventServiceTest { custodyEventService.handlePayment(payment) + verify(exactly = 0) { sep6CustodyPaymentHandler.onReceived(any(), any()) } + verify(exactly = 0) { sep6CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onReceived(any(), any()) } verify(exactly = 0) { sep24CustodyPaymentHandler.onSent(any(), any()) } verify(exactly = 1) { sep31CustodyPaymentHandler.onReceived(txn, payment) } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyTransactionServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyTransactionServiceTest.kt index c7c9ebeaa2..2c5ec0d9bf 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyTransactionServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/CustodyTransactionServiceTest.kt @@ -6,7 +6,6 @@ import io.mockk.impl.annotations.MockK import io.mockk.slot import io.mockk.verify import java.time.Instant -import java.util.* import kotlin.test.assertTrue import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandlerTest.kt new file mode 100644 index 0000000000..df5cc6f0ec --- /dev/null +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/Sep6CustodyPaymentHandlerTest.kt @@ -0,0 +1,393 @@ +package org.stellar.anchor.platform.custody + +import io.micrometer.core.instrument.Counter +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.slot +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode +import org.stellar.anchor.apiclient.PlatformApiClient +import org.stellar.anchor.metrics.MetricsService +import org.stellar.anchor.platform.config.RpcConfig +import org.stellar.anchor.platform.data.JdbcCustodyTransaction +import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo +import org.stellar.anchor.platform.service.AnchorMetrics +import org.stellar.anchor.util.GsonUtils + +class Sep6CustodyPaymentHandlerTest { + + @MockK(relaxed = true) private lateinit var custodyTransactionRepo: JdbcCustodyTransactionRepo + + @MockK(relaxed = true) private lateinit var platformApiClient: PlatformApiClient + + @MockK(relaxed = true) private lateinit var paymentReceivedCounter: Counter + + @MockK(relaxed = true) private lateinit var paymentSentCounter: Counter + + @MockK(relaxed = true) private lateinit var rpcConfig: RpcConfig + + @MockK(relaxed = true) private lateinit var metricsService: MetricsService + + private lateinit var sep6CustodyPaymentHandler: Sep6CustodyPaymentHandler + + private val gson = GsonUtils.getInstance() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true) + sep6CustodyPaymentHandler = + Sep6CustodyPaymentHandler( + custodyTransactionRepo, + platformApiClient, + rpcConfig, + metricsService + ) + } + + @Test + fun test_handleEvent_onReceived_payment_success() { + val txn = + gson.fromJson( + custodyTransactionInputSep6WithdrawalPayment, + JdbcCustodyTransaction::class.java + ) + val payment = gson.fromJson(custodyPaymentWithId, CustodyPayment::class.java) + + val custodyTxCapture = slot() + + every { rpcConfig.customMessages.incomingPaymentReceived } returns "payment received" + every { custodyTransactionRepo.save(capture(custodyTxCapture)) } returns txn + every { + metricsService.counter(AnchorMetrics.PAYMENT_RECEIVED, "asset", "testAmountInAsset") + } returns paymentReceivedCounter + + sep6CustodyPaymentHandler.onReceived(txn, payment) + + verify(exactly = 1) { paymentReceivedCounter.increment(1.0000000) } + verify(exactly = 1) { + platformApiClient.notifyOnchainFundsReceived( + txn.id, + payment.transactionHash, + payment.amount, + "payment received" + ) + } + + JSONAssert.assertEquals( + custodyTransactionDbSep6WithdrawalPayment, + gson.toJson(custodyTxCapture.captured), + JSONCompareMode.STRICT + ) + } + + @Test + fun test_handleEvent_onReceived_refund_success() { + val txn = + gson.fromJson(custodyTransactionInputSep6WithdrawalRefund, JdbcCustodyTransaction::class.java) + val payment = gson.fromJson(custodyPaymentWithId, CustodyPayment::class.java) + + val custodyTxCapture = slot() + + every { rpcConfig.customMessages.incomingPaymentReceived } returns "payment received" + every { custodyTransactionRepo.save(capture(custodyTxCapture)) } returns txn + + sep6CustodyPaymentHandler.onReceived(txn, payment) + + verify(exactly = 0) { metricsService.counter(AnchorMetrics.PAYMENT_RECEIVED, any(), any()) } + verify(exactly = 1) { + platformApiClient.notifyRefundSent( + txn.id, + payment.transactionHash, + payment.amount, + txn.amountFee, + txn.asset + ) + } + + JSONAssert.assertEquals( + custodyTransactionDbSep6WithdrawalRefund, + gson.toJson(custodyTxCapture.captured), + JSONCompareMode.STRICT + ) + } + + @Test + fun test_handleEvent_onReceived_payment_error() { + val txn = + gson.fromJson( + custodyTransactionInputSep6WithdrawalPayment, + JdbcCustodyTransaction::class.java + ) + val payment = gson.fromJson(custodyPaymentWithIdError, CustodyPayment::class.java) + + val custodyTxCapture = slot() + + every { rpcConfig.customMessages.custodyTransactionFailed } returns "payment failed" + every { custodyTransactionRepo.save(capture(custodyTxCapture)) } returns txn + + sep6CustodyPaymentHandler.onReceived(txn, payment) + + verify(exactly = 0) { metricsService.counter(AnchorMetrics.PAYMENT_SENT, any(), any()) } + verify(exactly = 1) { platformApiClient.notifyTransactionError(txn.id, "payment failed") } + + JSONAssert.assertEquals( + custodyTransactionDbSep6WithdrawalPaymentError, + gson.toJson(custodyTxCapture.captured), + JSONCompareMode.STRICT + ) + } + + @Test + fun test_handleEvent_onSent_success() { + val txn = + gson.fromJson(custodyTransactionInputSep6DepositPayment, JdbcCustodyTransaction::class.java) + val payment = gson.fromJson(custodyPaymentWithId, CustodyPayment::class.java) + + val custodyTxCapture = slot() + + every { rpcConfig.customMessages.outgoingPaymentSent } returns "payment sent" + every { custodyTransactionRepo.save(capture(custodyTxCapture)) } returns txn + every { + metricsService.counter(AnchorMetrics.PAYMENT_SENT, "asset", "testAmountInAsset") + } returns paymentSentCounter + + sep6CustodyPaymentHandler.onSent(txn, payment) + + verify(exactly = 1) { paymentSentCounter.increment(1.0000000) } + verify(exactly = 1) { + platformApiClient.notifyOnchainFundsSent(txn.id, payment.transactionHash, "payment sent") + } + + JSONAssert.assertEquals( + custodyTransactionDbSep6DepositPayment, + gson.toJson(custodyTxCapture.captured), + JSONCompareMode.STRICT + ) + } + + @Test + fun test_handleEvent_onSent_payment_error() { + val txn = + gson.fromJson(custodyTransactionInputSep6DepositPayment, JdbcCustodyTransaction::class.java) + val payment = gson.fromJson(custodyPaymentWithIdError, CustodyPayment::class.java) + + val custodyTxCapture = slot() + + every { rpcConfig.customMessages.custodyTransactionFailed } returns "payment failed" + every { custodyTransactionRepo.save(capture(custodyTxCapture)) } returns txn + + sep6CustodyPaymentHandler.onSent(txn, payment) + + verify(exactly = 0) { metricsService.counter(AnchorMetrics.PAYMENT_SENT, any(), any()) } + verify(exactly = 1) { platformApiClient.notifyTransactionError(txn.id, "payment failed") } + + JSONAssert.assertEquals( + custodyTransactionDbSep6DepositPaymentError, + gson.toJson(custodyTxCapture.captured), + JSONCompareMode.STRICT + ) + } + + private val custodyPaymentWithId = + """ + { + "id": "12345", + "externalTxId": "testEventId", + "type": "payment", + "from": "testFrom", + "to": "testTo", + "amount": "1.0000000", + "assetType": "credit_alphanum4", + "assetName": "testAmountInAsset", + "updatedAt": "2023-05-10T10:18:25.778Z", + "status": "SUCCESS", + "transactionHash": "testTxHash", + "transactionMemoType": "none", + "transactionEnvelope": "testEnvelopeXdr" + } + """ + + private val custodyPaymentWithIdError = + """ + { + "id": "12345", + "externalTxId": "testEventId", + "type": "payment", + "from": "testFrom", + "to": "testTo", + "amount": "1.0000000", + "assetType": "credit_alphanum4", + "assetName": "testAmountInAsset", + "updatedAt": "2023-05-10T10:18:25.778Z", + "status": "ERROR", + "transactionHash": "testTxHash", + "transactionMemoType": "none", + "transactionEnvelope": "testEnvelopeXdr" + } + """ + + private val custodyTransactionDbSep6DepositPayment = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "external_tx_id": "testEventId", + "status": "completed", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "updated_at": "2023-05-10T10:18:25.778Z", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFrom", + "to_account": "testToAccount", + "kind": "deposit", + "reconciliation_attempt_count": 0, + "type": "payment" + } + """ + + private val custodyTransactionDbSep6DepositPaymentError = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "external_tx_id": "testEventId", + "status": "failed", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "updated_at": "2023-05-10T10:18:25.778Z", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFrom", + "to_account": "testToAccount", + "kind": "deposit", + "reconciliation_attempt_count": 0, + "type": "payment" + } + """ + + private val custodyTransactionDbSep6WithdrawalPayment = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "external_tx_id": "testEventId", + "status": "completed", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "updated_at": "2023-05-10T10:18:25.778Z", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFrom", + "to_account": "testToAccount", + "kind": "withdrawal", + "reconciliation_attempt_count": 0, + "type": "payment" + } + """ + + private val custodyTransactionDbSep6WithdrawalPaymentError = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "external_tx_id": "testEventId", + "status": "failed", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "updated_at": "2023-05-10T10:18:25.778Z", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFrom", + "to_account": "testToAccount", + "kind": "withdrawal", + "reconciliation_attempt_count": 0, + "type": "payment" + } + """ + + private val custodyTransactionDbSep6WithdrawalRefund = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "external_tx_id": "testEventId", + "status": "completed", + "amount": "1", + "amount_fee": "0.1", + "asset": "stellar:testAmountInAsset", + "updated_at": "2023-05-10T10:18:25.778Z", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFrom", + "to_account": "testToAccount", + "kind": "withdrawal", + "reconciliation_attempt_count": 0, + "type": "refund" + } + """ + + private val custodyTransactionInputSep6DepositPayment = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "status": "submitted", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFromAccount1", + "to_account": "testToAccount", + "kind": "deposit", + "type": "payment" + } + """ + + private val custodyTransactionInputSep6WithdrawalPayment = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "status": "submitted", + "amount": "1", + "asset": "stellar:testAmountInAsset", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFromAccount1", + "to_account": "testToAccount", + "kind": "withdrawal", + "type": "payment" + } + """ + + private val custodyTransactionInputSep6WithdrawalRefund = + """ + { + "id": "testId", + "sep_tx_id": "testId", + "status": "submitted", + "amount": "1", + "amount_fee": "0.1", + "asset": "stellar:testAmountInAsset", + "memo": "testMemo", + "memo_type": "testMemoType", + "protocol": "6", + "from_account": "testFromAccount1", + "to_account": "testToAccount", + "kind": "withdrawal", + "type": "refund" + } + """ +} diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventServiceTest.kt index 310cfdc09a..62b04e4efd 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksEventServiceTest.kt @@ -1,12 +1,7 @@ package org.stellar.anchor.platform.custody.fireblocks import com.google.gson.reflect.TypeToken -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs -import io.mockk.slot -import io.mockk.verify +import io.mockk.* import java.security.Signature import java.util.* import kotlin.test.assertEquals @@ -29,6 +24,7 @@ import org.stellar.anchor.platform.config.PropertyCustodySecretConfig import org.stellar.anchor.platform.custody.CustodyPayment import org.stellar.anchor.platform.custody.Sep24CustodyPaymentHandler import org.stellar.anchor.platform.custody.Sep31CustodyPaymentHandler +import org.stellar.anchor.platform.custody.Sep6CustodyPaymentHandler import org.stellar.anchor.platform.custody.fireblocks.FireblocksEventService.FIREBLOCKS_SIGNATURE_HEADER import org.stellar.anchor.platform.data.JdbcCustodyTransaction import org.stellar.anchor.platform.data.JdbcCustodyTransactionRepo @@ -51,6 +47,7 @@ class FireblocksEventServiceTest { private lateinit var secretConfig: PropertyCustodySecretConfig private lateinit var custodyTransactionRepo: JdbcCustodyTransactionRepo + private lateinit var sep6CustodyPaymentHandler: Sep6CustodyPaymentHandler private lateinit var sep24CustodyPaymentHandler: Sep24CustodyPaymentHandler private lateinit var sep31CustodyPaymentHandler: Sep31CustodyPaymentHandler private lateinit var horizon: Horizon @@ -62,6 +59,7 @@ class FireblocksEventServiceTest { fun setUp() { secretConfig = mockk() custodyTransactionRepo = mockk() + sep6CustodyPaymentHandler = mockk() sep24CustodyPaymentHandler = mockk() sep31CustodyPaymentHandler = mockk() horizon = mockk() @@ -77,6 +75,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -99,6 +98,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -135,6 +135,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -171,6 +172,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -212,6 +214,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -257,6 +260,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -319,6 +323,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -349,6 +354,7 @@ class FireblocksEventServiceTest { assertThrows { FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -365,6 +371,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -386,6 +393,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -407,6 +415,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -429,6 +438,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, @@ -450,6 +460,7 @@ class FireblocksEventServiceTest { val eventsService = FireblocksEventService( custodyTransactionRepo, + sep6CustodyPaymentHandler, sep24CustodyPaymentHandler, sep31CustodyPaymentHandler, horizon, diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandlerTest.kt similarity index 87% rename from platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedTest.kt rename to platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandlerTest.kt index 14800814b7..8e355d334c 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyAmountsUpdatedHandlerTest.kt @@ -45,7 +45,7 @@ import org.stellar.anchor.sep31.Sep31TransactionStore import org.stellar.anchor.sep6.Sep6TransactionStore import org.stellar.anchor.util.GsonUtils -class NotifyAmountsUpdatedTest { +class NotifyAmountsUpdatedHandlerTest { companion object { private val gson = GsonUtils.getInstance() @@ -332,25 +332,58 @@ class NotifyAmountsUpdatedTest { assertTrue(sep24TxnCapture.captured.updatedAt <= endDate) } + @CsvSource(value = ["deposit", "deposit-exchange"]) @ParameterizedTest - @CsvSource( - value = - [ - "deposit, incomplete", - "deposit, pending_anchor", - "deposit, pending_customer_info_update", - "deposit-exchange, incomplete", - "deposit-exchange, pending_anchor", - "deposit-exchange, pending_customer_info_update", - "withdrawal, incomplete", - "withdrawal, pending_anchor", - "withdrawal, pending_customer_info_update", - "withdrawal-exchange, incomplete", - "withdrawal-exchange, pending_anchor", - "withdrawal-exchange, pending_customer_info_update" - ] - ) - fun test_handle_sep6_ok(kind: String, status: String) { + fun test_handle_sep6_unsupportedKind(kind: String) { + val request = NotifyAmountsUpdatedRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_amounts_updated] is not supported. Status[pending_anchor], kind[$kind], protocol[6], funds received[true]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep6_transferNotReceived() { + val request = NotifyAmountsUpdatedRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.kind = WITHDRAWAL.kind + txn6.status = PENDING_ANCHOR.toString() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_amounts_updated] is not supported. Status[pending_anchor], kind[withdrawal], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @ParameterizedTest + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + fun test_handle_sep6_ok(kind: String) { + val transferReceivedAt = Instant.now() val request = NotifyAmountsUpdatedRequest.builder() .transactionId(TX_ID) @@ -358,13 +391,14 @@ class NotifyAmountsUpdatedTest { .amountFee(AmountRequest("0.1")) .build() val txn6 = JdbcSep6Transaction() - txn6.status = status + txn6.status = PENDING_ANCHOR.toString() txn6.kind = kind txn6.requestAssetCode = FIAT_USD_CODE txn6.amountOutAsset = STELLAR_USDC txn6.amountOut = "1.8" txn6.amountFeeAsset = STELLAR_USDC txn6.amountFee = "0.2" + txn6.transferReceivedAt = transferReceivedAt val sep6TxnCapture = slot() val anchorEventCapture = slot() @@ -393,6 +427,7 @@ class NotifyAmountsUpdatedTest { expectedSep6Txn.amountOut = "0.9" expectedSep6Txn.amountFeeAsset = STELLAR_USDC expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.transferReceivedAt = transferReceivedAt JSONAssert.assertEquals( gson.toJson(expectedSep6Txn), @@ -408,6 +443,7 @@ class NotifyAmountsUpdatedTest { expectedResponse.amountOut = Amount("0.9", STELLAR_USDC) expectedResponse.amountFee = Amount("0.1", STELLAR_USDC) expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) JSONAssert.assertEquals( diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandlerTest.kt index 7208642db0..beb7ac816e 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundPendingHandlerTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.event.AnchorEvent @@ -17,16 +19,14 @@ import org.stellar.anchor.api.exception.BadRequestException import org.stellar.anchor.api.exception.rpc.InvalidParamsException import org.stellar.anchor.api.exception.rpc.InvalidRequestException import org.stellar.anchor.api.platform.GetTransactionResponse +import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24 -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_38 +import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.* import org.stellar.anchor.api.rpc.method.AmountAssetRequest import org.stellar.anchor.api.rpc.method.NotifyRefundPendingRequest import org.stellar.anchor.api.sep.SepTransactionStatus.* -import org.stellar.anchor.api.shared.Amount -import org.stellar.anchor.api.shared.RefundPayment -import org.stellar.anchor.api.shared.Refunds +import org.stellar.anchor.api.shared.* import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.event.EventService @@ -36,6 +36,7 @@ import org.stellar.anchor.metrics.MetricsService import org.stellar.anchor.platform.data.JdbcSep24RefundPayment import org.stellar.anchor.platform.data.JdbcSep24Refunds import org.stellar.anchor.platform.data.JdbcSep24Transaction +import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.platform.service.AnchorMetrics.PLATFORM_RPC_TRANSACTION import org.stellar.anchor.platform.validator.RequestValidator import org.stellar.anchor.sep24.Sep24TransactionStore @@ -98,6 +99,7 @@ class NotifyRefundPendingHandlerTest { txn24.status = PENDING_ANCHOR.toString() val spyTxn24 = spyk(txn24) + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns spyTxn24 every { txn31Store.findByTransactionId(any()) } returns null every { spyTxn24.protocol } returns SEP_38.sep.toString() @@ -108,49 +110,7 @@ class NotifyRefundPendingHandlerTest { ex.message ) - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - - @Test - fun test_handle_unsupportedKind() { - val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() - val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() - txn24.kind = WITHDRAWAL.kind - txn24.transferReceivedAt = Instant.now() - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[notify_refund_pending] is not supported. Status[pending_anchor], kind[withdrawal], protocol[24], funds received[true]", - ex.message - ) - - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - - @Test - fun test_handle_unsupportedStatus() { - val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() - val txn24 = JdbcSep24Transaction() - txn24.status = INCOMPLETE.toString() - txn24.kind = DEPOSIT.kind - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[notify_refund_pending] is not supported. Status[incomplete], kind[deposit], protocol[24], funds received[false]", - ex.message - ) - + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -164,6 +124,7 @@ class NotifyRefundPendingHandlerTest { txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { requestValidator.validate(request) } throws @@ -172,6 +133,7 @@ class NotifyRefundPendingHandlerTest { val ex = assertThrows { handler.handle(request) } assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -196,6 +158,7 @@ class NotifyRefundPendingHandlerTest { txn24.transferReceivedAt = Instant.now() txn24.kind = DEPOSIT.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null @@ -208,6 +171,7 @@ class NotifyRefundPendingHandlerTest { ex = assertThrows { handler.handle(request) } assertEquals("refund.amountFee.amount should be non-negative", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -233,6 +197,7 @@ class NotifyRefundPendingHandlerTest { txn24.transferReceivedAt = Instant.now() txn24.kind = DEPOSIT.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null @@ -248,13 +213,61 @@ class NotifyRefundPendingHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_unsupportedKind() { + val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() + val txn24 = JdbcSep24Transaction() + txn24.status = PENDING_ANCHOR.toString() + txn24.kind = WITHDRAWAL.kind + txn24.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_refund_pending] is not supported. Status[pending_anchor], kind[withdrawal], protocol[24], funds received[true]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_ok_first_refund() { + fun test_handle_sep24_unsupportedStatus() { + val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() + val txn24 = JdbcSep24Transaction() + txn24.status = INCOMPLETE.toString() + txn24.kind = DEPOSIT.kind + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_refund_pending] is not supported. Status[incomplete], kind[deposit], protocol[24], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_ok_first_refund() { val transferReceivedAt = Instant.now() val request = NotifyRefundPendingRequest.builder() @@ -285,6 +298,7 @@ class NotifyRefundPendingHandlerTest { payment.amount = request.refund.amount.amount payment.fee = request.refund.amountFee.amount + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -296,6 +310,7 @@ class NotifyRefundPendingHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -363,7 +378,7 @@ class NotifyRefundPendingHandlerTest { } @Test - fun test_handle_ok_second_refund() { + fun test_handle_sep24_ok_second_refund() { val transferReceivedAt = Instant.now() val request = NotifyRefundPendingRequest.builder() @@ -398,6 +413,7 @@ class NotifyRefundPendingHandlerTest { refunds.payments = listOf(payment) txn24.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -409,6 +425,7 @@ class NotifyRefundPendingHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -476,7 +493,7 @@ class NotifyRefundPendingHandlerTest { } @Test - fun test_handle_more_then_amount_in() { + fun test_handle_sep24_more_then_amount_in() { val transferReceivedAt = Instant.now() val request = NotifyRefundPendingRequest.builder() @@ -499,6 +516,7 @@ class NotifyRefundPendingHandlerTest { txn24.amountFee = "0.1" txn24.amountFeeAsset = FIAT_USD + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(any()) } returns null @@ -506,6 +524,329 @@ class NotifyRefundPendingHandlerTest { val ex = assertThrows { handler.handle(request) } assertEquals("Refund amount exceeds amount_in", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedKind(kind: String) { + val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_refund_pending] is not supported. Status[pending_anchor], kind[$kind], protocol[6], funds received[true]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedStatus(kind: String) { + val request = NotifyRefundPendingRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = INCOMPLETE.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_refund_pending] is not supported. Status[incomplete], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_first_refund(kind: String) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundPendingRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundPendingRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "1" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment = RefundPayment() + payment.id = request.refund.id + payment.idType = RefundPayment.IdType.EXTERNAL + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0", FIAT_USD) + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_EXTERNAL.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "1" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("1", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0", FIAT_USD) + expectedRefunds.payments = arrayOf(payment) + expectedSep6Txn.refunds = expectedRefunds + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_EXTERNAL + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("1", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment = RefundPayment() + refundPayment.amount = Amount("1", txn6.amountInAsset) + refundPayment.fee = Amount("0", txn6.amountFeeAsset) + refundPayment.id = request.refund.id + refundPayment.idType = RefundPayment.IdType.EXTERNAL + val refunded = Amount("1", txn6.amountInAsset) + val refundedFee = Amount("0", txn6.amountFeeAsset) + expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_second_refund(kind: String) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundPendingRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundPendingRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "2.2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment = RefundPayment() + payment.id = request.refund.id + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0.1", FIAT_USD) + val refunds = Refunds() + payment.id = request.refund.id + payment.idType = RefundPayment.IdType.EXTERNAL + refunds.amountRefunded = Amount("1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", STELLAR_USDC) + refunds.payments = arrayOf(payment) + txn6.refunds = refunds + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_EXTERNAL.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "2.2" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("2.2", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0.2", FIAT_USD) + expectedRefunds.payments = arrayOf(payment, payment) + expectedSep6Txn.refunds = expectedRefunds + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_EXTERNAL + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("2.2", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment = RefundPayment() + refundPayment.amount = Amount("1", txn6.amountInAsset) + refundPayment.fee = Amount("0.1", txn6.amountFeeAsset) + refundPayment.id = request.refund.id + refundPayment.idType = RefundPayment.IdType.EXTERNAL + val refunded = Amount("2.2", txn6.amountInAsset) + val refundedFee = Amount("0.2", txn6.amountFeeAsset) + expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment, refundPayment)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_more_then_amount_in(kind: String) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundPendingRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundPendingRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = DEPOSIT.kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "1" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals("Refund amount exceeds amount_in", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundSentHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundSentHandlerTest.kt index 060c99bc40..73b28b70f4 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundSentHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyRefundSentHandlerTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.Customization import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode @@ -19,12 +21,14 @@ import org.stellar.anchor.api.exception.BadRequestException import org.stellar.anchor.api.exception.rpc.InvalidParamsException import org.stellar.anchor.api.exception.rpc.InvalidRequestException import org.stellar.anchor.api.platform.GetTransactionResponse +import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.* import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.* import org.stellar.anchor.api.rpc.method.AmountAssetRequest import org.stellar.anchor.api.rpc.method.NotifyRefundSentRequest import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.anchor.api.shared.* +import org.stellar.anchor.api.shared.RefundPayment.IdType import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.event.EventService @@ -32,12 +36,6 @@ import org.stellar.anchor.event.EventService.EventQueue.TRANSACTION import org.stellar.anchor.event.EventService.Session import org.stellar.anchor.metrics.MetricsService import org.stellar.anchor.platform.data.* -import org.stellar.anchor.platform.data.JdbcSep24RefundPayment -import org.stellar.anchor.platform.data.JdbcSep24Refunds -import org.stellar.anchor.platform.data.JdbcSep24Transaction -import org.stellar.anchor.platform.data.JdbcSep31RefundPayment -import org.stellar.anchor.platform.data.JdbcSep31Refunds -import org.stellar.anchor.platform.data.JdbcSep31Transaction import org.stellar.anchor.platform.service.AnchorMetrics.PLATFORM_RPC_TRANSACTION import org.stellar.anchor.platform.validator.RequestValidator import org.stellar.anchor.sep24.Sep24TransactionStore @@ -101,6 +99,7 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind val spyTxn24 = spyk(txn24) + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns spyTxn24 every { txn31Store.findByTransactionId(any()) } returns null every { spyTxn24.protocol } returns SEP_38.sep.toString() @@ -111,6 +110,7 @@ class NotifyRefundSentHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -124,6 +124,7 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null @@ -141,6 +142,7 @@ class NotifyRefundSentHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -154,12 +156,36 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null val ex = assertThrows { handler.handle(request) } assertEquals("refund is required", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange", "withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_invalidRequest_missing_refunds(kind: String) { + val request = NotifyRefundSentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals("refund is required", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -173,6 +199,7 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { requestValidator.validate(request) } throws @@ -208,6 +235,7 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -221,6 +249,7 @@ class NotifyRefundSentHandlerTest { ex = assertThrows { handler.handle(request) } assertEquals("refund.amountFee.amount should be non-negative", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -247,6 +276,7 @@ class NotifyRefundSentHandlerTest { txn24.kind = DEPOSIT.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -263,6 +293,50 @@ class NotifyRefundSentHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sent_more_then_amount_in() { + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("10", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn24 = JdbcSep24Transaction() + txn24.status = PENDING_ANCHOR.toString() + txn24.kind = DEPOSIT.kind + txn24.transferReceivedAt = Instant.now() + txn24.requestAssetCode = FIAT_USD_CODE + txn24.amountIn = "1" + txn24.amountInAsset = STELLAR_USDC + txn24.amountFee = "1" + txn24.amountFeeAsset = FIAT_USD + + val sep24TxnCapture = slot() + val payment = JdbcSep24RefundPayment() + payment.id = "1" + payment.amount = "1" + payment.fee = "0" + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(capture(sep24TxnCapture)) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals("Refund amount exceeds amount_in", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -300,6 +374,7 @@ class NotifyRefundSentHandlerTest { payment.amount = request.refund.amount.amount payment.fee = request.refund.amountFee.amount + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -311,6 +386,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -348,7 +424,7 @@ class NotifyRefundSentHandlerTest { refundPayment.amount = Amount("1", txn24.amountInAsset) refundPayment.fee = Amount("0.1", txn24.amountInAsset) refundPayment.id = request.refund.id - refundPayment.idType = RefundPayment.IdType.STELLAR + refundPayment.idType = IdType.STELLAR val refunded = Amount("1.1", txn24.amountInAsset) val refundedFee = Amount("0.1", txn24.amountInAsset) expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) @@ -417,6 +493,7 @@ class NotifyRefundSentHandlerTest { refunds.payments = listOf(payment1) txn24.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -428,6 +505,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -466,12 +544,12 @@ class NotifyRefundSentHandlerTest { refundPayment1.amount = Amount("1", txn24.amountInAsset) refundPayment1.fee = Amount("0.1", txn24.amountInAsset) refundPayment1.id = "1" - refundPayment1.idType = RefundPayment.IdType.STELLAR + refundPayment1.idType = IdType.STELLAR val refundPayment2 = RefundPayment() refundPayment2.amount = Amount("1", txn24.amountInAsset) refundPayment2.fee = Amount("0.1", txn24.amountInAsset) refundPayment2.id = "2" - refundPayment2.idType = RefundPayment.IdType.STELLAR + refundPayment2.idType = IdType.STELLAR val refunded = Amount("2.2", txn24.amountInAsset) val refundedFee = Amount("0.2", txn24.amountInAsset) expectedResponse.refunds = @@ -535,6 +613,7 @@ class NotifyRefundSentHandlerTest { payment.amount = "1" payment.fee = "0" + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -546,6 +625,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -584,7 +664,7 @@ class NotifyRefundSentHandlerTest { refundPayment.amount = Amount("1", txn24.amountInAsset) refundPayment.fee = Amount("0", txn24.amountInAsset) refundPayment.id = "1" - refundPayment.idType = RefundPayment.IdType.STELLAR + refundPayment.idType = IdType.STELLAR val refunded = Amount("1", txn24.amountInAsset) val refundedFee = Amount("0", txn24.amountInAsset) expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) @@ -616,47 +696,6 @@ class NotifyRefundSentHandlerTest { assertTrue(sep24TxnCapture.captured.completedAt <= endDate) } - @Test - fun test_handle_sent_more_then_amount_in() { - val request = - NotifyRefundSentRequest.builder() - .transactionId(TX_ID) - .refund( - NotifyRefundSentRequest.Refund.builder() - .amount(AmountAssetRequest("10", STELLAR_USDC)) - .amountFee(AmountAssetRequest("0", FIAT_USD)) - .id("1") - .build() - ) - .build() - val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() - txn24.kind = DEPOSIT.kind - txn24.transferReceivedAt = Instant.now() - txn24.requestAssetCode = FIAT_USD_CODE - txn24.amountIn = "1" - txn24.amountInAsset = STELLAR_USDC - txn24.amountFee = "1" - txn24.amountFeeAsset = FIAT_USD - - val sep24TxnCapture = slot() - val payment = JdbcSep24RefundPayment() - payment.id = "1" - payment.amount = "1" - payment.fee = "0" - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - every { txn24Store.save(capture(sep24TxnCapture)) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals("Refund amount exceeds amount_in", ex.message) - - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - @Test fun test_handle_ok_sep24_pending_external_empty_refund() { val transferReceivedAt = Instant.now() @@ -683,6 +722,7 @@ class NotifyRefundSentHandlerTest { refunds.payments = listOf(payment) txn24.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -694,6 +734,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -731,7 +772,7 @@ class NotifyRefundSentHandlerTest { refundPayment.amount = Amount("1", txn24.amountInAsset) refundPayment.fee = Amount("0.1", txn24.amountInAsset) refundPayment.id = "1" - refundPayment.idType = RefundPayment.IdType.STELLAR + refundPayment.idType = IdType.STELLAR val refunded = Amount("1", txn24.amountInAsset) val refundedFee = Amount("0.1", txn24.amountInAsset) expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) @@ -800,6 +841,7 @@ class NotifyRefundSentHandlerTest { refunds.payments = listOf(payment1, payment2) txn24.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -811,6 +853,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -856,12 +899,12 @@ class NotifyRefundSentHandlerTest { refundPayment1.amount = Amount("1.5", txn24.amountInAsset) refundPayment1.fee = Amount("0.2", txn24.amountInAsset) refundPayment1.id = request.refund.id - refundPayment1.idType = RefundPayment.IdType.STELLAR + refundPayment1.idType = IdType.STELLAR val refundPayment2 = RefundPayment() refundPayment2.amount = Amount("0.1", txn24.amountInAsset) refundPayment2.fee = Amount("0", txn24.amountInAsset) refundPayment2.id = "2" - refundPayment2.idType = RefundPayment.IdType.STELLAR + refundPayment2.idType = IdType.STELLAR val refunded = Amount("1.8", txn24.amountInAsset) val refundedFee = Amount("0.2", txn24.amountInAsset) expectedResponse.refunds = @@ -925,6 +968,7 @@ class NotifyRefundSentHandlerTest { refunds.payments = listOf(payment) txn24.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -932,6 +976,709 @@ class NotifyRefundSentHandlerTest { val ex = assertThrows { handler.handle(request) } assertEquals("Invalid refund id", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource( + value = + [ + "deposit, EXTERNAL", + "deposit-exchange, EXTERNAL", + "withdrawal, STELLAR", + "withdrawal-exchange, STELLAR" + ] + ) + @ParameterizedTest + fun test_handle_ok_sep6_partial_refund(kind: String, refundIdType: IdType) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.amountIn = "2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountInAsset = STELLAR_USDC + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment = RefundPayment() + payment.id = request.refund.id + payment.idType = refundIdType + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0.1", FIAT_USD) + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "2" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("1.1", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0.1", FIAT_USD) + expectedRefunds.payments = arrayOf(payment) + expectedSep6Txn.refunds = expectedRefunds + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("2", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment = RefundPayment() + refundPayment.amount = Amount("1", txn6.amountInAsset) + refundPayment.fee = Amount("0.1", txn6.amountFeeAsset) + refundPayment.id = request.refund.id + refundPayment.idType = refundIdType + val refunded = Amount("1.1", txn6.amountInAsset) + val refundedFee = Amount("0.1", txn6.amountFeeAsset) + expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource( + value = + [ + "deposit, EXTERNAL", + "deposit-exchange, EXTERNAL", + "withdrawal, STELLAR", + "withdrawal-exchange, STELLAR" + ] + ) + @ParameterizedTest + fun test_handle_ok_sep6_full_refund(kind: String, refundIdType: IdType) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .id("2") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "2.2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment1 = RefundPayment() + payment1.id = "1" + payment1.idType = refundIdType + payment1.amount = Amount("1", STELLAR_USDC) + payment1.fee = Amount("0.1", FIAT_USD) + val payment2 = RefundPayment() + payment2.id = request.refund.id + payment2.idType = refundIdType + payment2.amount = Amount(request.refund.amount.amount, STELLAR_USDC) + payment2.fee = Amount(request.refund.amountFee.amount, FIAT_USD) + val refunds = Refunds() + refunds.amountRefunded = Amount("1.1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", FIAT_USD) + refunds.payments = arrayOf(payment1) + txn6.refunds = refunds + + every { txn6Store.findByTransactionId(any()) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = REFUNDED.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "2.2" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("2.2", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0.2", FIAT_USD) + expectedRefunds.payments = arrayOf(payment1, payment2) + expectedSep6Txn.refunds = expectedRefunds + expectedSep6Txn.completedAt = sep6TxnCapture.captured.completedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + CustomComparator(JSONCompareMode.STRICT, Customization("completed_at") { _, _ -> true }) + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = REFUNDED + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("2.2", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment1 = RefundPayment() + refundPayment1.amount = Amount("1", txn6.amountInAsset) + refundPayment1.fee = Amount("0.1", txn6.amountFeeAsset) + refundPayment1.id = "1" + refundPayment1.idType = refundIdType + val refundPayment2 = RefundPayment() + refundPayment2.amount = Amount("1", txn6.amountInAsset) + refundPayment2.fee = Amount("0.1", txn6.amountFeeAsset) + refundPayment2.id = "2" + refundPayment2.idType = refundIdType + val refunded = Amount("2.2", txn6.amountInAsset) + val refundedFee = Amount("0.2", txn6.amountFeeAsset) + expectedResponse.refunds = + Refunds(refunded, refundedFee, arrayOf(refundPayment1, refundPayment2)) + expectedResponse.completedAt = sep6TxnCapture.captured.completedAt + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + CustomComparator(JSONCompareMode.STRICT, Customization("completed_at") { _, _ -> true }) + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + assertTrue(sep6TxnCapture.captured.completedAt >= startDate) + assertTrue(sep6TxnCapture.captured.completedAt <= endDate) + } + + @CsvSource( + value = + [ + "deposit, EXTERNAL", + "deposit-exchange, EXTERNAL", + "withdrawal, STELLAR", + "withdrawal-exchange, STELLAR" + ] + ) + @ParameterizedTest + fun test_handle_ok_sep6_full_refund_in_single_call(kind: String, refundIdType: IdType) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "1" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment = RefundPayment() + payment.id = "1" + payment.idType = refundIdType + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0", FIAT_USD) + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = REFUNDED.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "1" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("1", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0", FIAT_USD) + expectedRefunds.payments = arrayOf(payment) + expectedSep6Txn.refunds = expectedRefunds + expectedSep6Txn.completedAt = sep6TxnCapture.captured.completedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + CustomComparator(JSONCompareMode.STRICT, Customization("completed_at") { _, _ -> true }) + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = REFUNDED + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("1", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment = RefundPayment() + refundPayment.amount = Amount("1", txn6.amountInAsset) + refundPayment.fee = Amount("0", txn6.amountFeeAsset) + refundPayment.id = "1" + refundPayment.idType = refundIdType + val refunded = Amount("1", txn6.amountInAsset) + val refundedFee = Amount("0", txn6.amountFeeAsset) + expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) + expectedResponse.completedAt = sep6TxnCapture.captured.completedAt + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + CustomComparator(JSONCompareMode.STRICT, Customization("completed_at") { _, _ -> true }) + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + assertTrue(sep6TxnCapture.captured.completedAt >= startDate) + assertTrue(sep6TxnCapture.captured.completedAt <= endDate) + } + + @CsvSource( + value = + [ + "deposit, EXTERNAL", + "deposit-exchange, EXTERNAL", + ] + ) + @ParameterizedTest + fun test_handle_ok_sep6_pending_external_empty_refund(kind: String, refundIdType: IdType) { + val transferReceivedAt = Instant.now() + val request = NotifyRefundSentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_EXTERNAL.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment = RefundPayment() + payment.id = "1" + payment.idType = refundIdType + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0.1", FIAT_USD) + val refunds = Refunds() + refunds.amountRefunded = Amount("1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", FIAT_USD) + refunds.payments = arrayOf(payment) + txn6.refunds = refunds + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "2" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("1", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0.1", FIAT_USD) + expectedRefunds.payments = arrayOf(payment) + expectedSep6Txn.refunds = expectedRefunds + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("2", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment = RefundPayment() + refundPayment.amount = Amount("1", txn6.amountInAsset) + refundPayment.fee = Amount("0.1", txn6.amountFeeAsset) + refundPayment.id = "1" + refundPayment.idType = refundIdType + val refunded = Amount("1", txn6.amountInAsset) + val refundedFee = Amount("0.1", txn6.amountFeeAsset) + expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource( + value = + [ + "deposit, EXTERNAL", + "deposit-exchange, EXTERNAL", + ] + ) + @ParameterizedTest + fun test_handle_ok_sep6_pending_external_override_amount(kind: String, refundIdType: IdType) { + val transferReceivedAt = Instant.now() + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("1.5", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.2", FIAT_USD)) + .id("1") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_EXTERNAL.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val payment1 = RefundPayment() + payment1.id = request.refund.id + payment1.idType = refundIdType + payment1.amount = Amount("1", STELLAR_USDC) + payment1.fee = Amount("0.1", FIAT_USD) + val payment2 = RefundPayment() + payment2.id = "2" + payment2.idType = refundIdType + payment2.amount = Amount("0.1", STELLAR_USDC) + payment2.fee = Amount("0", FIAT_USD) + val refunds = Refunds() + refunds.amountRefunded = Amount("1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", FIAT_USD) + refunds.payments = arrayOf(payment1, payment2) + txn6.refunds = refunds + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountIn = "2" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.transferReceivedAt = transferReceivedAt + val expectedRefunds = Refunds() + expectedRefunds.amountRefunded = Amount("1.8", STELLAR_USDC) + expectedRefunds.amountFee = Amount("0.2", FIAT_USD) + val expectedPayment1 = RefundPayment() + expectedPayment1.id = request.refund.id + expectedPayment1.idType = refundIdType + expectedPayment1.amount = Amount("1.5", STELLAR_USDC) + expectedPayment1.fee = Amount("0.2", FIAT_USD) + val expectedPayment2 = RefundPayment() + expectedPayment2.id = "2" + expectedPayment2.idType = refundIdType + expectedPayment2.amount = Amount("0.1", STELLAR_USDC) + expectedPayment2.fee = Amount("0", FIAT_USD) + expectedRefunds.payments = arrayOf(expectedPayment2, expectedPayment1) + expectedSep6Txn.refunds = expectedRefunds + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("2", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + val refundPayment1 = RefundPayment() + refundPayment1.amount = Amount("1.5", txn6.amountInAsset) + refundPayment1.fee = Amount("0.2", txn6.amountFeeAsset) + refundPayment1.id = request.refund.id + refundPayment1.idType = refundIdType + val refundPayment2 = RefundPayment() + refundPayment2.amount = Amount("0.1", txn6.amountInAsset) + refundPayment2.fee = Amount("0", txn6.amountFeeAsset) + refundPayment2.id = "2" + refundPayment2.idType = refundIdType + val refunded = Amount("1.8", txn6.amountInAsset) + val refundedFee = Amount("0.2", txn6.amountFeeAsset) + expectedResponse.refunds = + Refunds(refunded, refundedFee, arrayOf(refundPayment2, refundPayment1)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_ok_sep6_pending_external_invalid_id(kind: String) { + val request = + NotifyRefundSentRequest.builder() + .transactionId(TX_ID) + .refund( + NotifyRefundSentRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .id("2") + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_EXTERNAL.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountIn = "2" + txn6.amountInAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.amountFeeAsset = FIAT_USD + + val sep6TxnCapture = slot() + val payment = RefundPayment() + payment.id = "1" + payment.idType = IdType.EXTERNAL + payment.amount = Amount("1", STELLAR_USDC) + payment.fee = Amount("0.1", FIAT_USD) + val refunds = Refunds() + refunds.amountRefunded = Amount("1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", FIAT_USD) + refunds.payments = arrayOf(payment) + txn6.refunds = refunds + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals("Invalid refund id", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -969,6 +1716,7 @@ class NotifyRefundSentHandlerTest { val sep31TxnCapture = slot() val anchorEventCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns null every { txn31Store.findByTransactionId(any()) } returns txn31 every { txn31Store.save(capture(sep31TxnCapture)) } returns null @@ -978,6 +1726,7 @@ class NotifyRefundSentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } val expectedSep31Txn = JdbcSep31Transaction() @@ -1020,7 +1769,7 @@ class NotifyRefundSentHandlerTest { refundPayment.amount = Amount("1", txn31.amountInAsset) refundPayment.fee = Amount("0", txn31.amountInAsset) refundPayment.id = "1" - refundPayment.idType = RefundPayment.IdType.STELLAR + refundPayment.idType = IdType.STELLAR val refunded = Amount("1", txn31.amountInAsset) val refundedFee = Amount("0", txn31.amountInAsset) expectedResponse.refunds = Refunds(refunded, refundedFee, arrayOf(refundPayment)) @@ -1056,12 +1805,14 @@ class NotifyRefundSentHandlerTest { val txn31 = JdbcSep31Transaction() txn31.status = PENDING_RECEIVER.toString() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns null every { txn31Store.findByTransactionId(any()) } returns txn31 val ex = assertThrows { handler.handle(request) } assertEquals("refund is required", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -1093,6 +1844,7 @@ class NotifyRefundSentHandlerTest { refunds.payments = listOf(payment) txn31.refunds = refunds + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns null every { txn31Store.findByTransactionId(any()) } returns txn31 @@ -1102,6 +1854,7 @@ class NotifyRefundSentHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt index 0fe4a59846..952e4075a6 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt @@ -86,6 +86,7 @@ class CustodyServiceTest { @Test fun test_createTransaction_sep24Deposit() { val txn = gson.fromJson(sep24DepositEntity, JdbcSep24Transaction::class.java) + val requestCapture = slot() every { custodyApiClient.createTransaction(capture(requestCapture)) } just Runs diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoGeneratoCustodyTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoGeneratoCustodyTest.kt deleted file mode 100644 index 8aec02c01f..0000000000 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoGeneratoCustodyTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.stellar.anchor.platform.service - -import kotlin.test.assertEquals -import org.junit.jupiter.api.Test -import org.stellar.anchor.api.shared.SepDepositInfo -import org.stellar.anchor.platform.data.JdbcSep31Transaction - -class Sep31DepositInfoGeneratoCustodyTest { - - companion object { - private const val TX_ID = "123e4567-e89b-12d3-a456-426614174000" - private const val ADDRESS = "testAccount" - private const val MEMO = "MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc=" - private const val MEMO_TYPE = "hash" - } - - @Test - fun test_sep31_selfGenerator_success() { - val txn = JdbcSep31Transaction() - txn.id = TX_ID - txn.stellarAccountId = ADDRESS - val generator = Sep31DepositInfoSelfGenerator() - - val actualInfo = generator.generate(txn) - - val expectedInfo = SepDepositInfo(ADDRESS, MEMO, MEMO_TYPE) - - assertEquals(expectedInfo, actualInfo) - } -} diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoSelfGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoSelfGeneratorTest.kt new file mode 100644 index 0000000000..b13418dec1 --- /dev/null +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep31DepositInfoSelfGeneratorTest.kt @@ -0,0 +1,46 @@ +package org.stellar.anchor.platform.service + +import java.util.stream.Stream +import kotlin.test.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.stellar.anchor.api.shared.SepDepositInfo +import org.stellar.anchor.platform.data.JdbcSep31Transaction + +class Sep31DepositInfoSelfGeneratorTest { + + companion object { + private const val TX_ID = "123e4567-e89b-12d3-a456-426614174000" + private const val ADDRESS = "testAccount" + private const val MEMO = "MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc=" + private const val MEMO_TYPE = "hash" + + @JvmStatic + fun assets(): Stream { + return Stream.of( + Arguments.of( + "testId1", + "GBJDTHT4562X2H37JMOE6IUTZZSDU6RYGYUNFYCHVFG3J4MYJIMU33HK", + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHRlc3RJZDE=" + ), + Arguments.of("testId2", null, "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHRlc3RJZDI=") + ) + } + } + + @ParameterizedTest + @MethodSource("assets") + fun test_sep31_selfGenerator_success(txnId: String, address: String?, memo: String) { + val txn = JdbcSep31Transaction() + txn.id = txnId + txn.stellarAccountId = address + val generator = Sep31DepositInfoSelfGenerator() + + val actualInfo = generator.generate(txn) + + val expectedInfo = SepDepositInfo(address, memo, "hash") + + assertEquals(expectedInfo, actualInfo) + } +} diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt index 6edb815058..303263a1b5 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt @@ -1,49 +1,61 @@ package org.stellar.anchor.platform.service import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk +import java.util.stream.Stream import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.stellar.anchor.api.sep.AssetInfo +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.stellar.anchor.api.shared.SepDepositInfo import org.stellar.anchor.asset.AssetService +import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.platform.data.JdbcSep6Transaction class Sep6DepositInfoSelfGeneratorTest { companion object { - private val TXN_ID = "testId" - private const val ASSET_CODE = "USDC" - private const val ASSET_ISSUER = "testIssuer" - private const val DISTRIBUTION_ACCOUNT = "testAccount" + @JvmStatic + fun assets(): Stream { + return Stream.of( + Arguments.of( + "testId1", + "USDC", + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "GBJDTHT4562X2H37JMOE6IUTZZSDU6RYGYUNFYCHVFG3J4MYJIMU33HK", + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHRlc3RJZDE=" + ), + Arguments.of("testId2", "USD", null, null, "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHRlc3RJZDI=") + ) + } } - @MockK(relaxed = true) lateinit var assetService: AssetService + private val assetService: AssetService = DefaultAssetService.fromJsonResource("test_assets.json") private lateinit var generator: Sep6DepositInfoSelfGenerator @BeforeEach fun setup() { MockKAnnotations.init(this, relaxUnitFun = true) - val asset = mockk() - every { asset.distributionAccount } returns DISTRIBUTION_ACCOUNT - every { assetService.getAsset(ASSET_CODE, ASSET_ISSUER) } returns asset generator = Sep6DepositInfoSelfGenerator(assetService) } - @Test - fun test_sep6_custodyGenerator_success() { + @ParameterizedTest + @MethodSource("assets") + fun test_sep6_selfGenerator_success( + txnId: String, + assetCode: String, + assetIssuer: String?, + distributionAccount: String?, + memo: String + ) { val txn = JdbcSep6Transaction() - txn.id = TXN_ID - txn.requestAssetCode = ASSET_CODE - txn.requestAssetIssuer = ASSET_ISSUER + txn.id = txnId + txn.requestAssetCode = assetCode + txn.requestAssetIssuer = assetIssuer val result = generator.generate(txn) - val expected = - SepDepositInfo(DISTRIBUTION_ACCOUNT, "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDB0ZXN0SWQ=", "hash") + val expected = SepDepositInfo(distributionAccount, memo, "hash") assertEquals(expected, result) } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt index f2ccf24081..022bb95ca9 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt @@ -101,7 +101,8 @@ class Sep31DepositInfoGeneratorTest { sep10Config, sep31Config, txnStore, - Sep31DepositInfoSelfGenerator(), // set deposit info generator + org.stellar.anchor.platform.service + .Sep31DepositInfoSelfGenerator(), // set deposit info generator quoteStore, clientsConfig, assetService, diff --git a/platform/src/test/resources/test_assets_missing_distribution_account.json b/platform/src/test/resources/test_assets_missing_distribution_account.json new file mode 100644 index 0000000000..7d32ed1235 --- /dev/null +++ b/platform/src/test/resources/test_assets_missing_distribution_account.json @@ -0,0 +1,96 @@ +{ + "assets": [ + { + "schema": "stellar", + "code": "USDC", + "issuer": "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", + "significant_decimals": 2, + "deposit": { + "enabled": true, + "min_amount": 1, + "max_amount": 10000, + "methods": [ + "SEPA", + "SWIFT" + ] + }, + "withdraw": { + "enabled": true, + "min_amount": 1, + "max_amount": 10000, + "methods": [ + "bank_account", + "cash" + ] + }, + "send": { + "fee_fixed": 0, + "fee_percent": 0, + "min_amount": 1, + "max_amount": 1000000 + }, + "sep31": { + "quotes_supported": true, + "quotes_required": true, + "sep12": { + "sender": { + "types": { + "sep31-sender": { + "description": "U.S. citizens limited to sending payments of less than $10,000 in value" + }, + "sep31-large-sender": { + "description": "U.S. citizens that do not have sending limits" + }, + "sep31-foreign-sender": { + "description": "non-U.S. citizens sending payments of less than $10,000 in value" + } + } + }, + "receiver": { + "types": { + "sep31-receiver": { + "description": "U.S. citizens receiving USD" + }, + "sep31-foreign-receiver": { + "description": "non-U.S. citizens receiving USD" + } + } + } + }, + "fields": { + "transaction": { + "receiver_routing_number": { + "description": "routing number of the destination bank account", + "optional": false + }, + "receiver_account_number": { + "description": "bank account number of the destination", + "optional": false + }, + "receiver_phone_number": { + "description": "phone number of the receiver", + "optional": true + }, + "type": { + "description": "type of deposit to make", + "choices": [ + "SEPA", + "SWIFT" + ] + } + } + } + }, + "sep38": { + "exchangeable_assets": [ + "stellar:JPYC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + "iso4217:USD" + ] + }, + "sep6_enabled": true, + "sep24_enabled": true, + "sep31_enabled": true, + "sep38_enabled": true + } + ] +} diff --git a/service-runner/build.gradle.kts b/service-runner/build.gradle.kts index 935d33d0fa..e7e4803e7b 100644 --- a/service-runner/build.gradle.kts +++ b/service-runner/build.gradle.kts @@ -19,11 +19,13 @@ dependencies { implementation(libs.docker.compose.rule) implementation(libs.dotenv) implementation(libs.google.gson) + implementation(libs.kotlin.serialization.json) implementation(libs.okhttp3) implementation(libs.coroutines.core) // From projects implementation(project(":api-schema")) + implementation(project(":lib-util")) implementation(project(":core")) implementation(project(":platform")) implementation(project(":kotlin-reference-server")) @@ -37,3 +39,23 @@ tasks { } application { mainClass.set("org.stellar.anchor.platform.ServiceRunner") } + +/** + * Start all the servers based on the `default` test configuration. + */ +tasks.register("startAllServers") { + println("Starting all servers based on the `default` test configuration.") + group = "application" + classpath = sourceSets["main"].runtimeClasspath + mainClass.set("org.stellar.anchor.platform.run_profiles.RunAllServers") +} + +/** + * Run docker-compose up to start Postgres, Kafka, Zookeeper,etc. + */ +tasks.register("dockerComposeUp") { + println("Running docker-compose up to start Postgres, Kafka, Zookeeper,etc.") + group = "application" + classpath = sourceSets["main"].runtimeClasspath + mainClass.set("org.stellar.anchor.platform.run_profiles.RunDockerDevStack") +} diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/ResourceHelper.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/ResourceHelper.kt index afccf85ef3..e995910d7b 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/ResourceHelper.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/ResourceHelper.kt @@ -39,7 +39,7 @@ fun getResourceFile(resourceName: String): File { val resourceUrl: URL = {}::class.java.classLoader.getResource(fixedResourcePath) ?: throw RuntimeException("Resource $resourceName not found") - File(resourceUrl!!.toURI()) + File(resourceUrl.toURI()) } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestConfig.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestConfig.kt index e2deff829a..5524094e98 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestConfig.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestConfig.kt @@ -1,17 +1,57 @@ package org.stellar.anchor.platform -data class TestConfig(var testProfileName: String) { +import org.stellar.anchor.util.StringHelper.isNotEmpty + +/** + * TestConfig is a class that reads and merges env variables from the following files in the + * following order: The later files override the previous ones. + * - profiles/default/test.env + * - profiles/default/config.env + * - profiles/{testProfileName}/test.env (if testProfileName is not empty) + * - profiles/{testProfileName}/config.env (if testProfileName is not empty) + * - system env variables + * + * It also allows to override env variables with a custom function. + * + * @param testProfileName - name of the test profile to use. If null, the default test profile will + * be used. + * @param customize - a function that allows to override env variables + * @constructor creates a TestConfig instance + */ +class TestConfig { val env = mutableMapOf() // override test profile name with TEST_PROFILE_NAME system env variable - private val profileName = System.getenv("TEST_PROFILE_NAME") ?: testProfileName - init { - // read test.env file - val testEnv = readResourceAsMap("profiles/${profileName}/test.env") - // read config.env file - val configEnv = readResourceAsMap("profiles/${profileName}/config.env") - // merge test.env, config.env and system env variables - env.putAll(testEnv) - env.putAll(configEnv) - env.putAll(System.getenv()) + private val envProfileName = System.getenv("TEST_PROFILE_NAME") ?: null + + constructor(testProfileName: String? = null, customize: () -> Unit = {}) { + var profileName = testProfileName + if (System.getenv("TEST_PROFILE_NAME") != null) profileName = System.getenv("TEST_PROFILE_NAME") + + if (this.envProfileName != null) profileName = this.envProfileName + // starting from the default test.env file + env.putAll(readResourceAsMap("profiles/default/test.env")) + env.putAll(readResourceAsMap("profiles/default/config.env")) + // if test profile name is not "default", read test.env and config.env files + if (isNotEmpty(profileName) && !"default".equals(profileName, ignoreCase = true)) { + // read and merge test.env file + env.putAll(readResourceAsMap("profiles/${profileName}/test.env")) + // read and merge config.env file + env.putAll(readResourceAsMap("profiles/${profileName}/config.env")) + } + + // customize env variables + customize() + + // read and merge system env variables + env.putAll(readSystemEnvAsMap()) + } + + private fun readSystemEnvAsMap(): Map { + val env = mutableMapOf() + System.getenv().forEach { (key, value) -> + val mappedKey = key.replace("_", ".").lowercase() + env[mappedKey] = value + } + return env } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestHelper.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestHelper.kt index de1aa637c0..d65929cbc6 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestHelper.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestHelper.kt @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets.UTF_8 import org.springframework.core.io.DefaultResourceLoader import org.springframework.util.FileCopyUtils import org.stellar.anchor.util.GsonUtils +import org.stellar.anchor.util.Log.debug import org.stellar.anchor.util.StringHelper.json val gson: Gson = GsonUtils.getInstance() @@ -19,8 +20,7 @@ fun resourceAsString(path: String): String { fun printRequest(title: String?, payload: Any? = null) { if (title != null) println(title) if (payload != null) { - print("request=") - println(if (payload is String) payload else json(payload)) + debug("request=" + if (payload is String) payload else json(payload)) } } @@ -31,8 +31,7 @@ fun printResponse(payload: Any?) { fun printResponse(title: String?, payload: Any?) { if (title != null) println(title) if (payload != null) { - print("response=") - println(if (payload is String) payload else json(payload)) + debug("response=" + if (payload is String) payload else json(payload)) } println() } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt index 15ffd6c255..a91eaa75b7 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt @@ -13,21 +13,22 @@ import kotlinx.coroutines.* import org.springframework.context.ConfigurableApplicationContext import org.stellar.anchor.util.Log.info -const val RUN_DOCKER = "run_docker" -const val RUN_ALL_SERVERS = "run_all_servers" -const val RUN_SEP_SERVER = "run_sep_server" -const val RUN_PLATFORM_SERVER = "run_platform_server" -const val RUN_EVENT_PROCESSING_SERVER = "run_event_processing_server" -const val RUN_PAYMENT_OBSERVER = "run_observer" -const val RUN_CUSTODY_SERVER = "run_custody_server" -const val RUN_KOTLIN_REFERENCE_SERVER = "run_kotlin_reference_server" -const val RUN_WALLET_SERVER = "run_wallet_server" +const val RUN_DOCKER = "run.docker" +const val RUN_ALL_SERVERS = "run.all.servers" +const val RUN_SEP_SERVER = "run.sep.server" +const val RUN_PLATFORM_SERVER = "run.platform.server" +const val RUN_EVENT_PROCESSING_SERVER = "run.event.processing.server" +const val RUN_PAYMENT_OBSERVER = "run.observer" +const val RUN_CUSTODY_SERVER = "run.custody.server" +const val RUN_KOTLIN_REFERENCE_SERVER = "run.kotlin.reference.server" +const val RUN_WALLET_SERVER = "run.wallet.server" +const val WALLET_SECRET_KEY = "wallet.secret.key" lateinit var testProfileExecutor: TestProfileExecutor fun main() = runBlocking { info("Starting TestPfofileExecutor...") - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { Runtime.getRuntime() diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServers.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServers.kt index acb4c5b827..8966736b29 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServers.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServers.kt @@ -4,15 +4,13 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { - it.env["run_docker"] = "false" - it.env["run_all_servers"] = "true" + it.env[RUN_DOCKER] = "false" + it.env[RUN_ALL_SERVERS] = "true" } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServersWithDocker.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServersWithDocker.kt index 52602001e6..53e33b865f 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServersWithDocker.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunAllServersWithDocker.kt @@ -4,15 +4,13 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { - it.env["run_docker"] = "true" - it.env["run_all_servers"] = "true" + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "true" } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunCustodyServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunCustodyServer.kt index af4f08341d..229932be97 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunCustodyServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunCustodyServer.kt @@ -4,16 +4,14 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default-custody")) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { - it.env["run_docker"] = "false" - it.env["run_all_servers"] = "false" - it.env["run_custody_server"] = "true" + it.env[RUN_DOCKER] = "false" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_CUSTODY_SERVER] = "true" } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunDockerDevStack.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunDockerDevStack.kt index 94af7f03fa..1b12cbc112 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunDockerDevStack.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunDockerDevStack.kt @@ -5,16 +5,14 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { - it.env["run_docker"] = "true" - it.env["run_all_servers"] = "false" + it.env[RUN_DOCKER] = "true" + it.env[RUN_ALL_SERVERS] = "false" } while (true) { diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunEventProcessingServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunEventProcessingServer.kt index 3b9f2f58a8..4d8c0ec357 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunEventProcessingServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunEventProcessingServer.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { it.env[RUN_DOCKER] = "false" diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunKotlinReferenceServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunKotlinReferenceServer.kt index 82de727067..2a628d1a9a 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunKotlinReferenceServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunKotlinReferenceServer.kt @@ -3,16 +3,14 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) registerShutdownHook(testProfileExecutor) testProfileExecutor.start(true) { - it.env["run_docker"] = "false" - it.env["run_all_servers"] = "false" - it.env["run_kotlin_reference_server"] = "true" + it.env[RUN_DOCKER] = "false" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunPlatformServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunPlatformServer.kt index b4ae0041b3..2010fd2fa2 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunPlatformServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunPlatformServer.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { it.env[RUN_DOCKER] = "false" diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSEP24NoFrontend.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSEP24NoFrontend.kt index 52294efe3f..f6f9347bd8 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSEP24NoFrontend.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSEP24NoFrontend.kt @@ -4,16 +4,14 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { - it.env["run_docker"] = "false" - it.env["run_all_servers"] = "false" - it.env["run_kotlin_reference_server"] = "true" + it.env[RUN_DOCKER] = "false" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" } } diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSepServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSepServer.kt index a1865f3569..fe840a05b5 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSepServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunSepServer.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { it.env[RUN_DOCKER] = "false" diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunStellarObserver.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunStellarObserver.kt index a87f134879..4ea893ba8d 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunStellarObserver.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunStellarObserver.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) launch { registerShutdownHook(testProfileExecutor) } testProfileExecutor.start(true) { it.env[RUN_DOCKER] = "false" diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunWalletServer.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunWalletServer.kt index fcf8ed9417..f950400e95 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunWalletServer.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/run_profiles/RunWalletServer.kt @@ -3,16 +3,14 @@ package org.stellar.anchor.platform.run_profiles import kotlinx.coroutines.runBlocking -import org.stellar.anchor.platform.TestConfig -import org.stellar.anchor.platform.TestProfileExecutor -import org.stellar.anchor.platform.testProfileExecutor +import org.stellar.anchor.platform.* fun main() = runBlocking { - testProfileExecutor = TestProfileExecutor(TestConfig(testProfileName = "default")) + testProfileExecutor = TestProfileExecutor(TestConfig()) registerShutdownHook(testProfileExecutor) testProfileExecutor.start(true) { - it.env["run_docker"] = "false" - it.env["run_all_servers"] = "false" - it.env["run_wallet_server"] = "true" + it.env[RUN_DOCKER] = "false" + it.env[RUN_ALL_SERVERS] = "false" + it.env[RUN_WALLET_SERVER] = "true" } } diff --git a/service-runner/src/main/resources/common/docker-compose.yaml b/service-runner/src/main/resources/common/docker-compose.yaml index ad68ae4e3e..7fcaa6f2a3 100644 --- a/service-runner/src/main/resources/common/docker-compose.yaml +++ b/service-runner/src/main/resources/common/docker-compose.yaml @@ -27,8 +27,8 @@ services: observer: image: stellar/anchor-platform:edge build: - context: ../../../../../integration-tests/src/test - dockerfile: integration-tests/docker-compose-configs/Dockerfile + context: ../../../../../essential-tests/src/test + dockerfile: essential-tests/docker-compose-configs/Dockerfile command: "--stellar-observer" volumes: # add mounts for the new config directory diff --git a/service-runner/src/main/resources/profiles/default-custody/config.env b/service-runner/src/main/resources/profiles/default-custody/config.env index 5ce0f9026d..eff0403db4 100644 --- a/service-runner/src/main/resources/profiles/default-custody/config.env +++ b/service-runner/src/main/resources/profiles/default-custody/config.env @@ -1,61 +1,14 @@ -# secrets -secret.data.username=postgres -secret.data.password=password -secret.platform_api.auth_secret=myAnchorToPlatformSecret -secret.callback_api.auth_secret=myPlatformToAnchorSecret -secret.sep10.jwt_secret=secret_sep10_secret -secret.sep10.signing_seed=SAX3AH622R2XT6DXWWSRIDCMMUCCMATBZ5U6XKJWDO7M2EJUBFC3AW5X -secret.sep24.interactive_url.jwt_secret=secret_sep24_interactive_url_jwt_secret -secret.sep24.more_info_url.jwt_secret=secret_sep24_more_info_url_jwt_secret -secret.custody_server.auth_secret=myPlatformToCustodySecret secret.custody.fireblocks.api_key=testApiKey secret.custody.fireblocks.secret_key=MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJPq9AU6f8GrkSccloRb+UJmbxilZ7iLkvp0FeR/yymQuJDMNXPXwt5MYR9VJoY0uMDHThaEwbQAc2M4L9wl0EtsESebODUTqnsWZ8/a6GsmvIM3kz02ZOUHLst0krSkECPUHLLUKuw/HWz3LWxWLeqVRIcsCwKJNaBPZJzFw/8PAgMBAAECgYBhWXSYLFQApmW1k/8LxWxa4wei9Nk6f8GPy+7Mn76Z8IFH6t4TC6FYpHQXJvdfxDsDxSgDcgP574IBfu0gulJHEIsoDy1xR8TPobfhwoS1NJn70a8KQA6Whu+K5pjJxGUC7bpJH4ZK0O+szB1mNC0WgUywIc0lXCFFnCrgbNFWQQJBAM7+zgHAyD6rqQem09JcabMkwxu0mYih1mvQ3uv/fay8BdgZAqMuacMAPQ/A1TLeuTbMQ0LojnK/QlotbhHVnSkCQQC27684POaRrVIsVo5uKvQsKSNlYPpvGrGapHKiVgIQYfZSEY9SrazqKbA5yux0OR8ZFFSiJCThRElpzhnWFYl3AkEArNsLnVsn3W3sUX93FAwoGHlylQhTzk2XiaF7BwjsIftBxhvcn/h6SWVBmI4ne7uSX7hj0tPxYNFmz3dwm2QPQQJBAKS8mPSy2wtqoiotVBvfcHzoGujrePpednuFBXosq7UnEpN7Hq7cmW9RVVHl7CMJYXjLNx/AHroBLX8rS1bflCcCQBclpUG1PybVy1jHXTdI0w6zB6AwjaeFN5x4+b7hRe29yLNF532uIatxif19LHb5jUC7EefpLWBxx/bB4JCIyug= -# logging -app_logging.stellar_level=DEBUG -app_logging.request_logger_enabled=true -# events -events.enabled=true -events.queue.type=kafka -events.queue.kafka.bootstrap_server=kafka:29092 -# callback API endpoint -callback_api.base_url=http://reference-server:8091/ -# platform API endpoint -platform_api.base_url=http://platform:8085/ -custody_server.base_url=http://custody-server:8086 -# data -data.type=postgres -data.server=db:5432 -data.database=postgres -data.flyway_enabled=true -# assets -assets.type=file -assets.value=/config/assets.yaml + # seps -sep1.enabled=true -sep1.toml.type=file -sep1.toml.value=/config/stellar.localhost.toml -sep6.enabled=true -sep10.enabled=true -sep12.enabled=true -sep31.enabled=true -sep38.enabled=true -sep24.enabled=true -sep24.interactive_url.base_url=http://localhost:8091/sep24/interactive -sep24.more_info_url.base_url=http://localhost:8091/sep24/transaction/more_info sep6.deposit_info_generator_type=custody sep24.deposit_info_generator_type=custody sep31.deposit_info_generator_type=custody + +# custody custody.type=fireblocks custody.fireblocks.vault_account_id=1 custody.fireblocks.public_key=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCT6vQFOn/Bq5EnHJaEW/lCZm8YpWe4i5L6dBXkf8spkLiQzDVz18LeTGEfVSaGNLjAx04WhMG0AHNjOC/cJdBLbBEnmzg1E6p7FmfP2uhrJryDN5M9NmTlBy7LdJK0pBAj1Byy1CrsPx1s9y1sVi3qlUSHLAsCiTWgT2ScxcP/DwIDAQAB custody.fireblocks.asset_mappings=XLM_USDC_T_CEKS stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5\nXLM_TEST stellar:native custody.fireblocks.reconciliation.cron_expression=0/10 * * * * * -# clients -clients[0].name=reference -clients[0].type=noncustodial -clients[0].domain=wallet-server:8092 -clients[0].callback_url=http://wallet-server:8092/callbacks -clients[1].name=referenceCustodial -clients[1].type=custodial -clients[1].signing_key=GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG -clients[1].callback_url=http://wallet-server:8092/callbacks \ No newline at end of file diff --git a/service-runner/src/main/resources/profiles/default-custody/test.env b/service-runner/src/main/resources/profiles/default-custody/test.env index 50a2648dcd..1e6b699be2 100644 --- a/service-runner/src/main/resources/profiles/default-custody/test.env +++ b/service-runner/src/main/resources/profiles/default-custody/test.env @@ -1,15 +1 @@ -# Test configurations -anchor.domain=http://localhost:8080 -sep.server.url=http://localhost:8080 -reference.server.url=http://localhost:8091 -observer.server.url=http://localhost:8083 -platform.server.url=http://localhost:8085 -custody.server.url=http://localhost:8086 -wallet.server.url=http://localhost:8092 -run_docker=true -run_all_servers=true - -# Kotlin reference server configuration -app.custodyEnabled=true -wallet.hostname=wallet-server:8092 -sep24.interactiveJwtKey=secret_sep24_interactive_url_jwt_secret \ No newline at end of file +app.custodyEnabled=true \ No newline at end of file diff --git a/service-runner/src/main/resources/profiles/default-rpc/config.env b/service-runner/src/main/resources/profiles/default-rpc/config.env index 9efef89cf5..e69de29bb2 100644 --- a/service-runner/src/main/resources/profiles/default-rpc/config.env +++ b/service-runner/src/main/resources/profiles/default-rpc/config.env @@ -1,51 +0,0 @@ -# secrets -secret.data.username=postgres -secret.data.password=password -secret.platform_api.auth_secret=myAnchorToPlatformSecret -secret.callback_api.auth_secret=myPlatformToAnchorSecret -secret.sep10.jwt_secret=secret_sep10_secret -secret.sep10.signing_seed=SAX3AH622R2XT6DXWWSRIDCMMUCCMATBZ5U6XKJWDO7M2EJUBFC3AW5X -secret.sep24.interactive_url.jwt_secret=secret_sep24_interactive_url_jwt_secret -secret.sep24.more_info_url.jwt_secret=secret_sep24_more_info_url_jwt_secret -secret.custody_server.auth_secret=myPlatformToCustodySecret -# logging -app_logging.stellar_level=DEBUG -app_logging.request_logger_enabled=true -# events -events.enabled=true -events.queue.type=kafka -events.queue.kafka.bootstrap_server=kafka:29092 -# callback API endpoint -callback_api.base_url=http://reference-server:8091/ -# platform API endpoint -platform_api.base_url=http://platform:8085/ -custody_server.base_url=http://custody-server:8086 -# data -data.type=postgres -data.server=db:5432 -data.database=postgres -data.flyway_enabled=true -# assets -assets.type=file -assets.value=/config/assets.yaml -# seps -sep1.enabled=true -sep1.toml.type=file -sep1.toml.value=/config/stellar.localhost.toml -sep6.enabled=true -sep10.enabled=true -sep12.enabled=true -sep31.enabled=true -sep38.enabled=true -sep24.enabled=true -sep24.interactive_url.base_url=http://localhost:8091/sep24/interactive -sep24.more_info_url.base_url=http://localhost:8091/sep24/transaction/more_info -# clients -clients[0].name=reference -clients[0].type=noncustodial -clients[0].domain=wallet-server:8092 -clients[0].callback_url=http://wallet-server:8092/callbacks -clients[1].name=referenceCustodial -clients[1].type=custodial -clients[1].signing_key=GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG -clients[1].callback_url=http://wallet-server:8092/callbacks \ No newline at end of file diff --git a/service-runner/src/main/resources/profiles/default-rpc/test.env b/service-runner/src/main/resources/profiles/default-rpc/test.env index 406a8bc727..72c050d5d4 100644 --- a/service-runner/src/main/resources/profiles/default-rpc/test.env +++ b/service-runner/src/main/resources/profiles/default-rpc/test.env @@ -1,15 +1 @@ -# Test configurations -anchor.domain=http://localhost:8080 -sep.server.url=http://localhost:8080 -reference.server.url=http://localhost:8091 -observer.server.url=http://localhost:8083 -platform.server.url=http://localhost:8085 -custody.server.url=http://localhost:8086 -wallet.server.url=http://localhost:8092 -run_docker=true -run_all_servers=true - -# Kotlin reference server configuration app.rpcEnabled=true -wallet.hostname=wallet-server:8092 -sep24.interactiveJwtKey=secret_sep24_interactive_url_jwt_secret \ No newline at end of file diff --git a/service-runner/src/main/resources/profiles/default/config.env b/service-runner/src/main/resources/profiles/default/config.env index ebf4a9a296..81e2fa3c36 100644 --- a/service-runner/src/main/resources/profiles/default/config.env +++ b/service-runner/src/main/resources/profiles/default/config.env @@ -48,8 +48,6 @@ clients[0].callback_url=http://wallet-server:8092/callbacks clients[1].name=referenceCustodial clients[1].type=custodial clients[1].signing_key=GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG -clients[1].callback_url=http://wallet-server:8092/callbacks clients[2].name=stellar_anchor_tests clients[2].type=custodial -clients[2].signing_key=GDOHXZYP5ABGCTKAEROOJFN6X5GY7VQNXFNK2SHSAD32GSVMUJBPG75E -clients[2].callback_url=http://wallet-server:8092/callbacks \ No newline at end of file +clients[2].signing_key=GDOHXZYP5ABGCTKAEROOJFN6X5GY7VQNXFNK2SHSAD32GSVMUJBPG75E \ No newline at end of file diff --git a/service-runner/src/main/resources/profiles/default-custody-rpc/config.env b/service-runner/src/main/resources/profiles/unused-default-custody-rpc/config.env similarity index 100% rename from service-runner/src/main/resources/profiles/default-custody-rpc/config.env rename to service-runner/src/main/resources/profiles/unused-default-custody-rpc/config.env diff --git a/service-runner/src/main/resources/profiles/default-custody-rpc/test.env b/service-runner/src/main/resources/profiles/unused-default-custody-rpc/test.env similarity index 100% rename from service-runner/src/main/resources/profiles/default-custody-rpc/test.env rename to service-runner/src/main/resources/profiles/unused-default-custody-rpc/test.env diff --git a/settings.gradle.kts b/settings.gradle.kts index 1d01847068..c667bc85f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,7 @@ rootProject.name = "java-stellar-anchor-sdk" include("api-schema") /** Test libraries */ -include("test-lib") +include("lib-util") /** SDK */ include("core") @@ -14,10 +14,14 @@ include("platform") /** Reference Server */ include("kotlin-reference-server") + include("wallet-reference-server") -/** Integration tests */ -include("integration-tests") +/** Essential tests */ +include("essential-tests") + +/** Extended tests */ +include("extended-tests") /** Service runners */ include("service-runner") diff --git a/wallet-reference-server/src/main/kotlin/org/stellar/reference/wallet/WalletServerClient.kt b/wallet-reference-server/src/main/kotlin/org/stellar/reference/wallet/WalletServerClient.kt index 25e8d420d1..ed9c1c6e0d 100644 --- a/wallet-reference-server/src/main/kotlin/org/stellar/reference/wallet/WalletServerClient.kt +++ b/wallet-reference-server/src/main/kotlin/org/stellar/reference/wallet/WalletServerClient.kt @@ -53,8 +53,7 @@ class WalletServerClient(val endpoint: Url = Url("http://localhost:8092")) { var retries = 5 var callbacks: List = listOf() while (retries > 0) { - // TODO: remove when callbacks are de-duped - callbacks = getCallbacks(txnId, responseType).distinct() + callbacks = getCallbacks(txnId, responseType) if (callbacks.size >= expected) { return callbacks }