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-362] SEP-6: Update reference server to set required_customer_info_updates #1161

Merged
merged 4 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 2 additions & 3 deletions .github/workflows/sub_gradle_test_and_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

# Prepare Stellar Validation Tests
- name: Pull Stellar Validation Tests Docker Image
run: docker pull stellar/anchor-tests:v0.5.12 &
run: docker pull stellar/anchor-tests:v0.6.6 &

# Set up JDK 11
- name: Set up JDK 11
Expand Down Expand Up @@ -108,9 +108,8 @@ jobs:
interval: "1"

- name: Run Stellar validation tool
# TODO: re-enable SEP-24 tests once it passes (ANCHOR-487)
run: |
docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.1 --home-domain http://host.docker.internal:8080 --seps 1 6 10 12 31 38 --sep-config //config/stellar-anchor-tests-sep-config.json --verbose
docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.6 --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

analyze:
name: CodeQL Analysis
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.anchor.api.sep.sep12;

import com.google.gson.annotations.SerializedName;
import java.time.Instant;
import lombok.Builder;
import lombok.Data;

Expand Down Expand Up @@ -108,10 +109,10 @@ public class Sep12PutCustomerRequest implements Sep12CustomerRequestBase {
String idCountryCode;

@SerializedName("id_issue_date")
String idIssueDate;
Instant idIssueDate;

@SerializedName("id_expiration_date")
String idExpirationDate;
Instant idExpirationDate;

@SerializedName("id_number")
String idNumber;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
companion object {
private val USDC =
IssuedAssetId("USDC", "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP")
private val basicInfoFields = listOf("first_name", "last_name", "email_address")
private val customerInfo =
mapOf(
"first_name" to "John",
"last_name" to "Doe",
"address" to "123 Bay Street",
"email_address" to "[email protected]",
"id_type" to "drivers_license",
"id_country_code" to "CAN",
"id_issue_date" to "2023-01-01T05:00:00Z",
"id_expiration_date" to "2099-01-01T05:00:00Z",
"id_number" to "1234567890",
"bank_account_number" to "13719713158835300",
"bank_account_type" to "checking",
"bank_number" to "123",
"bank_branch_number" to "121122676"
)
}

private fun `test typical deposit end-to-end flow`() = runBlocking {
Expand All @@ -58,9 +75,7 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token.token)

// Create a customer before starting the transaction
anchor
.customer(token)
.add(mapOf("first_name" to "John", "last_name" to "Doe", "email_address" to "[email protected]"))
anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! })

val deposit =
sep6Client.deposit(
Expand All @@ -74,7 +89,9 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
waitStatus(deposit.id, "pending_customer_info_update", sep6Client)

// Supply missing KYC info to continue with the transaction
anchor.customer(token).add(mapOf("email_address" to "[email protected]"))
val additionalRequiredFields =
sep6Client.getTransaction(mapOf("id" to deposit.id)).transaction.requiredCustomerInfoUpdates
anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! })
waitStatus(deposit.id, "completed", sep6Client)

val completedDepositTxn = sep6Client.getTransaction(mapOf("id" to deposit.id))
Expand All @@ -99,8 +116,7 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
)
assertEquals(completedDepositTxn.transaction.id, transactionByStellarId.transaction.id)

val expectedStatuses =
listOf("incomplete", "pending_anchor", "pending_customer_info_update", "completed")
val expectedStatuses = listOf("incomplete", "pending_customer_info_update", "completed")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Can we use the enum here?

assertAnchorReceivedStatuses(deposit.id, expectedStatuses)
assertWalletReceivedStatuses(deposit.id, expectedStatuses)
}
Expand All @@ -111,9 +127,7 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token.token)

// Create a customer before starting the transaction
anchor
.customer(token)
.add(mapOf("first_name" to "John", "last_name" to "Doe", "email_address" to "[email protected]"))
anchor.customer(token).add(basicInfoFields.associateWith { customerInfo[it]!! })

val withdraw =
sep6Client.withdraw(
Expand All @@ -122,15 +136,9 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
waitStatus(withdraw.id, "pending_customer_info_update", sep6Client)

// Supply missing financial account info to continue with the transaction
anchor
.customer(token)
.add(
mapOf(
"bank_account_type" to "checking",
"bank_account_number" to "121122676",
"bank_number" to "13719713158835300",
)
)
val additionalRequiredFields =
sep6Client.getTransaction(mapOf("id" to withdraw.id)).transaction.requiredCustomerInfoUpdates
anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! })
waitStatus(withdraw.id, "pending_user_transfer_start", sep6Client)

// Transfer the withdrawal amount to the Anchor
Expand All @@ -142,7 +150,6 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) {
.build()
transfer.sign(keypair)
wallet.stellar().submitTransaction(transfer)

waitStatus(withdraw.id, "completed", sep6Client)

val expectedStatuses =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,18 @@ class CustomerService(private val customerRepository: CustomerRepository) {
customer.copy(
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 Is there anything like BeanUtils in Kotlin?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or use gson.toJson and gson.fromJson for copying?

I think this is for the reference and does not require critical performance but rather explanations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wrote it this way since I don't want null request fields to delete a previously set customer field. By default, BeanUtils#copyProperties does not ignore null fields, we would need to write a wrapper around it if we want this behavior. This is the same reason, I did not use gson here.

firstName = request.firstName ?: customer.firstName,
lastName = request.lastName ?: customer.lastName,
address = request.address ?: customer.address,
emailAddress = request.emailAddress ?: customer.emailAddress,
bankAccountNumber = request.bankAccountNumber ?: customer.bankAccountNumber,
bankAccountType = request.bankAccountType ?: customer.bankAccountType,
bankRoutingNumber = request.bankNumber ?: customer.bankRoutingNumber,
clabeNumber = request.clabeNumber ?: customer.clabeNumber
bankNumber = request.bankNumber ?: customer.bankNumber,
bankBranchNumber = request.bankBranchNumber ?: customer.bankBranchNumber,
clabeNumber = request.clabeNumber ?: customer.clabeNumber,
idType = request.idType ?: customer.idType,
idCountryCode = request.idCountryCode ?: customer.idCountryCode,
idIssueDate = request.idIssueDate ?: customer.idIssueDate,
idExpirationDate = request.idExpirationDate ?: customer.idExpirationDate,
idNumber = request.idNumber ?: customer.idNumber,
)
)
return PutCustomerResponse(customer.id)
Expand All @@ -73,11 +80,18 @@ class CustomerService(private val customerRepository: CustomerRepository) {
memoType = request.memoType,
firstName = request.firstName,
lastName = request.lastName,
address = request.address,
emailAddress = request.emailAddress,
bankAccountNumber = request.bankAccountNumber,
bankAccountType = request.bankAccountType,
bankRoutingNumber = request.bankNumber,
clabeNumber = request.clabeNumber
bankNumber = request.bankNumber,
bankBranchNumber = request.bankBranchNumber,
clabeNumber = request.clabeNumber,
idType = request.idType,
idCountryCode = request.idCountryCode,
idIssueDate = request.idIssueDate,
idExpirationDate = request.idExpirationDate,
idNumber = request.idNumber,
)
)
return PutCustomerResponse(id)
Expand All @@ -100,33 +114,82 @@ class CustomerService(private val customerRepository: CustomerRepository) {
val providedFields = mutableMapOf<String, ProvidedCustomerField>()
val missingFields = mutableMapOf<String, CustomerField>()

val commonFields =
val fields =
mapOf(
"first_name" to createField(customer.firstName, "string", "The customer's first name"),
"last_name" to createField(customer.lastName, "string", "The customer's last name"),
"address" to
createField(customer.address, "string", "The customer's address", optional = true),
"email_address" to
createField(customer.emailAddress, "string", "The customer's email address"),
)
val sep31ReceiverFields =
mapOf(
"bank_account_number" to
createField(customer.bankAccountNumber, "string", "The customer's bank account number"),
createField(
customer.bankAccountNumber,
"string",
"The customer's bank account number",
optional = type != "sep31-receiver"
),
"bank_account_type" to
createField(
customer.bankAccountType,
"string",
"The customer's bank account type",
choices = listOf("checking", "savings")
choices = listOf("checking", "savings"),
optional = type != "sep31-receiver"
),
"bank_number" to
createField(customer.bankRoutingNumber, "string", "The customer's bank routing number"),
"clabe_number" to createField(customer.clabeNumber, "string", "The customer's CLABE number")
createField(
customer.bankNumber,
"string",
"The customer's bank routing number",
optional = type != "sep31-receiver"
),
"bank_branch_number" to
createField(
customer.bankBranchNumber,
"string",
"The customer's bank branch number",
optional = true
),
"clabe_number" to
createField(
customer.clabeNumber,
"string",
"The customer's CLABE number",
optional = type != "sep31-receiver"
),
"id_type" to
createField(
customer.idType,
"string",
"The customer's ID type",
optional = true,
choices = listOf("drivers_license", "passport", "national_id")
),
"id_country_code" to
createField(
customer.idCountryCode,
"string",
"The customer's ID country code",
optional = true
),
"id_issue_date" to
createField(
customer.idIssueDate,
"string",
"The customer's ID issue date",
optional = true
),
"id_expiration_date" to
createField(
customer.idExpirationDate,
"string",
"The customer's ID expiration date",
optional = true
),
"id_number" to
createField(customer.idNumber, "string", "The customer's ID number", optional = true)
)
val fields =
when (type) {
"sep31-receiver" -> commonFields.plus(sep31ReceiverFields)
else -> commonFields
}

// Extract fields from customer
fields.forEach(
Expand All @@ -140,7 +203,7 @@ class CustomerService(private val customerRepository: CustomerRepository) {

val status =
when {
missingFields.isNotEmpty() -> Status.NEEDS_INFO
missingFields.filter { !it.value.optional }.isNotEmpty() -> Status.NEEDS_INFO
else -> Status.ACCEPTED
}.toString()

Expand All @@ -154,8 +217,10 @@ class CustomerService(private val customerRepository: CustomerRepository) {

sealed class Field {
class Provided(val field: ProvidedCustomerField) : Field()

class Missing(val field: CustomerField) : Field()
}

private fun createField(
value: Any?,
type: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.stellar.reference.dao

import java.time.Instant
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
Expand All @@ -8,9 +9,13 @@ import org.stellar.reference.model.Customer

interface CustomerRepository {
fun get(id: String): Customer?

fun get(stellarAccount: String, memo: String?, memoType: String?): Customer?

fun create(customer: Customer): String?

fun update(customer: Customer)

fun delete(id: String)
}

Expand All @@ -29,12 +34,20 @@ class JdbcCustomerRepository(private val db: Database) : CustomerRepository {
memo = it[Customers.memo],
memoType = it[Customers.memoType],
firstName = it[Customers.firstName],
address = it[Customers.address],
lastName = it[Customers.lastName],
emailAddress = it[Customers.emailAddress],
bankAccountNumber = it[Customers.bankAccountNumber],
bankAccountType = it[Customers.bankAccountType],
bankRoutingNumber = it[Customers.bankRoutingNumber],
clabeNumber = it[Customers.clabeNumber]
bankNumber = it[Customers.bankNumber],
bankBranchNumber = it[Customers.bankBranchNumber],
clabeNumber = it[Customers.clabeNumber],
idType = it[Customers.idType],
idCountryCode = it[Customers.idCountryCode],
idIssueDate = it[Customers.idIssueDate]?.let { ts -> Instant.ofEpochMilli(ts) },
idExpirationDate =
it[Customers.idExpirationDate]?.let { ts -> Instant.ofEpochMilli(ts) },
idNumber = it[Customers.idNumber]
)
}
}
Expand Down Expand Up @@ -64,11 +77,19 @@ class JdbcCustomerRepository(private val db: Database) : CustomerRepository {
memoType = it[Customers.memoType],
firstName = it[Customers.firstName],
lastName = it[Customers.lastName],
address = it[Customers.address],
emailAddress = it[Customers.emailAddress],
bankAccountNumber = it[Customers.bankAccountNumber],
bankAccountType = it[Customers.bankAccountType],
bankRoutingNumber = it[Customers.bankRoutingNumber],
clabeNumber = it[Customers.clabeNumber]
bankNumber = it[Customers.bankNumber],
bankBranchNumber = it[Customers.bankBranchNumber],
clabeNumber = it[Customers.clabeNumber],
idType = it[Customers.idType],
idCountryCode = it[Customers.idCountryCode],
idIssueDate = it[Customers.idIssueDate]?.let { ts -> Instant.ofEpochMilli(ts) },
idExpirationDate =
it[Customers.idExpirationDate]?.let { ts -> Instant.ofEpochMilli(ts) },
idNumber = it[Customers.idNumber]
)
}
}
Expand All @@ -84,11 +105,18 @@ class JdbcCustomerRepository(private val db: Database) : CustomerRepository {
it[memoType] = customer.memoType
it[firstName] = customer.firstName
it[lastName] = customer.lastName
it[address] = customer.address
it[emailAddress] = customer.emailAddress
it[bankAccountNumber] = customer.bankAccountNumber
it[bankAccountType] = customer.bankAccountType
it[bankRoutingNumber] = customer.bankRoutingNumber
it[bankNumber] = customer.bankNumber
it[bankAccountNumber] = customer.bankAccountNumber
it[clabeNumber] = customer.clabeNumber
it[idType] = customer.idType
it[idCountryCode] = customer.idCountryCode
it[idIssueDate] = customer.idIssueDate?.toEpochMilli()
it[idExpirationDate] = customer.idExpirationDate?.toEpochMilli()
it[idNumber] = customer.idNumber
}
}
.resultedValues
Expand All @@ -103,11 +131,18 @@ class JdbcCustomerRepository(private val db: Database) : CustomerRepository {
it[memoType] = customer.memoType
it[firstName] = customer.firstName
it[lastName] = customer.lastName
it[address] = customer.address
it[emailAddress] = customer.emailAddress
it[bankAccountNumber] = customer.bankAccountNumber
it[bankAccountType] = customer.bankAccountType
it[bankRoutingNumber] = customer.bankRoutingNumber
it[bankNumber] = customer.bankNumber
it[bankBranchNumber] = customer.bankBranchNumber
it[clabeNumber] = customer.clabeNumber
it[idType] = customer.idType
it[idCountryCode] = customer.idCountryCode
it[idIssueDate] = customer.idIssueDate?.toEpochMilli()
it[idExpirationDate] = customer.idExpirationDate?.toEpochMilli()
it[idNumber] = customer.idNumber
}
}

Expand Down
Loading
Loading