diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b3f43f2a1a..659e4ce1b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,13 @@ - `./gradlew test` - TODO: replace with any additional test steps +### Documentation + +TODO: If this pull request adds a feature, describe changes to documentation that has been made + +- Attach stellar-docs pull request, documenting new feature +- If it's an urgent feature request, please create a ticket and attach ticket number to this PR + ### Known limitations TODO: describe any limitations or replace with N/A diff --git a/.github/workflows/sub_gradle_test_and_build.yml b/.github/workflows/sub_gradle_test_and_build.yml index 8d855a748b..4b7fcb1349 100644 --- a/.github/workflows/sub_gradle_test_and_build.yml +++ b/.github/workflows/sub_gradle_test_and_build.yml @@ -111,6 +111,18 @@ jobs: run: | docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.7 --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 diff --git a/.github/workflows/wf_pull_request.yml b/.github/workflows/wf_pull_request.yml index a81488d377..c5bd6e8700 100644 --- a/.github/workflows/wf_pull_request.yml +++ b/.github/workflows/wf_pull_request.yml @@ -14,8 +14,8 @@ jobs: complete: if: always() - needs: [gradle_test_and_build] + needs: [ gradle_test_and_build ] runs-on: ubuntu-22.04 steps: - - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - run: exit 1 + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/api-schema/build.gradle.kts b/api-schema/build.gradle.kts index 01ce980aef..b162a0ad0e 100644 --- a/api-schema/build.gradle.kts +++ b/api-schema/build.gradle.kts @@ -14,25 +14,8 @@ dependencies { annotationProcessor(libs.lombok) } -tasks.test { - // Enable parallel test execution - systemProperty("junit.jupiter.execution.parallel.enabled", true) - // Allocate thread count based on available processors - systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic") - // Set default parallel mode to same thread. All tests within a class are run in sequence. - systemProperty("junit.jupiter.execution.parallel.mode.default", "same_thread") - // Set default parallel mode for classes to concurrent. All test classes are run in parallel. - systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") +apply(from = "$rootDir/scripts.gradle.kts") +@Suppress("UNCHECKED_CAST") +val enableTestConcurrency = extra["enableTestConcurrency"] as (Test) -> Unit - // Set default test class order to order annotation. All test classes are run in parallel. - // Some tests take longer to run. Enabling the order will execute long-running tests first to - // shorten the overall test time. - systemProperty( - "junit.jupiter.testclass.order.default", - "org.junit.jupiter.api.ClassOrderer\$OrderAnnotation" - ) - maxParallelForks = - (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also { - println("junit5 ... setting maxParallelForks to $it") - } -} +tasks.test { enableTestConcurrency(this) } 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 aee50a7979..831161534b 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 @@ -86,6 +86,9 @@ public class PlatformTransactionData { @SerializedName("refund_memo_type") String refundMemoType; + @SerializedName("withdraw_anchor_account") + String withdrawAnchorAccount; + Customers customers; StellarId creator; diff --git a/build.gradle.kts b/build.gradle.kts index 1a25322e4d..517a06f245 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -123,6 +123,10 @@ subprojects { test { useJUnitPlatform() + systemProperty( + "junit.jupiter.testclass.order.default", + "org.junit.jupiter.api.ClassOrderer\$OrderAnnotation" + ) exclude("**/AnchorPlatformCustodyEnd2EndTest**") exclude("**/AnchorPlatformCustodyApiRpcEnd2EndTest**") @@ -181,6 +185,4 @@ allprojects { } } -tasks.register("printVersionName") { - println(rootProject.version.toString()) -} \ No newline at end of file +tasks.register("printVersionName") { println(rootProject.version.toString()) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index dc26b99aa0..1b360130a9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation( libs.scala.library ) // used to force the version of scala-library (used by kafka-json-schema-serializer) to a safer - // one. + // one. implementation(libs.bundles.kafka) // TODO: Consider to simplify @@ -119,33 +119,9 @@ publishing { configure { sign(publishing.publications) } } -// TODO: when we enable parallelization for all sub-projects, we can extract the following block. -tasks.test { - // Enable parallel test execution - systemProperty("junit.jupiter.execution.parallel.enabled", true) - // Use PER_METHOD test instance life cycle. This avoids the race condition when tests are run in parallel mode - // if the test class has a non-static fields. The non-static fields are shared across all test methods. If the life - // cycle is not PER_METHOD, the test methods may overwrite the fields and cause test failures. - // - // However, the life cycle can still be over-written by @TestInstance(Lifecycle) annotation. - // See https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution - systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_method") - // Allocate thread count based on available processors - systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic") - // Set default parallel mode to same thread. All tests within a class are run in sequence. - systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") - // Set default parallel mode for classes to concurrent. All test classes are run in parallel. - systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") - - // Set default test class order to order annotation. All test classes are run in parallel. - // Some tests take longer to run. Enabling the order will execute long-running tests first to - // shorten the overall test time. - systemProperty( - "junit.jupiter.testclass.order.default", - "org.junit.jupiter.api.ClassOrderer\$OrderAnnotation" - ) - maxParallelForks = - (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also { - println("junit5 ... setting maxParallelForks to $it") - } -} +apply(from = "$rootDir/scripts.gradle.kts") + +@Suppress("UNCHECKED_CAST") +val enableTestConcurrency = extra["enableTestConcurrency"] as (Test) -> Unit + +tasks.test { enableTestConcurrency(this) } diff --git a/core/src/main/java/org/stellar/anchor/auth/JwtService.java b/core/src/main/java/org/stellar/anchor/auth/JwtService.java index ff2c5a31dc..a41665bb2f 100644 --- a/core/src/main/java/org/stellar/anchor/auth/JwtService.java +++ b/core/src/main/java/org/stellar/anchor/auth/JwtService.java @@ -1,10 +1,12 @@ package org.stellar.anchor.auth; +import static java.util.Date.*; + import io.jsonwebtoken.*; import io.jsonwebtoken.impl.DefaultJwsHeader; import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; -import java.util.Calendar; +import java.time.Instant; import java.util.Map; import lombok.Getter; import org.apache.commons.codec.binary.Base64; @@ -57,19 +59,15 @@ public JwtService( } public String encode(Sep10Jwt token) { - Calendar calIat = Calendar.getInstance(); - calIat.setTimeInMillis(1000L * token.getIat()); - - Calendar calExp = Calendar.getInstance(); - calExp.setTimeInMillis(1000L * token.getExp()); - + Instant timeExp = Instant.ofEpochSecond(token.getExp()); + Instant timeIat = Instant.ofEpochSecond(token.getIat()); JwtBuilder builder = Jwts.builder() .setId(token.getJti()) .setIssuer(token.getIss()) .setSubject(token.getSub()) - .setIssuedAt(calIat.getTime()) - .setExpiration(calExp.getTime()) + .setIssuedAt(from(timeIat)) + .setExpiration(from(timeExp)) .setSubject(token.getSub()); if (token.getClientDomain() != null) { @@ -88,12 +86,11 @@ public String encode(Sep24InteractiveUrlJwt token) throws InvalidConfigException throw new InvalidConfigException( "Please provide the secret before encoding JWT for Sep24 interactive url"); } - Calendar calExp = Calendar.getInstance(); - calExp.setTimeInMillis(1000L * token.getExp()); + Instant timeExp = Instant.ofEpochSecond(token.getExp()); JwtBuilder builder = Jwts.builder() .setId(token.getJti()) - .setExpiration(calExp.getTime()) + .setExpiration(from(timeExp)) .setSubject(token.getSub()); for (Map.Entry claim : token.claims.entrySet()) { builder.claim(claim.getKey(), claim.getValue()); @@ -107,12 +104,11 @@ public String encode(Sep24MoreInfoUrlJwt token) throws InvalidConfigException { throw new InvalidConfigException( "Please provide the secret before encoding JWT for more_info_url"); } - Calendar calExp = Calendar.getInstance(); - calExp.setTimeInMillis(1000L * token.getExp()); + Instant timeExp = Instant.ofEpochSecond(token.getExp()); JwtBuilder builder = Jwts.builder() .setId(token.getJti()) - .setExpiration(calExp.getTime()) + .setExpiration(from(timeExp)) .setSubject(token.getSub()); for (Map.Entry claim : token.claims.entrySet()) { builder.claim(claim.getKey(), claim.getValue()); @@ -139,11 +135,9 @@ private String encode(ApiAuthJwt token, String secret) throws InvalidConfigExcep "Please provide the secret before encoding JWT for API Authentication"); } - Calendar calNow = Calendar.getInstance(); - Calendar calExp = Calendar.getInstance(); - calExp.setTimeInMillis(1000L * token.getExp()); - JwtBuilder builder = - Jwts.builder().setIssuedAt(calNow.getTime()).setExpiration(calExp.getTime()); + Instant timeExp = Instant.ofEpochSecond(token.getExp()); + Instant timeIat = Instant.ofEpochSecond(token.getIat()); + JwtBuilder builder = Jwts.builder().setIssuedAt(from(timeIat)).setExpiration(from(timeExp)); return builder.signWith(SignatureAlgorithm.HS256, secret).compact(); } diff --git a/core/src/main/java/org/stellar/anchor/config/Sep10Config.java b/core/src/main/java/org/stellar/anchor/config/Sep10Config.java index fc46aca399..f69c6a980c 100644 --- a/core/src/main/java/org/stellar/anchor/config/Sep10Config.java +++ b/core/src/main/java/org/stellar/anchor/config/Sep10Config.java @@ -15,23 +15,20 @@ public interface Sep10Config { /** * The `web_auth_domain` property of SEP-10. - * If the `web_auth_domain` is not specified, the `web_auth_domain` will be set to the domain of - * the value of the `home_domain`. `web_auth_domain` value must be equal to the host of the SEP - * server. + * If the `web_auth_domain` is not specified, the `web_auth_domain` will be set to the first value + * of `home_domains`. The `web_auth_domain` value must equal to the host of the SEP server. * * @return the web auth domain. */ String getWebAuthDomain(); /** - * The `home_domain` property of SEP-10. - * `home_domain` value must be equal to the host of the toml file. If sep1 is enabled, toml file - * will be hosted on the SEP server. * - * @return the home domain. + * @return the list of home domains. */ - String getHomeDomain(); + List getHomeDomains(); /** * Set the authentication challenge transaction timeout in seconds. An expired signed transaction diff --git a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java index 562979077f..f6b06693c1 100644 --- a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java +++ b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java @@ -116,7 +116,7 @@ public String validateChallengeTransactionHomeDomain(ChallengeTransaction challe throw new SepValidationException("Invalid challenge transaction."); } - if (!Objects.equals(sep10Config.getHomeDomain(), homeDomain)) { + if ((!sep10Config.getHomeDomains().contains(homeDomain))) { throw new SepValidationException(format("Invalid home_domain. %s", homeDomain)); } @@ -257,10 +257,11 @@ String fetchSigningKeyFromClientDomain(String clientDomain) throws SepException void validateHomeDomain(ChallengeRequest request) throws SepValidationException { String homeDomain = request.getHomeDomain(); + String defaultHomeDomain = sep10Config.getHomeDomains().get(0); if (homeDomain == null) { - debugF("home_domain is not specified. Will use the default: {}", sep10Config.getHomeDomain()); - request.setHomeDomain(sep10Config.getHomeDomain()); - } else if (!homeDomain.equalsIgnoreCase(sep10Config.getHomeDomain())) { + debugF("home_domain is not specified. Will use the default: {}", defaultHomeDomain); + request.setHomeDomain(defaultHomeDomain); + } else if (!sep10Config.getHomeDomains().contains(homeDomain)) { infoF("Bad home_domain: {}", homeDomain); throw new SepValidationException(format("home_domain [%s] is not supported.", homeDomain)); } @@ -295,7 +296,7 @@ void validateChallengeRequest( request.getTransaction(), serverAccountId, new Network(appConfig.getStellarNetworkPassphrase()), - sep10Config.getHomeDomain(), + sep10Config.getHomeDomains().toArray(new String[0]), sep10Config.getWebAuthDomain(), threshold, signers); @@ -349,7 +350,7 @@ AccountResponse fetchAccount( request.getTransaction(), serverAccountId, new Network(appConfig.getStellarNetworkPassphrase()), - sep10Config.getHomeDomain(), + sep10Config.getHomeDomains().toArray(new String[0]), sep10Config.getWebAuthDomain(), signers); @@ -395,7 +396,7 @@ ChallengeTransaction parseChallenge(ValidationRequest request) transaction, serverAccountId, new Network(appConfig.getStellarNetworkPassphrase()), - sep10Config.getHomeDomain(), + sep10Config.getHomeDomains().toArray(new String[0]), sep10Config.getWebAuthDomain()); debugF( @@ -461,35 +462,35 @@ public synchronized ChallengeTransaction readChallengeTransaction( String challengeXdr, String serverAccountId, Network network, - String domainName, + String[] domainNames, String webAuthDomain) throws InvalidSep10ChallengeException, IOException { return Sep10Challenge.readChallengeTransaction( - challengeXdr, serverAccountId, network, domainName, webAuthDomain); + challengeXdr, serverAccountId, network, domainNames, webAuthDomain); } public synchronized void verifyChallengeTransactionSigners( String challengeXdr, String serverAccountId, Network network, - String domainName, + String[] domainNames, String webAuthDomain, Set signers) throws InvalidSep10ChallengeException, IOException { Sep10Challenge.verifyChallengeTransactionSigners( - challengeXdr, serverAccountId, network, domainName, webAuthDomain, signers); + challengeXdr, serverAccountId, network, domainNames, webAuthDomain, signers); } public synchronized void verifyChallengeTransactionThreshold( String challengeXdr, String serverAccountId, Network network, - String domainName, + String[] domainNames, String webAuthDomain, int threshold, Set signers) throws InvalidSep10ChallengeException, IOException { Sep10Challenge.verifyChallengeTransactionThreshold( - challengeXdr, serverAccountId, network, domainName, webAuthDomain, threshold, signers); + challengeXdr, serverAccountId, network, domainNames, webAuthDomain, threshold, signers); } } diff --git a/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt index 511bc3d07a..0431925c32 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt @@ -111,7 +111,7 @@ internal class Sep10ServiceTest { every { sep10Config.webAuthDomain } returns TEST_WEB_AUTH_DOMAIN every { sep10Config.authTimeout } returns 900 every { sep10Config.jwtTimeout } returns 900 - every { sep10Config.homeDomain } returns TEST_HOME_DOMAIN + every { sep10Config.homeDomains } returns listOf(TEST_HOME_DOMAIN) every { appConfig.stellarNetworkPassphrase } returns TESTNET.networkPassphrase diff --git a/docs/01 - Contributing/B - Git Guidelines.md b/docs/01 - Contributing/B - Git Guidelines.md index eaaf44c12d..a98bae84ec 100644 --- a/docs/01 - Contributing/B - Git Guidelines.md +++ b/docs/01 - Contributing/B - Git Guidelines.md @@ -47,3 +47,16 @@ Resolve: #123 We use the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) for this project. +## How to access GitHub workflow artifacts +The following artifacts are available for each workflow run: +- `/etc/hosts`: The hosts file of the runner. +- `/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. + +To access the artifacts, follow these steps: +1. Go to the `Actions` tab of the repository. +2. Click on the workflow run you want to access. +3. Find the `Artifacts` section on the bottom of the page. +4. Click to download. \ No newline at end of file diff --git a/integration-tests/build.gradle.kts b/integration-tests/build.gradle.kts index 12bf019503..f1d00ce0d9 100644 --- a/integration-tests/build.gradle.kts +++ b/integration-tests/build.gradle.kts @@ -14,7 +14,8 @@ dependencies { 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. + 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) 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 index 7145175ef8..22ae3f78eb 100644 --- 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 @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.test -import java.lang.Thread.sleep import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.assertThrows import org.stellar.anchor.api.exception.SepNotFoundException @@ -64,14 +63,11 @@ class Sep12Tests(config: TestConfig, toml: Sep1Helper.TomlContent, jwt: String) var pr = sep12Client.putCustomer(customer) printResponse(pr) - sleep(1000) - // make sure the customer was uploaded correctly. printRequest("Calling GET /customer", customer) var gr = sep12Client.getCustomer(pr!!.id) printResponse(gr) - assertEquals(Sep12Status.NEEDS_INFO, gr?.status) assertEquals(pr.id, gr?.id) customer.emailAddress = "john.doe@stellar.org" @@ -102,6 +98,7 @@ class Sep12Tests(config: TestConfig, toml: Sep1Helper.TomlContent, jwt: String) assertEquals("customer for 'id' '$id' not found", ex.message) println(ex) } + fun testAll() { println("Performing Sep12 tests...") `test put, get customers`() diff --git a/platform/build.gradle.kts b/platform/build.gradle.kts index bd1956ce2f..284b6f1e9c 100644 --- a/platform/build.gradle.kts +++ b/platform/build.gradle.kts @@ -55,29 +55,12 @@ dependencies { testImplementation(libs.okhttp3.tls) } -tasks.test { - // Enable parallel test execution - systemProperty("junit.jupiter.execution.parallel.enabled", true) - // Enable parallel test execution - systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_method") - // Allocate thread count based on available processors - systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic") - // Set default parallel mode to same thread. All tests within a class are run in sequence. - systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") - // Set default parallel mode for classes to concurrent. All test classes are run in parallel. - systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") +apply(from = "$rootDir/scripts.gradle.kts") +@Suppress("UNCHECKED_CAST") +val enableTestConcurrency = extra["enableTestConcurrency"] as (Test) -> Unit - // Set default test class order to order annotation. All test classes are run in parallel. - // Some tests take longer to run. Enabling the order will execute long-running tests first to - // shorten the overall test time. - systemProperty( - "junit.jupiter.testclass.order.default", - "org.junit.jupiter.api.ClassOrderer\$OrderAnnotation" - ) - maxParallelForks = - (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also { - println("junit5 ... setting maxParallelForks to $it") - } +tasks.test { + enableTestConcurrency(this) testLogging { exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL events = setOf(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED) diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java index 1486dc9522..bc5b4f6fb7 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java @@ -26,6 +26,7 @@ public class PropertySep10Config implements Sep10Config, Validator { private Boolean enabled; private String webAuthDomain; private String homeDomain; + private List homeDomains; private boolean clientAttributionRequired = false; private List clientAllowList = null; private Integer authTimeout = 900; @@ -50,8 +51,12 @@ public PropertySep10Config( @PostConstruct public void postConstruct() { - if (isEmpty(webAuthDomain)) { - webAuthDomain = homeDomain; + if (homeDomains == null || homeDomains.isEmpty()) { + homeDomains = List.of(homeDomain); + } + // If webAuthDomain is not specified and there is 1 and only 1 domain in the home_domains + if (isEmpty(webAuthDomain) && homeDomains.size() == 1) { + webAuthDomain = homeDomains.get(0); } } @@ -93,27 +98,22 @@ void validateConfig(Errors errors) { "Please set the secret.sep10.jwt_secret or SECRET_SEP10_JWT_SECRET environment variable"); } - if (isEmpty(homeDomain)) { + // Only one of homeDomain or homeDomains can be defined. + if (isEmpty(homeDomain) && (homeDomains == null || homeDomains.isEmpty())) { errors.rejectValue( "homeDomain", "home-domain-empty", "The sep10.home_domain is not defined."); + } else if (!isEmpty(homeDomain) && (homeDomains != null && !homeDomains.isEmpty())) { + errors.rejectValue( + "homeDomain", + "home-domain-coexist", + "home_domain and home_domains cannot coexist. Please choose one to use."); } else { - try { - new ManageDataOperation.Builder(String.format("%s %s", homeDomain, "auth"), new byte[64]) - .build(); - } catch (IllegalArgumentException iaex) { - errors.rejectValue( - "homeDomain", - "sep10-home-domain-too-long", - format( - "The sep10.home_domain (%s) is longer than the maximum length (64) of a domain. Error=%s", - homeDomain, iaex)); - } - - if (!NetUtil.isServerPortValid(homeDomain, false)) { - errors.rejectValue( - "homeDomain", - "sep10-home-domain-invalid", - "The sep10.home_domain does not have valid format."); + if (!isEmpty(homeDomain)) { + validateDomain(errors, homeDomain); + } else { + for (String domain : homeDomains) { + validateDomain(errors, domain); + } } } @@ -135,6 +135,11 @@ void validateConfig(Errors errors) { "sep10-web-auth-domain-invalid", "The sep10.web_auth_domain does not have valid format."); } + } else if (homeDomains != null && homeDomains.size() > 1) { + errors.rejectValue( + "webAuthDomain", + "sep10-web-auth-domain-empty", + "The sep10.web_auth_domain is required for multiple home domains."); } if (authTimeout <= 0) { @@ -191,6 +196,26 @@ void validateCustodialAccounts(Errors errors) { } } + private void validateDomain(Errors errors, String domain) { + try { + new ManageDataOperation.Builder(String.format("%s %s", domain, "auth"), new byte[64]).build(); + } catch (IllegalArgumentException iaex) { + errors.rejectValue( + "homeDomain", + "sep10-home-domain-too-long", + format( + "The sep10.home_domain (%s) is longer than the maximum length (64) of a domain. Error=%s", + domain, iaex)); + } + + if (!NetUtil.isServerPortValid(domain, false)) { + errors.rejectValue( + "homeDomain", + "sep10-home-domain-invalid", + "The sep10.home_domain does not have valid format."); + } + } + @Override public List getAllowedClientDomains() { // if clientAllowList is not defined, all client domains from the clients section are allowed. 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 8c5a3a56c4..2e8b0d1ffb 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 @@ -271,17 +271,6 @@ private GetTransactionResponse patchTransaction(PatchTransactionRequest patch) case "24": JdbcSep24Transaction sep24Txn = (JdbcSep24Transaction) txn; - // add a memo for the transaction if the transaction is ready for user to send funds - if (sep24Txn.getMemo() == null - && Kind.WITHDRAWAL.getKind().equals(sep24Txn.getKind()) - && sep24Txn.getStatus().equals(PENDING_USR_TRANSFER_START.toString())) { - SepDepositInfo sep24DepositInfo = sep24DepositInfoGenerator.generate(sep24Txn); - sep24Txn.setToAccount(sep24DepositInfo.getStellarAddress()); - sep24Txn.setWithdrawAnchorAccount(sep24DepositInfo.getStellarAddress()); - sep24Txn.setMemo(sep24DepositInfo.getMemo()); - sep24Txn.setMemoType(sep24DepositInfo.getMemoType()); - } - if (custodyConfig.isCustodyIntegrationEnabled() && !lastStatus.equals(sep24Txn.getStatus()) && ((Kind.DEPOSIT.getKind().equals(sep24Txn.getKind()) @@ -372,7 +361,17 @@ void updateSepTransaction(PlatformTransactionData patch, JdbcSepTransaction txn) txnUpdated = updateField(patch, sep24Txn, "memoType", txnUpdated); } + // add a memo for the transaction if the transaction is ready for user to send funds + if (sep24Txn.getMemo() == null + && Kind.WITHDRAWAL.getKind().equals(sep24Txn.getKind()) + && sep24Txn.getStatus().equals(PENDING_USR_TRANSFER_START.toString())) { + SepDepositInfo sep24DepositInfo = sep24DepositInfoGenerator.generate(sep24Txn); + sep24Txn.setMemo(sep24DepositInfo.getMemo()); + sep24Txn.setMemoType(sep24DepositInfo.getMemoType()); + } + txnUpdated = updateField(patch, sep24Txn, "message", txnUpdated); + txnUpdated = updateField(patch, sep24Txn, "withdrawAnchorAccount", txnUpdated); // update refunds if (patch.getRefunds() != null) { 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 21ecd61613..91e25f78aa 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -315,12 +315,23 @@ sep10: enabled: false # # The `web_auth_domain` property of SEP-10. https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#response - # If the `web_auth_domain` is not specified, the `web_auth_domain` will be set to the domain of the value of the `home_domain`. - # `web_auth_domain` value must equal to the host of the SEP server. + # The `web_auth_domain` is optional and will be set to the value of the `home_domain` or `home_domains` if + # 1) the `home_domain` is in use, + # 2) or the `home_domains` is in use and has only one value + # The `web_auth_domain` is required if + # 1) the `home_domains` is in use has more than one value web_auth_domain: # The `home_domain` property of SEP-10. https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#request # `home_domain` value must be equal to the host of the toml file. If sep1 is enabled, toml file will be hosted on the SEP server. + # This property cannot coexist with `home_domains` and is going to be deprecated. Please use `home_domains` instead. home_domain: localhost:8080 + # The `home_domains` property of SEP-10. This is a list of domains that the client can use to authenticate. + # https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#request + # This property cannot coexist with `home_domain`. Please also set web_auth_domain if you have more than one home domain in this list. + # The following lists are examples: + # Ex: home_domains: [ap.stellar.org, sdp.stellar.org] + # Ex: home_domains: ap.stellar.org, sdp.stellar.org + home_domains: # Set if the client attribution is required. Client Attribution requires clients to verify their identity by passing # a domain in the challenge transaction request and signing the challenge with the ``SIGNING_KEY`` on that domain's # SEP-1 stellar.toml. See the SEP-10 section `Verifying Client Application Identity` for more information diff --git a/platform/src/main/resources/config/anchor-config-schema-v1.yaml b/platform/src/main/resources/config/anchor-config-schema-v1.yaml index 9f515d72c7..136de05c91 100644 --- a/platform/src/main/resources/config/anchor-config-schema-v1.yaml +++ b/platform/src/main/resources/config/anchor-config-schema-v1.yaml @@ -74,6 +74,7 @@ sep10.client_allow_list: sep10.client_attribution_required: sep10.enabled: sep10.home_domain: +sep10.home_domains: sep10.jwt_timeout: sep10.known_custodial_account_required: sep10.web_auth_domain: diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt index 250cd3fa13..ba8d93531f 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt @@ -2,13 +2,12 @@ package org.stellar.anchor.platform.config import io.mockk.every import io.mockk.mockk +import java.util.stream.Stream import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.junit.jupiter.params.provider.NullSource -import org.junit.jupiter.params.provider.ValueSource +import org.junit.jupiter.params.provider.* import org.springframework.validation.BindException import org.springframework.validation.Errors import org.stellar.anchor.config.AppConfig @@ -217,4 +216,38 @@ class Sep10ConfigTest { config.postConstruct() assertEquals("localhost:8080", config.webAuthDomain) } + + @ParameterizedTest + @MethodSource("generatedHomeDomainsTestConfig") + fun `test web_auth_domain, home_domain and home_domains in valid config format`( + webAuthDomain: String?, + homeDomain: String?, + homeDomains: List?, + hasError: Boolean, + numberOfHomeDomains: Int + ) { + config.webAuthDomain = webAuthDomain + config.homeDomain = homeDomain + config.homeDomains = homeDomains + + config.validateConfig(errors) + assertEquals(hasError, errors.hasErrors()) + + if (!hasError) { + config.postConstruct() + assertEquals(numberOfHomeDomains, config.homeDomains.size) + } + } + + companion object { + @JvmStatic + fun generatedHomeDomainsTestConfig(): Stream { + return Stream.of( + Arguments.of(null, null, null, true, 0), + Arguments.of(null, "www.stellar.org", listOf("www.stellar.org", "www.losbstr.co"), true, 0), + Arguments.of(null, "www.stellar.org", emptyList(), false, 1), + Arguments.of("localhost:8080", "", listOf("www.stellar.org", "www.losbstr.co"), false, 2), + ) + } + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt index 4da4f0fefa..9242613349 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt @@ -4,12 +4,17 @@ import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import java.time.Instant +import java.util.Calendar import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import org.junit.jupiter.api.parallel.ExecutionMode.* import org.springframework.web.util.UriComponentsBuilder +import org.stellar.anchor.LockStatic import org.stellar.anchor.api.exception.SepValidationException import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.Sep24MoreInfoUrlJwt @@ -22,6 +27,7 @@ import org.stellar.anchor.platform.config.PropertySep24Config import org.stellar.anchor.platform.data.JdbcSep24Transaction import org.stellar.anchor.util.GsonUtils +@Execution(ExecutionMode.SAME_THREAD) class SimpleMoreInfoUrlConstructorTest { companion object { private val gson = GsonUtils.getInstance() @@ -71,6 +77,7 @@ class SimpleMoreInfoUrlConstructorTest { } @Test + @LockStatic([Calendar::class]) fun `test correct config`() { val config = gson.fromJson(SIMPLE_CONFIG_JSON, PropertySep24Config.MoreInfoUrlConfig::class.java) @@ -90,6 +97,7 @@ class SimpleMoreInfoUrlConstructorTest { } @Test + @LockStatic([Calendar::class]) fun `test unknown client domain`() { val config = gson.fromJson(SIMPLE_CONFIG_JSON, PropertySep24Config.MoreInfoUrlConfig::class.java) @@ -99,7 +107,6 @@ class SimpleMoreInfoUrlConstructorTest { txn.sep10AccountMemo = null val url = constructor.construct(txn) - val params = UriComponentsBuilder.fromUriString(url).build().queryParams val cipher = params["token"]!![0] @@ -112,6 +119,7 @@ class SimpleMoreInfoUrlConstructorTest { } @Test + @LockStatic([Calendar::class]) fun `test custodial wallet`() { val config = gson.fromJson(SIMPLE_CONFIG_JSON, PropertySep24Config.MoreInfoUrlConfig::class.java) @@ -134,6 +142,7 @@ class SimpleMoreInfoUrlConstructorTest { } @Test + @LockStatic([Calendar::class]) fun `test non-custodial wallet with missing client domain`() { val config = gson.fromJson(SIMPLE_CONFIG_JSON, PropertySep24Config.MoreInfoUrlConfig::class.java) diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt index fba0ef2835..aae5fae60d 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt @@ -44,7 +44,7 @@ class TransactionServiceTest { private const val fiatUSD = "iso4217:USD" private const val stellarUSDC = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" - private const val TEST_ACCOUNT = "GCHLHDBOKG2JWMJQBTLSL5XG6NO7ESXI2TAQKZXCXWXB5WI2X6W233PR" + private const val TEST_DEST_ACCOUNT = "testWithdrawAnchorAccount" private const val TEST_MEMO = "test memo" private const val TEST_TXN_ID = "a4baff5f-778c-43d6-bbef-3e9fb41d096e" private val gson = GsonUtils.getInstance() @@ -332,11 +332,13 @@ class TransactionServiceTest { val tx = JdbcSep24Transaction() tx.status = SepTransactionStatus.INCOMPLETE.toString() tx.kind = "withdrawal" + tx.withdrawAnchorAccount = null val data = PlatformTransactionData() data.id = txId data.memo = "12345" data.memoType = "id" data.status = SepTransactionStatus.PENDING_USR_TRANSFER_START + data.withdrawAnchorAccount = TEST_DEST_ACCOUNT val request = PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() @@ -350,6 +352,7 @@ class TransactionServiceTest { verify(exactly = 1) { custodyService.createTransaction(ofType(Sep24Transaction::class)) } verify(exactly = 1) { sep24TransactionStore.save(any()) } verify(exactly = 1) { eventSession.publish(any()) } + assertEquals(TEST_DEST_ACCOUNT, tx.withdrawAnchorAccount) } @Test diff --git a/scripts.gradle.kts b/scripts.gradle.kts new file mode 100644 index 0000000000..2f2607af52 --- /dev/null +++ b/scripts.gradle.kts @@ -0,0 +1,24 @@ +extra["enableTestConcurrency"] = + fun(test: Test) { + test.systemProperty("junit.jupiter.execution.parallel.enabled", true) + // Use PER_METHOD test instance life cycle. This avoids the race condition when tests are run in + // parallel mode and + // if the test class has a non-static fields. The non-static fields are shared across all test + // methods. If the life cycle is not PER_METHOD, the test methods may overwrite the fields and + // cause test failures. + // + // However, the life cycle can still be over-written by @TestInstance(Lifecycle) annotation. + // See https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution + test.systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_method") + // Allocate thread count based on available processors + test.systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic") + // Set default parallel mode to same thread. All tests within a class are run in sequence. + test.systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + // Set default parallel mode for classes to concurrent. All test classes are run in parallel. + test.systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") + + test.maxParallelForks = + (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also { + println("$test setting maxParallelForks to $it") + } + }