Skip to content

Commit

Permalink
[ANCHOR-515] SEP-6: Remove KYC verification (#1177)
Browse files Browse the repository at this point in the history
### Description

This removes KYC verification from the SEP-6 transaction initiation. Now
that KYC is removed, the customer may not be known to the Anchor at the
time of transaction initiation so the `customer` field cannot be set. To
allow Anchors to associate SEP transactions with a customer, the SEP-10
account memo is now added as a field to the `StellarId` object.

This means for the exchange endpoints, the platform can no longer make a
request to the Fee integration as it requires setting SEP-12 sender and
receiver fields. Business servers are expected to update the amounts
asynchronously like they already do for regular fees.

### Context

KYC verification is done "interactively" in SEP-6.

### Testing

- `./gradlew test`

### Documentation

Stellar docs need to be updated so that the `StellarId` object includes
an optional `memo` field

### Known limitations

N/A
  • Loading branch information
philipliu authored Oct 26, 2023
1 parent ae043c7 commit 7e42b5c
Show file tree
Hide file tree
Showing 28 changed files with 282 additions and 454 deletions.
4 changes: 2 additions & 2 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.6.6 &
run: docker pull stellar/anchor-tests:v0.6.7 &

# Set up JDK 11
- name: Set up JDK 11
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

- name: Run Stellar validation tool
run: |
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
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
analyze:
name: CodeQL Analysis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
public class StellarId {
String id;
String account;
String memo;

public StellarId() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ public Sep31PostTransactionResponse postTransaction(
StellarId creatorStellarId =
StellarId.builder()
.account(Objects.requireNonNullElse(sep10Jwt.getMuxedAccount(), sep10Jwt.getAccount()))
.memo(sep10Jwt.getAccountMemo())
.build();

Amount fee = Context.get().getFee();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,23 @@
package org.stellar.anchor.sep6;

import static org.stellar.anchor.util.MathHelper.decimal;
import static org.stellar.anchor.util.MathHelper.formatAmount;
import static org.stellar.anchor.util.SepHelper.amountEquals;

import java.math.BigDecimal;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.stellar.anchor.api.callback.FeeIntegration;
import org.stellar.anchor.api.callback.GetFeeRequest;
import org.stellar.anchor.api.exception.AnchorException;
import org.stellar.anchor.api.exception.BadRequestException;
import org.stellar.anchor.api.exception.SepValidationException;
import org.stellar.anchor.api.sep.AssetInfo;
import org.stellar.anchor.api.sep.sep38.RateFee;
import org.stellar.anchor.api.shared.Amount;
import org.stellar.anchor.asset.AssetService;
import org.stellar.anchor.auth.Sep10Jwt;
import org.stellar.anchor.client.ClientFinder;
import org.stellar.anchor.sep38.Sep38Quote;
import org.stellar.anchor.sep38.Sep38QuoteStore;

/** Calculates the amounts for an exchange request. */
@RequiredArgsConstructor
public class ExchangeAmountsCalculator {
@NonNull private final ClientFinder clientFinder;
@NonNull private final FeeIntegration feeIntegration;
@NonNull private final Sep38QuoteStore sep38QuoteStore;
@NonNull private final AssetService assetService;

/**
* Calculates the amounts from a saved quote.
Expand All @@ -52,11 +40,11 @@ public Amounts calculateFromQuote(String quoteId, AssetInfo sellAsset, String se
"amount(%s) does not match quote sell amount(%s)",
sellAmount, quote.getSellAmount()));
}
if (!sellAsset.getCode().equals(quote.getSellAsset())) {
if (!sellAsset.getSep38AssetName().equals(quote.getSellAsset())) {
throw new BadRequestException(
String.format(
"source asset(%s) does not match quote sell asset(%s)",
sellAsset.getCode(), quote.getSellAsset()));
sellAsset.getSep38AssetName(), quote.getSellAsset()));
}
RateFee fee = quote.getFee();
if (fee == null) {
Expand All @@ -73,52 +61,6 @@ public Amounts calculateFromQuote(String quoteId, AssetInfo sellAsset, String se
.build();
}

/**
* Calculates the amounts for an exchange request by calling the Fee integration.
*
* @param buyAsset The asset the user is buying
* @param sellAsset The asset the user is selling
* @param amount The amount the user is selling
* @param customerId The customer ID
* @param sep10Jwt The SEP-10 JWT used to authenticate the request
* @return The amounts
* @throws AnchorException if the fee integration fails
*/
public Amounts calculate(
AssetInfo buyAsset, AssetInfo sellAsset, String amount, String customerId, Sep10Jwt sep10Jwt)
throws AnchorException {
String clientId = clientFinder.getClientId(sep10Jwt);
Amount fee =
feeIntegration
.getFee(
GetFeeRequest.builder()
.sendAmount(amount)
.sendAsset(sellAsset.getSep38AssetName())
.receiveAsset(buyAsset.getSep38AssetName())
.receiveAmount(null)
.senderId(customerId)
.receiverId(customerId)
.clientId(clientId)
.build())
.getFee();

AssetInfo feeAsset = assetService.getAssetByName(fee.getAsset());

BigDecimal requestedAmount = decimal(amount, sellAsset.getSignificantDecimals());
BigDecimal feeAmount = decimal(fee.getAmount(), feeAsset.getSignificantDecimals());

BigDecimal amountOut = requestedAmount.subtract(feeAmount);

return Amounts.builder()
.amountIn(formatAmount(requestedAmount, buyAsset.getSignificantDecimals()))
.amountInAsset(sellAsset.getSep38AssetName())
.amountOut(formatAmount(amountOut, sellAsset.getSignificantDecimals()))
.amountOutAsset(buyAsset.getSep38AssetName())
.amountFee(formatAmount(feeAmount, feeAsset.getSignificantDecimals()))
.amountFeeAsset(feeAsset.getSep38AssetName())
.build();
}

/** Amounts calculated for an exchange request. */
@Builder
@Data
Expand Down
47 changes: 0 additions & 47 deletions core/src/main/java/org/stellar/anchor/sep6/RequestValidator.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
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.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;
Expand All @@ -17,7 +13,6 @@
@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 @@ -114,46 +109,4 @@ public void validateAccount(String account) throws AnchorException {
throw new SepValidationException(String.format("invalid account %s", account));
}
}

/**
* Validates that the authenticated account has been KYC'ed by the anchor and returns its SEP-12
* customer ID.
*
* @param sep10Account the authenticated account
* @param sep10AccountMemo the authenticated account memo
* @throws AnchorException if the account has not been KYC'ed
* @return the SEP-12 customer ID if the account has been KYC'ed
*/
public String validateKyc(String sep10Account, String sep10AccountMemo) throws AnchorException {
GetCustomerRequest request =
sep10AccountMemo != null
? GetCustomerRequest.builder()
.account(sep10Account)
.memo(sep10AccountMemo)
.memoType("id")
.build()
: GetCustomerRequest.builder().account(sep10Account).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()));
}

return response.getId();
}
}
45 changes: 27 additions & 18 deletions core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.stellar.anchor.auth.Sep10Jwt;
import org.stellar.anchor.config.Sep6Config;
import org.stellar.anchor.event.EventService;
import org.stellar.anchor.sep6.ExchangeAmountsCalculator.Amounts;
import org.stellar.anchor.util.SepHelper;
import org.stellar.anchor.util.TransactionHelper;
import org.stellar.sdk.Memo;
Expand Down Expand Up @@ -76,7 +77,6 @@ public StartDepositResponse deposit(Sep10Jwt token, StartDepositRequest request)
asset.getDeposit().getMaxAmount());
}
requestValidator.validateAccount(request.getAccount());
String customerId = requestValidator.validateKyc(token.getAccount(), token.getAccountMemo());

Memo memo = makeMemo(request.getMemo(), request.getMemoType());
String id = SepHelper.generateSepTransactionId();
Expand All @@ -94,8 +94,7 @@ public StartDepositResponse deposit(Sep10Jwt token, StartDepositRequest request)
.startedAt(Instant.now())
.sep10Account(token.getAccount())
.sep10AccountMemo(token.getAccountMemo())
.toAccount(request.getAccount())
.customer(customerId);
.toAccount(request.getAccount());

if (memo != null) {
builder.memo(memo.toString());
Expand Down Expand Up @@ -144,17 +143,24 @@ public StartDepositResponse depositExchange(Sep10Jwt token, StartDepositExchange
buyAsset.getDeposit().getMinAmount(),
buyAsset.getDeposit().getMaxAmount());
requestValidator.validateAccount(request.getAccount());
String customerId = requestValidator.validateKyc(token.getAccount(), token.getAccountMemo());

ExchangeAmountsCalculator.Amounts amounts;
Amounts amounts;
if (request.getQuoteId() != null) {
amounts =
exchangeAmountsCalculator.calculateFromQuote(
request.getQuoteId(), sellAsset, request.getAmount());
} else {
// If a quote is not provided, set the fee and out amounts to 0.
// The business server should use the notify_amounts_updated RPC to update the amounts.
amounts =
exchangeAmountsCalculator.calculate(
buyAsset, sellAsset, request.getAmount(), customerId, token);
Amounts.builder()
.amountIn(request.getAmount())
.amountInAsset(sellAsset.getSep38AssetName())
.amountOut("0")
.amountOutAsset(buyAsset.getSep38AssetName())
.amountFee("0")
.amountFeeAsset(sellAsset.getSep38AssetName())
.build();
}

Memo memo = makeMemo(request.getMemo(), request.getMemoType());
Expand All @@ -181,8 +187,7 @@ public StartDepositResponse depositExchange(Sep10Jwt token, StartDepositExchange
.sep10Account(token.getAccount())
.sep10AccountMemo(token.getAccountMemo())
.toAccount(request.getAccount())
.quoteId(request.getQuoteId())
.customer(customerId);
.quoteId(request.getQuoteId());

if (memo != null) {
builder.memo(memo.toString());
Expand Down Expand Up @@ -231,7 +236,6 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque
}
String sourceAccount = request.getAccount() != null ? request.getAccount() : token.getAccount();
requestValidator.validateAccount(sourceAccount);
String customerId = requestValidator.validateKyc(token.getAccount(), token.getAccountMemo());

String id = SepHelper.generateSepTransactionId();

Expand All @@ -253,8 +257,7 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque
.fromAccount(sourceAccount)
.withdrawAnchorAccount(asset.getDistributionAccount())
.refundMemo(request.getRefundMemo())
.refundMemoType(request.getRefundMemoType())
.customer(customerId);
.refundMemoType(request.getRefundMemoType());

Sep6Transaction txn = builder.build();
txnStore.save(txn);
Expand Down Expand Up @@ -302,19 +305,26 @@ public StartWithdrawResponse withdrawExchange(
sellAsset.getWithdraw().getMaxAmount());
String sourceAccount = request.getAccount() != null ? request.getAccount() : token.getAccount();
requestValidator.validateAccount(sourceAccount);
String customerId = requestValidator.validateKyc(token.getAccount(), token.getAccountMemo());

String id = SepHelper.generateSepTransactionId();

ExchangeAmountsCalculator.Amounts amounts;
Amounts amounts;
if (request.getQuoteId() != null) {
amounts =
exchangeAmountsCalculator.calculateFromQuote(
request.getQuoteId(), sellAsset, request.getAmount());
} else {
// If a quote is not provided, set the fee and out amounts to 0.
// The business server should use the notify_amounts_updated RPC to update the amounts.
amounts =
exchangeAmountsCalculator.calculate(
buyAsset, sellAsset, request.getAmount(), customerId, token);
Amounts.builder()
.amountIn(request.getAmount())
.amountInAsset(sellAsset.getSep38AssetName())
.amountOut("0")
.amountOutAsset(buyAsset.getSep38AssetName())
.amountFee("0")
.amountFeeAsset(sellAsset.getSep38AssetName())
.build();
}

Sep6TransactionBuilder builder =
Expand Down Expand Up @@ -342,8 +352,7 @@ public StartWithdrawResponse withdrawExchange(
.withdrawAnchorAccount(sellAsset.getDistributionAccount())
.refundMemo(request.getRefundMemo())
.refundMemoType(request.getRefundMemoType())
.quoteId(request.getQuoteId())
.customer(customerId);
.quoteId(request.getQuoteId());

Sep6Transaction txn = builder.build();
txnStore.save(txn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,15 +362,6 @@ public interface Sep6Transaction extends SepTransaction {

void setInstructions(Map<String, InstructionField> instructions);

/**
* The SEP-12 customer ID of the user initiating the transaction.
*
* @return the customer ID.
*/
String getCustomer();

void setCustomer(String customer);

enum Kind {
DEPOSIT("deposit"),
WITHDRAWAL("withdrawal"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,6 @@ public Sep6TransactionBuilder instructions(Map<String, InstructionField> instruc
return this;
}

public Sep6TransactionBuilder customer(String customer) {
txn.setCustomer(customer);
return this;
}

public Sep6Transaction build() {
return txn;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static Sep6TransactionResponse fromTxn(Sep6Transaction txn) {
.requiredCustomerInfoUpdates(txn.getRequiredCustomerInfoUpdates())
.instructions(txn.getInstructions());

if (Sep6Transaction.Kind.valueOf(txn.getKind().toUpperCase()).isDeposit()) {
if (Sep6Transaction.Kind.valueOf(txn.getKind().toUpperCase().replace("-", "_")).isDeposit()) {
return builder.depositMemo(txn.getMemo()).depositMemoType(txn.getMemoType()).build();
} else {
return builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ public static GetTransactionResponse toGetTransactionResponse(
String amountOutAsset = makeAsset(txn.getAmountOutAsset(), assetService, txn);
String amountFeeAsset = makeAsset(txn.getAmountFeeAsset(), assetService, txn);
String amountExpectedAsset = makeAsset(null, assetService, txn);
StellarId customer = StellarId.builder().id(txn.getCustomer()).build();
StellarId customer =
StellarId.builder().account(txn.getSep10Account()).memo(txn.getSep10AccountMemo()).build();

return GetTransactionResponse.builder()
.id(txn.getId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,4 @@ public class PojoSep6Transaction implements Sep6Transaction {
String requiredCustomerInfoMessage;
List<String> requiredCustomerInfoUpdates;
Map<String, InstructionField> instructions;
String customer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,8 @@ class Sep31ServiceTest {
"receiverId":"137938d4-43a7-4252-a452-842adcee474c",
"senderId":"d2bd1412-e2f6-4047-ad70-a1a2f133b25c",
"creator": {
"account": "GBJDSMTMG4YBP27ZILV665XBISBBNRP62YB7WZA2IQX2HIPK7ABLF4C2"
"account": "GBJDSMTMG4YBP27ZILV665XBISBBNRP62YB7WZA2IQX2HIPK7ABLF4C2",
"memo": "123456"
}
}"""
.trimMargin()
Expand Down
Loading

0 comments on commit 7e42b5c

Please sign in to comment.