Skip to content

Commit

Permalink
[ANCHOR-809][SEP-31] Add funding_method to SEP-31 (#1581)
Browse files Browse the repository at this point in the history
### Description

- Add `funding_methods` to `/info` response. This field is mandatory for
the anchor.
- Add `funding_method` to `POST /transaction` param. This field is
mandatory for wallet.

### Context

SEP-31 changes scoped in
stellar/stellar-protocol#1567

### Testing

- `./gradlew test`

### Next
The SEP-6 change will be handled in a separate PR.
  • Loading branch information
JiahuiWho authored Nov 21, 2024
1 parent d2eac28 commit 6eeb100
Show file tree
Hide file tree
Showing 30 changed files with 293 additions and 203 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.anchor.api.asset;

import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
import lombok.Data;

Expand All @@ -23,6 +24,8 @@ public static class ReceiveOperation {

@SerializedName("max_amount")
Long maxAmount;

List<String> methods;
}

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.anchor.api.sep.sep31;

import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
import lombok.Data;

Expand Down Expand Up @@ -30,5 +31,8 @@ public static class AssetResponse {

@SerializedName("max_amount")
Long maxAmount;

@SerializedName("funding_methods")
List<String> fundingMethods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ public class Sep31PostTransactionRequest {
@SerializedName("receiver_id")
String receiverId;

Sep31TxnFields fields;
@Deprecated Sep31TxnFields fields;

String lang;

@SerializedName("funding_method")
String fundingMethod;

@Data
@AllArgsConstructor
public static class Sep31TxnFields {
Expand Down
12 changes: 8 additions & 4 deletions core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
import static org.stellar.anchor.util.MathHelper.formatAmount;
import static org.stellar.anchor.util.MetricConstants.SEP31_TRANSACTION_CREATED;
import static org.stellar.anchor.util.MetricConstants.SEP31_TRANSACTION_PATCHED;
import static org.stellar.anchor.util.SepHelper.amountEquals;
import static org.stellar.anchor.util.SepHelper.generateSepTransactionId;
import static org.stellar.anchor.util.SepHelper.validateAmount;
import static org.stellar.anchor.util.SepHelper.validateAmountLimit;
import static org.stellar.anchor.util.SepHelper.*;
import static org.stellar.anchor.util.SepLanguageHelper.validateLanguage;

import io.micrometer.core.instrument.Counter;
Expand Down Expand Up @@ -126,6 +123,10 @@ public Sep31PostTransactionResponse postTransaction(
request.getAmount(),
assetInfo.getSep31().getReceive().getMinAmount(),
assetInfo.getSep31().getReceive().getMaxAmount());
validateFundingMethod(
assetInfo.getId(),
request.getFundingMethod(),
assetInfo.getSep31().getReceive().getMethods());
validateLanguage(appConfig, request.getLang());

/*
Expand Down Expand Up @@ -173,6 +174,7 @@ public Sep31PostTransactionResponse postTransaction(
new Sep31TransactionBuilder(sep31TransactionStore)
.id(generateSepTransactionId())
.status(SepTransactionStatus.PENDING_RECEIVER.getStatus())
.fundingMethod(request.getFundingMethod())
.statusEta(null)
.feeDetails(feeDetails)
.startedAt(now)
Expand Down Expand Up @@ -570,11 +572,13 @@ private static Sep31InfoResponse sep31InfoResponseFromAssetInfoList(List<AssetIn
if (assetInfo.getSep31() != null && assetInfo.getSep31().getEnabled()) {
boolean isQuotesSupported = assetInfo.getSep31().isQuotesSupported();
boolean isQuotesRequired = assetInfo.getSep31().isQuotesRequired();
List<String> methods = assetInfo.getSep31().getReceive().getMethods();
AssetResponse assetResponse = new AssetResponse();
assetResponse.setQuotesSupported(isQuotesSupported);
assetResponse.setQuotesRequired(isQuotesRequired);
assetResponse.setMinAmount(assetInfo.getSep31().getReceive().getMinAmount());
assetResponse.setMaxAmount(assetInfo.getSep31().getReceive().getMaxAmount());
assetResponse.setFundingMethods(methods);
response.getReceive().put(assetInfo.getCode(), assetResponse);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ public interface Sep31Transaction extends SepTransaction {

void setCreator(StellarId creator);

String getFundingMethod();

void setFundingMethod(String fundingMethod);

default Customers getCustomers() {
return new Customers(
StellarId.builder().id(getSenderId()).build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ public Sep31TransactionBuilder creator(StellarId creator) {
return this;
}

public Sep31TransactionBuilder fundingMethod(String fundingMethod) {
txn.setFundingMethod(fundingMethod);
return this;
}

public Sep31Transaction build() {
return txn;
}
Expand Down
15 changes: 14 additions & 1 deletion core/src/main/java/org/stellar/anchor/util/AssetValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ static void validateSep31(Sep31Info sep31Info, String assetId) throws InvalidCon
"if quotes_required is true, quotes_supported must also be true for asset: %s",
assetId));

// Validate SEP-31 `receive.min_amount` and `receive.max_amount` fields
// Validate SEP-31 `receive.min_amount`, `receive.max_amount`, and `receive.methods` fields
ReceiveOperation receiveInfo = sep31Info.getReceive();
if (receiveInfo != null) {
if (receiveInfo.getMinAmount() < 0)
Expand All @@ -100,6 +100,19 @@ static void validateSep31(Sep31Info sep31Info, String assetId) throws InvalidCon
format(
"Invalid max_amount defined for asset %s. sep31.receive.max_amount = %s",
assetId, receiveInfo.getMaxAmount()));
// Check for empty and duplicate receive methods
if (isEmpty(receiveInfo.getMethods())) {
throw new InvalidConfigException(
format("No receive methods defined for asset %s", assetId));
}
Set<String> existingReceiveMethods = new HashSet<>();
for (String method : receiveInfo.getMethods()) {
if (!existingReceiveMethods.add(method)) {
throw new InvalidConfigException(
format(
"Duplicate receive method defined for asset %s. Type = %s", assetId, method));
}
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions core/src/main/java/org/stellar/anchor/util/SepHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.stellar.anchor.util.MathHelper.decimal;

import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.stellar.anchor.api.exception.AnchorException;
Expand Down Expand Up @@ -137,6 +138,28 @@ public static void validateAmountLimit(String messagePrefix, String amount, Long
}
}

/**
* Validates whether a specified funding method is supported for a given asset.
*
* @param assetID the unique id of the asset being validated.
* @param method the funding method to validate.
* @param supportedMethods a list of funding methods supported for the asset.
* @throws BadRequestException if the provided funding method is not in the list of supported
* methods.
*/
public static void validateFundingMethod(
String assetID, String method, List<String> supportedMethods) throws BadRequestException {
if (StringHelper.isEmpty(method)) {
throw new BadRequestException("funding_method cannot be empty");
}
if (!supportedMethods.contains(method)) {
throw new BadRequestException(
String.format(
"invalid funding method %s for asset %s, supported types are %s",
method, assetID, supportedMethods));
}
}

/**
* Checks if the status is valid in a SEP.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static GetTransactionResponse toGetTransactionResponse(Sep31Transaction t
.sep(PlatformTransactionData.Sep.SEP_31)
.kind(RECEIVE)
.status(SepTransactionStatus.from(txn.getStatus()))
.type(txn.getFundingMethod())
.amountExpected(new Amount(txn.getAmountExpected(), txn.getAmountInAsset()))
.amountIn(new Amount(txn.getAmountIn(), txn.getAmountInAsset()))
.amountOut(new Amount(txn.getAmountOut(), txn.getAmountOutAsset()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ items:
# the maximum amount that the user can receive.
min_amount: 0
max_amount: 1000
# The list of methods supported by the anchor to receive assets.
# Example:
# - SEPA
# - SWIFT
methods: []

# The configuration for the SEP-38 transactions.
sep38:
Expand All @@ -122,6 +127,7 @@ items:
# the maximum amount that the user can receive.
min_amount: 0
max_amount:
methods: []
# `true` if the asset supports quotesinforesponse.java.
quotes_supported: true
# `true` if the asset requires quotes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class PojoSep31Transaction implements Sep31Transaction {
String receiverId;
String senderId;
StellarId creator;
String fundingMethod;
List<FeeDescription> feeDetailsList;

public void setFeeDetails(FeeDetails feeDetails) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ internal class DefaultAssetServiceTest {
}
}

@Test
fun `test invalid config with duplicate receive type when sep-31 enabled`() {
assertThrows<InvalidConfigException> {
DefaultAssetService.fromYamlResource("test_assets_duplicate_receive_methods.yaml")
}
}

@Test
fun `test invalid config with missing receive type when sep-31 enabled`() {
assertThrows<InvalidConfigException> {
DefaultAssetService.fromYamlResource("test_assets_missing_receive_method.yaml")
}
}

@Test
fun `test trailing comma in JSON does not result in null element`() {
val assetsService = DefaultAssetService.fromJsonContent(trailingCommaInAssets)
Expand Down Expand Up @@ -146,7 +160,11 @@ internal class DefaultAssetServiceTest {
"enabled": true,
"receive": {
"min_amount": 1,
"max_amount": 1000000
"max_amount": 1000000,
"methods": [
"SEPA",
"SWIFT"
]
},
"quotes_supported": true,
"quotes_required": true
Expand Down Expand Up @@ -183,7 +201,11 @@ internal class DefaultAssetServiceTest {
"enabled": true,
"receive": {
"min_amount": 1,
"max_amount": 1000000
"max_amount": 1000000,
"methods": [
"SEPA",
"SWIFT"
]
},
"quotes_supported": true,
"quotes_required": true
Expand All @@ -203,7 +225,11 @@ internal class DefaultAssetServiceTest {
"enabled": false,
"receive": {
"min_amount": 1,
"max_amount": 1000000
"max_amount": 1000000,
"methods": [
"SEPA",
"SWIFT"
]
}
},
"sep38": {
Expand Down
44 changes: 18 additions & 26 deletions core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,30 +99,14 @@ class Sep31ServiceTest {
"enabled": true,
"receive": {
"min_amount": 1,
"max_amount": 1000000
"max_amount": 1000000,
"methods": [
"SEPA",
"SWIFT"
]
},
"quotes_supported": true,
"quotes_required": true,
"fields": {
"transaction": {
"receiver_routing_number": {
"description": "routing number of the destination bank account",
"optional": false
},
"receiver_account_number": {
"description": "bank account number of the destination",
"optional": false
},
"type": {
"description": "type of deposit to make",
"choices": [
"SEPA",
"SWIFT"
],
"optional": false
}
}
}
"quotes_required": true
},
"sep38": {
"enabled": true,
Expand Down Expand Up @@ -537,10 +521,14 @@ class Sep31ServiceTest {
assertInstanceOf(BadRequestException::class.java, ex)
assertEquals("amount should be positive", ex.message)

// ----- QUOTE_ID IS USED ⬇️ -----
postTxRequest.lang = "en"
postTxRequest.amount = "1"
ex = assertThrows { sep31Service.postTransaction(jwtToken, postTxRequest) }
assertInstanceOf(BadRequestException::class.java, ex)
assertEquals("funding_method cannot be empty", ex.message)

postTxRequest.fundingMethod = "SEPA"
// ----- QUOTE_ID IS USED ⬇️ -----
// not found quote_id
val fields =
hashMapOf(
Expand Down Expand Up @@ -622,6 +610,7 @@ class Sep31ServiceTest {
postTxRequest.senderId = senderId
postTxRequest.receiverId = receiverId
postTxRequest.quoteId = "my_quote_id"
postTxRequest.fundingMethod = "SEPA"
postTxRequest.fields =
Sep31TxnFields(
hashMapOf(
Expand Down Expand Up @@ -678,6 +667,7 @@ class Sep31ServiceTest {
"""{
"id": "$txId",
"status": "pending_receiver",
"fundingMethod": "SEPA",
"amountFee": "10",
"amountFeeAsset": "$stellarUSDC",
"startedAt": "$txStartedAt",
Expand Down Expand Up @@ -721,6 +711,7 @@ class Sep31ServiceTest {
postTxRequest.assetIssuer = "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"
postTxRequest.senderId = senderId
postTxRequest.receiverId = receiverId
postTxRequest.fundingMethod = "SEPA"
postTxRequest.fields =
Sep31TxnFields(
hashMapOf(
Expand Down Expand Up @@ -778,6 +769,7 @@ class Sep31ServiceTest {
postTxRequest.assetIssuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
postTxRequest.senderId = senderId
postTxRequest.receiverId = receiverId
postTxRequest.fundingMethod = "SEPA"
postTxRequest.fields =
Sep31TxnFields(
hashMapOf(
Expand Down Expand Up @@ -807,13 +799,13 @@ class Sep31ServiceTest {

private val jpycJson =
"""
{"enabled":true,"quotes_supported":true,"quotes_required":true,"min_amount":1,"max_amount":1000000,"fields":{"transaction":{"receiver_routing_number":{"description":"routing number of the destination bank account","optional":false},"receiver_account_number":{"description":"bank account number of the destination","optional":false},"type":{"description":"type of deposit to make","choices":["ACH","SWIFT","WIRE"],"optional":false}}}}
"""
{"enabled":true,"quotes_supported":true,"quotes_required":true,"min_amount":1,"max_amount":1000000,"funding_methods":["SEPA","SWIFT"]}
"""
.trimIndent()

private val usdcJson =
"""
{"enabled":true,"quotes_supported":true,"quotes_required":true,"min_amount":1,"max_amount":1000000,"fields":{"transaction":{"receiver_routing_number":{"description":"routing number of the destination bank account","optional":false},"receiver_account_number":{"description":"bank account number of the destination","optional":false}, "receiver_phone_number": {"description": "phone number of the receiver", "optional": true},"type":{"description":"type of deposit to make","choices":["SEPA","SWIFT"],"optional":false}}}}
{"enabled":true,"quotes_supported":true,"quotes_required":true,"min_amount":1,"max_amount":1000000,"funding_methods":["SEPA","SWIFT"]}
"""
.trimIndent()

Expand Down
Loading

0 comments on commit 6eeb100

Please sign in to comment.