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-353] SEP-6: Verify SEP-12 status before deposit/withdraw proceeds #1136

Merged
merged 5 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import org.stellar.anchor.api.event.AnchorEvent;
import org.stellar.anchor.api.platform.CustomerUpdatedResponse;
import org.stellar.anchor.api.platform.GetQuoteResponse;
import org.stellar.anchor.api.platform.GetTransactionResponse;

Expand All @@ -13,6 +14,7 @@
public class SendEventRequestPayload {
GetTransactionResponse transaction;
GetQuoteResponse quote;
CustomerUpdatedResponse customer;

/**
* Creates a SendEventRequestPayload from an AnchorEvent.
Expand All @@ -23,6 +25,8 @@ public class SendEventRequestPayload {
public static SendEventRequestPayload from(AnchorEvent event) {
SendEventRequestPayload payload = new SendEventRequestPayload();
switch (event.getType()) {
case CUSTOMER_UPDATED:
payload.setCustomer(event.getCustomer());
case QUOTE_CREATED:
payload.setQuote(event.getQuote());
case TRANSACTION_CREATED:
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class StartWithdrawExchangeRequest {
@SerializedName("quote_id")
String quoteId;

/** The account to withdraw from. */
String account;

/** The amount of the source asset the user would like to withdraw. */
@NonNull String amount;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class StartWithdrawRequest {
/** Type of withdrawal. */
String type;

/** The account to withdraw from. */
String account;

/** The amount to withdraw. */
String amount;

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()));
}
}
}
8 changes: 6 additions & 2 deletions core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque
asset.getWithdraw().getMinAmount(),
asset.getWithdraw().getMaxAmount());
}
String sourceAccount = request.getAccount() != null ? request.getAccount() : token.getAccount();
requestValidator.validateAccount(sourceAccount);

String id = SepHelper.generateSepTransactionId();

Expand All @@ -245,7 +247,7 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque
.sep10AccountMemo(token.getAccountMemo())
.memo(generateMemo(id))
.memoType(memoTypeAsString(MEMO_HASH))
.fromAccount(token.getAccount())
.fromAccount(sourceAccount)
.withdrawAnchorAccount(asset.getDistributionAccount())
.toAccount(asset.getDistributionAccount())
.refundMemo(request.getRefundMemo())
Expand Down Expand Up @@ -295,6 +297,8 @@ public StartWithdrawResponse withdrawExchange(
sellAsset.getSignificantDecimals(),
sellAsset.getWithdraw().getMinAmount(),
sellAsset.getWithdraw().getMaxAmount());
String sourceAccount = request.getAccount() != null ? request.getAccount() : token.getAccount();
requestValidator.validateAccount(sourceAccount);

String id = SepHelper.generateSepTransactionId();

Expand Down Expand Up @@ -330,7 +334,7 @@ public StartWithdrawResponse withdrawExchange(
.sep10AccountMemo(token.getAccountMemo())
.memo(generateMemo(id))
.memoType(memoTypeAsString(MEMO_HASH))
.fromAccount(token.getAccount())
.fromAccount(sourceAccount)
.withdrawAnchorAccount(sellAsset.getDistributionAccount())
.refundMemo(request.getRefundMemo())
.refundMemoType(request.getRefundMemoType())
Expand Down
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) }
}
}
Loading