Skip to content

Commit

Permalink
Validate KYC before initiating transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
philipliu committed Oct 2, 2023
1 parent 8bea964 commit 786ffd2
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.stellar.anchor.api.exception;

import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/** Thrown when a customer's info is needed to complete a request. */
@RequiredArgsConstructor
@Getter
public class SepCustomerInfoNeededException extends AnchorException {
private final List<String> fields;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.stellar.anchor.api.sep;

import java.util.List;
import lombok.Data;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Data
public class CustomerInfoNeededResponse {
private final String type = "non_interactive_customer_info_needed";
private final List<String> fields;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ public enum Sep12Status {
PROCESSING("PROCESSING"),

@SerializedName("REJECTED")
REJECTED("REJECTED"),

@SerializedName("VERIFICATION_REQUIRED")
VERIFICATION_REQUIRED("VERIFICATION_REQUIRED");
REJECTED("REJECTED");

private final String name;

Expand Down
31 changes: 29 additions & 2 deletions core/src/main/java/org/stellar/anchor/sep6/RequestValidator.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package org.stellar.anchor.sep6;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.stellar.anchor.api.exception.SepValidationException;
import org.stellar.anchor.api.callback.CustomerIntegration;
import org.stellar.anchor.api.callback.GetCustomerRequest;
import org.stellar.anchor.api.callback.GetCustomerResponse;
import org.stellar.anchor.api.exception.*;
import org.stellar.anchor.api.sep.AssetInfo;
import org.stellar.anchor.asset.AssetService;
import org.stellar.sdk.KeyPair;
Expand All @@ -13,6 +17,7 @@
@RequiredArgsConstructor
public class RequestValidator {
@NonNull private final AssetService assetService;
@NonNull private final CustomerIntegration customerIntegration;

/**
* Validates that the requested asset is valid and enabled for deposit.
Expand Down Expand Up @@ -102,11 +107,33 @@ public void validateTypes(String requestType, String assetCode, List<String> val
* @param account the account
* @throws SepValidationException if the account is invalid
*/
public void validateAccount(String account) throws SepValidationException {
public void validateAccount(String account) throws AnchorException {
try {
KeyPair.fromAccountId(account);
} catch (RuntimeException ex) {
throw new SepValidationException(String.format("invalid account %s", account));
}

GetCustomerRequest request = GetCustomerRequest.builder().account(account).build();
GetCustomerResponse response = customerIntegration.getCustomer(request);

if (response == null || response.getStatus() == null) {
throw new ServerErrorException("unable to get required fields for customer") {};
}

switch (response.getStatus()) {
case "NEEDS_INFO":
throw new SepCustomerInfoNeededException(new ArrayList<>(response.getFields().keySet()));
case "PROCESSING":
throw new SepNotAuthorizedException("customer is being reviewed by anchor");
case "REJECTED":
throw new SepNotAuthorizedException("customer rejected by anchor");
case "ACCEPTED":
// do nothing
break;
default:
throw new ServerErrorException(
String.format("unknown customer status: %s", response.getStatus()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
package org.stellar.anchor.sep6

import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.*
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlin.test.assertEquals
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.ValueSource
import org.stellar.anchor.TestConstants.Companion.TEST_ACCOUNT
import org.stellar.anchor.TestConstants.Companion.TEST_ASSET
import org.stellar.anchor.api.callback.CustomerIntegration
import org.stellar.anchor.api.callback.GetCustomerRequest
import org.stellar.anchor.api.callback.GetCustomerResponse
import org.stellar.anchor.api.exception.SepCustomerInfoNeededException
import org.stellar.anchor.api.exception.SepNotAuthorizedException
import org.stellar.anchor.api.exception.SepValidationException
import org.stellar.anchor.api.exception.ServerErrorException
import org.stellar.anchor.api.sep.AssetInfo
import org.stellar.anchor.api.sep.sep12.Sep12Status
import org.stellar.anchor.api.shared.CustomerField
import org.stellar.anchor.asset.AssetService

class RequestValidatorTest {
@MockK(relaxed = true) lateinit var assetService: AssetService
@MockK(relaxed = true) lateinit var customerIntegration: CustomerIntegration

private lateinit var requestValidator: RequestValidator

@BeforeEach
fun setup() {
MockKAnnotations.init(this, relaxUnitFun = true)
requestValidator = RequestValidator(assetService)
requestValidator = RequestValidator(assetService, customerIntegration)
}

@Test
Expand Down Expand Up @@ -140,11 +148,63 @@ class RequestValidatorTest {

@Test
fun `test validateAccount`() {
every {
customerIntegration.getCustomer(GetCustomerRequest.builder().account(TEST_ACCOUNT).build())
} returns GetCustomerResponse.builder().status(Sep12Status.ACCEPTED.name).build()
requestValidator.validateAccount(TEST_ACCOUNT)
}

@Test
fun `test validateAccount with invalid account`() {
assertThrows<SepValidationException> { requestValidator.validateAccount("??") }

verify { customerIntegration wasNot called }
}

@Test
fun `test validateAccount customerIntegration failure`() {
every {
customerIntegration.getCustomer(GetCustomerRequest.builder().account(TEST_ACCOUNT).build())
} throws RuntimeException("test")
assertThrows<RuntimeException> { requestValidator.validateAccount(TEST_ACCOUNT) }
}

@Test
fun `test validateAccount with needs info customer`() {
every {
customerIntegration.getCustomer(GetCustomerRequest.builder().account(TEST_ACCOUNT).build())
} returns
GetCustomerResponse.builder()
.status(Sep12Status.NEEDS_INFO.name)
.fields(mapOf("first_name" to CustomerField.builder().build()))
.build()
val ex =
assertThrows<SepCustomerInfoNeededException> {
requestValidator.validateAccount(TEST_ACCOUNT)
}
assertEquals(listOf("first_name"), ex.fields)
}

@Test
fun `test validateAccount with processing customer`() {
every {
customerIntegration.getCustomer(GetCustomerRequest.builder().account(TEST_ACCOUNT).build())
} returns GetCustomerResponse.builder().status(Sep12Status.PROCESSING.name).build()
assertThrows<SepNotAuthorizedException> { requestValidator.validateAccount(TEST_ACCOUNT) }
}

@Test
fun `test validateAccount with rejected customer`() {
every {
customerIntegration.getCustomer(GetCustomerRequest.builder().account(TEST_ACCOUNT).build())
} returns GetCustomerResponse.builder().status(Sep12Status.REJECTED.name).build()
assertThrows<SepNotAuthorizedException> { requestValidator.validateAccount(TEST_ACCOUNT) }
}

@Test
fun `test validateAccount with unknown status customer`() {
every { customerIntegration.getCustomer(any()) } returns
GetCustomerResponse.builder().status("??").build()
assertThrows<ServerErrorException> { requestValidator.validateAccount(TEST_ACCOUNT) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ 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"))
anchor
.customer(token)
.add(mapOf("first_name" to "John", "last_name" to "Doe", "email_address" to "[email protected]"))

val deposit =
sep6Client.deposit(
Expand Down Expand Up @@ -98,7 +100,9 @@ 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"))
anchor
.customer(token)
.add(mapOf("first_name" to "John", "last_name" to "Doe", "email_address" to "[email protected]"))

val withdraw =
sep6Client.withdraw(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package org.stellar.anchor.platform.test

import org.skyscreamer.jsonassert.JSONAssert
import org.skyscreamer.jsonassert.JSONCompareMode
import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest
import org.stellar.anchor.platform.CLIENT_WALLET_ACCOUNT
import org.stellar.anchor.platform.Sep12Client
import org.stellar.anchor.platform.Sep6Client
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(toml.getString("TRANSFER_SERVER"), jwt)
private val sep6Client: Sep6Client
private val sep12Client: Sep12Client

init {
sep6Client = Sep6Client(toml.getString("TRANSFER_SERVER"), jwt)
sep12Client = Sep12Client(toml.getString("KYC_SERVER"), jwt)
// Create a customer before running any tests
putCustomer()
}

private val expectedSep6Info =
"""
Expand Down Expand Up @@ -150,6 +160,16 @@ class Sep6Tests(val toml: TomlContent, jwt: String) {
)
}

private fun putCustomer() {
val request =
Sep12PutCustomerRequest.builder()
.firstName("John")
.lastName("Doe")
.emailAddress("[email protected]")
.build()
sep12Client.putCustomer(request)
}

fun testAll() {
Log.info("Performing SEP6 tests")
`test Sep6 info endpoint`()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ Sep6Service sep6Service(
AssetService assetService,
Sep6TransactionStore txnStore,
EventService eventService,
CustomerIntegration customerIntegration,
FeeIntegration feeIntegration,
Sep38QuoteStore sep38QuoteStore) {
RequestValidator requestValidator = new RequestValidator(assetService);
RequestValidator requestValidator = new RequestValidator(assetService, customerIntegration);
ExchangeAmountsCalculator exchangeAmountsCalculator =
new ExchangeAmountsCalculator(feeIntegration, sep38QuoteStore, assetService);
return new Sep6Service(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.stellar.anchor.api.exception.*;
import org.stellar.anchor.api.sep.CustomerInfoNeededResponse;
import org.stellar.anchor.api.sep.SepExceptionResponse;

public abstract class AbstractControllerExceptionHandler {
Expand Down Expand Up @@ -41,6 +42,12 @@ public SepExceptionResponse handleAuthError(SepException ex) {
return new SepExceptionResponse(ex.getMessage());
}

@ExceptionHandler(SepCustomerInfoNeededException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN)
public CustomerInfoNeededResponse handle(SepCustomerInfoNeededException ex) {
return new CustomerInfoNeededResponse(ex.getFields());
}

@ExceptionHandler({SepNotFoundException.class, NotFoundException.class})
@ResponseStatus(value = HttpStatus.NOT_FOUND)
SepExceptionResponse handleNotFound(AnchorException ex) {
Expand Down

0 comments on commit 786ffd2

Please sign in to comment.