Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ANCHOR-295] Implement Kotlin reference server #1060

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .run/Run - Kotlin Reference Server - no Docker.run.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run - Kotlin Reference Server - no Docker" type="JetRunConfigurationType">
<envs>
<env name="TEST_PROFILE_NAME" value="sep24" />
<env name="TEST_PROFILE_NAME" value="default" />
<env name="sep24.enableTest" value="true" />
</envs>
<option name="MAIN_CLASS_NAME" value="org.stellar.anchor.platform.run_profiles.RunKotlinReferenceServer" />
Expand Down
2 changes: 1 addition & 1 deletion .run/Test - End2End Test - no fullstack.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<envs>
<env name="TEST_PROFILE_NAME" value="sep24" />
<env name="TEST_PROFILE_NAME" value="default" />
<env name="run_all_servers" value="false" />
<env name="run_docker" value="false" />
</envs>
Expand Down
2 changes: 1 addition & 1 deletion .run/Test - End2End Test - with fullstack.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<envs>
<env name="TEST_PROFILE_NAME" value="sep24" />
<env name="TEST_PROFILE_NAME" value="default" />
<env name="run_all_servers" value="true" />
<env name="run_docker" value="true" />
</envs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.stellar.anchor.api.sep.sep12.Sep12GetCustomerResponse;
import org.stellar.anchor.api.shared.CustomerField;
import org.stellar.anchor.api.shared.ProvidedCustomerField;
Expand All @@ -16,6 +19,9 @@
* API</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class GetCustomerResponse {
String id;
String status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.stellar.anchor.api.callback;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
@AllArgsConstructor
public class GetUniqueAddressRequest {
@SerializedName("transaction_id")
String transactionId;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.stellar.anchor.api.callback;

import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerResponse;

/**
Expand All @@ -12,6 +14,8 @@
* API</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PutCustomerResponse {
String id;

Expand Down
8 changes: 7 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ commons-validator = "1.7"
commons-text = "1.10.0"
docker-compose-rule = "1.9.0"
dotenv = "2.3.2"
exposed = "0.41.1"
findbugs-jsr305 = "3.0.2"
flyway-core = "8.5.13"
google-gson = "2.8.9"
Expand Down Expand Up @@ -76,6 +77,8 @@ commons-validator = { module = "commons-validator:commons-validator", version.re
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
docker-compose-rule = { module = "com.palantir.docker.compose:docker-compose-junit-jupiter", version.ref = "docker-compose-rule" }
dotenv = { module = "io.github.cdimascio:dotenv-java", version.ref = "dotenv" }
exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr305" }
flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway-core" }
google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" }
Expand Down Expand Up @@ -110,6 +113,8 @@ ktor-client-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
ktor-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" }
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
log4j2-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" }
log4j2-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
Expand Down Expand Up @@ -143,8 +148,9 @@ aws-java-sdk-s3 = { module = "com.amazonaws:aws-java-sdk-s3", version.ref = "aws
slf4j = ["slf4j-api", "slf4j-log4j12"]
junit = ["junit-api", "junit-engine", "junit-params", "junit-suite-engine", "kotlin-stdlib", "kotlin-junit", "mockk"]
kafka = ["kafka-clients", "kafka-connect", "kafka-json-schema"]
ktor = ["ktor-core", "ktor-content-negotiation", "ktor-netty", "ktor-yaml", "ktor-cors"]
ktor = ["ktor-core", "ktor-content-negotiation", "ktor-netty", "ktor-yaml", "ktor-cors", "ktor-server-auth", "ktor-server-auth-jwt"]
ktor-client = ["ktor-client-core", "ktor-client-content-negotiation", "ktor-client-json", "ktor-client-okhttp"]
exposed = ["exposed", "exposed-jdbc"]

[plugins]
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class Sep12Client(private val endpoint: String, private val jwt: String) : SepCl
* change to NEEDS_INFO if it's a receiving customer.
*/
fun invalidateCustomerClabe(id: String) {
val url = String.format("http://localhost:8081/invalidate_clabe/%s", id)
val url = String.format("http://localhost:8091/invalidate_clabe/%s", id)
httpGet(url, jwt)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ open class AbstractIntegrationTest(private val config: TestConfig) {
const val ANCHOR_TO_PLATFORM_SECRET = "myAnchorToPlatformSecret"
const val PLATFORM_TO_ANCHOR_SECRET = "myPlatformToAnchorSecret"
const val PLATFORM_SERVER_PORT = 8085
const val REFERENCE_SERVER_PORT = 8081
const val REFERENCE_SERVER_PORT = 8091
const val JWT_EXPIRATION_MILLISECONDS = 10000L
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package org.stellar.anchor.platform
import org.junit.jupiter.api.*

@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class AnchorPlatformEnd2EndTest : AbstractIntegrationTest(TestConfig(testProfileName = "sep24")) {
class AnchorPlatformEnd2EndTest : AbstractIntegrationTest(TestConfig(testProfileName = "default")) {

companion object {
private val singleton = AnchorPlatformEnd2EndTest()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,15 @@ internal class JwtAuthIntegrationTest : AbstractAuthIntegrationTest() {
TestProfileExecutor(
TestConfig(testProfileName = "default").also {
// enable platform server jwt auth
it.env["platform_server.auth.type"] = "jwt"
it.env["platform_server.auth.type"] = "JWT"
// enable business server callback auth
it.env["integration-auth.authType"] = "jwt"
it.env["integration-auth.platformToAnchorSecret"] = PLATFORM_TO_ANCHOR_SECRET
it.env["integration-auth.anchorToPlatformSecret"] = ANCHOR_TO_PLATFORM_SECRET
it.env["integration-auth.expirationMilliseconds"] =
JWT_EXPIRATION_MILLISECONDS.toString()
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()
testProfileRunner.start { it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true" }
}

@AfterAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.stellar.reference.client.AnchorReferenceServerClient
internal class KotlinReferenceServerIntegrationTest {
private val testProfileRunner =
TestProfileExecutor(
TestConfig("sep24").also {
TestConfig("default").also {
it.env[RUN_DOCKER] = "false"
it.env[RUN_ALL_SERVERS] = "false"
it.env[RUN_KOTLIN_REFERENCE_SERVER] = "true"
Expand Down
2 changes: 2 additions & 0 deletions kotlin-reference-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies {
implementation(libs.javax.jaxb.api)
implementation(libs.kotlin.logging)
implementation(libs.slf4j.simple)
implementation(libs.h2database)
implementation(libs.bundles.exposed)
implementation(project(mapOf("path" to ":api-schema")))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@ import com.sksamuel.hoplite.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import mu.KotlinLogging
import org.stellar.reference.data.Config
import org.stellar.reference.data.LocationConfig
import org.stellar.reference.event.EventService
import org.stellar.reference.plugins.event
import org.stellar.reference.plugins.sep24
import org.stellar.reference.plugins.testSep24
import org.stellar.reference.sep24.DepositService
import org.stellar.reference.sep24.Sep24Helper
import org.stellar.reference.sep24.WithdrawalService
import org.stellar.reference.plugins.*

val log = KotlinLogging.logger {}
lateinit var referenceKotlinSever: NettyApplicationEngine
Expand All @@ -35,14 +31,16 @@ fun startServer(envMap: Map<String, String>?, wait: Boolean) {

// start server
referenceKotlinSever =
embeddedServer(Netty, port = cfg.sep24.port) {
embeddedServer(Netty, port = cfg.appSettings.port) {
install(ContentNegotiation) { json() }
configureAuth(cfg)
configureRouting(cfg)
install(CORS) {
anyHost()
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
}
install(RequestLoggerPlugin)
}
.start(wait)
}
Expand Down Expand Up @@ -72,19 +70,3 @@ fun stopServer() {
if (::referenceKotlinSever.isInitialized) (referenceKotlinSever).stop(5000, 30000)
log.info("Kotlin reference server stopped...")
}

fun Application.configureRouting(cfg: Config) {
routing {
val helper = Sep24Helper(cfg)
val depositService = DepositService(cfg)
val withdrawalService = WithdrawalService(cfg)
val eventService = EventService()

sep24(helper, depositService, withdrawalService, cfg.sep24.interactiveJwtKey)
event(eventService)

if (cfg.sep24.enableTest) {
testSep24(helper, depositService, withdrawalService, cfg.sep24.interactiveJwtKey)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.stellar.reference.callbacks

import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
class BadRequestException(val error: String, @Transient override val cause: Throwable? = null) :
RuntimeException(error, cause)

@Serializable
class NotFoundException(
val error: String,
val id: String,
@Transient override val cause: Throwable? = null
) : RuntimeException(error, cause)

@Serializable
class UnprocessableEntityException(
val error: String,
val id: String,
@Transient override val cause: Throwable? = null
) : RuntimeException(error, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.stellar.reference.callbacks.customer

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.stellar.anchor.api.callback.GetCustomerRequest
import org.stellar.anchor.api.callback.PutCustomerRequest
import org.stellar.anchor.util.GsonUtils
import org.stellar.reference.callbacks.BadRequestException
import org.stellar.reference.callbacks.NotFoundException
import org.stellar.reference.log
import org.stellar.reference.plugins.AUTH_CONFIG_ENDPOINT

/**
* Defines the routes related to the customer callback API. See
* [Customer Callbacks](https://developers.stellar.org/api/anchor-platform/callbacks/customer/).
*
* @param customerService the [CustomerService] to use to process the requests.
*/
fun Route.customer(customerService: CustomerService) {
authenticate(AUTH_CONFIG_ENDPOINT) {
route("/customer") {
get {
val request =
GetCustomerRequest.builder()
.id(call.parameters["id"])
.account(call.parameters["account"])
.memo(call.parameters["memo"])
.memoType(call.parameters["memo_type"])
.type(call.parameters["type"])
.lang(call.parameters["lang"])
.build()
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a helper function for try/catch errors -> respond with status code?

val response = GsonUtils.getInstance().toJson(customerService.getCustomer(request))
call.respond(response)
} catch (e: BadRequestException) {
call.respond(HttpStatusCode.BadRequest, e)
} catch (e: NotFoundException) {
call.respond(HttpStatusCode.NotFound, e)
} catch (e: Exception) {
log.error("Unexpected exception", e)
call.respond(HttpStatusCode.InternalServerError)
}
}
put {
val request =
GsonUtils.getInstance().fromJson(call.receive<String>(), PutCustomerRequest::class.java)
try {
val response = GsonUtils.getInstance().toJson(customerService.upsertCustomer(request))
call.respond(response)
} catch (e: BadRequestException) {
call.respond(HttpStatusCode.BadRequest, e)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError)
}
}
delete("{id}") {
val id = call.parameters["id"]!!
try {
customerService.deleteCustomer(id)
call.respond(HttpStatusCode.NoContent)
} catch (e: NotFoundException) {
call.respond(HttpStatusCode.NotFound, e)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError)
}
}
}
route("/invalidate_clabe") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a config flag that marks it's only for test. You can do if (flag) route { }

get("{id}") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I'm assuming this is used to setup an integration test? Any chance we can make the endpoint available only when deployed in that context? We could do this later when we add a persistent deployment too, so I'll leave it to you.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a comment in the code to consider enabling this endpoint only when testing.

val id = call.parameters["id"]!!
try {
customerService.invalidateClabe(id)
call.respond(HttpStatusCode.OK)
} catch (e: NotFoundException) {
call.respond(HttpStatusCode.NotFound, e)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError)
}
}
}
}
}
Loading
Loading