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-514] Populate sep38quote info into sep24txn #1194

Merged
merged 12 commits into from
Nov 27, 2023
115 changes: 101 additions & 14 deletions core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import static org.stellar.anchor.api.sep.sep24.InfoResponse.FeeResponse;
import static org.stellar.anchor.event.EventService.EventQueue.TRANSACTION;
import static org.stellar.anchor.sep24.Sep24Helper.fromTxn;
import static org.stellar.anchor.sep24.Sep24Transaction.Kind.WITHDRAWAL;
import static org.stellar.anchor.sep24.Sep24Transaction.Kind.*;
import static org.stellar.anchor.util.Log.debug;
import static org.stellar.anchor.util.Log.debugF;
import static org.stellar.anchor.util.Log.info;
Expand All @@ -27,18 +27,9 @@
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import org.stellar.anchor.api.event.AnchorEvent;
import org.stellar.anchor.api.exception.AnchorException;
import org.stellar.anchor.api.exception.SepException;
import org.stellar.anchor.api.exception.SepNotAuthorizedException;
import org.stellar.anchor.api.exception.SepNotFoundException;
import org.stellar.anchor.api.exception.SepValidationException;
import org.stellar.anchor.api.exception.*;
import org.stellar.anchor.api.sep.AssetInfo;
import org.stellar.anchor.api.sep.sep24.GetTransactionRequest;
import org.stellar.anchor.api.sep.sep24.GetTransactionsRequest;
Expand All @@ -55,6 +46,8 @@
import org.stellar.anchor.config.CustodyConfig;
import org.stellar.anchor.config.Sep24Config;
import org.stellar.anchor.event.EventService;
import org.stellar.anchor.sep38.Sep38Quote;
import org.stellar.anchor.sep38.Sep38QuoteStore;
import org.stellar.anchor.util.ConfigHelper;
import org.stellar.anchor.util.CustodyUtils;
import org.stellar.anchor.util.MetricConstants;
Expand All @@ -75,6 +68,8 @@ public class Sep24Service {
final MoreInfoUrlConstructor moreInfoUrlConstructor;
final CustodyConfig custodyConfig;

final Sep38QuoteStore sep38QuoteStore;

final Counter sep24TransactionRequestedCounter =
counter(MetricConstants.SEP24_TRANSACTION_REQUESTED);
final Counter sep24TransactionQueriedCounter = counter(MetricConstants.SEP24_TRANSACTION_QUERIED);
Expand Down Expand Up @@ -103,7 +98,8 @@ public Sep24Service(
EventService eventService,
InteractiveUrlConstructor interactiveUrlConstructor,
MoreInfoUrlConstructor moreInfoUrlConstructor,
CustodyConfig custodyConfig) {
CustodyConfig custodyConfig,
Sep38QuoteStore sep38QuoteStore) {
debug("appConfig:", appConfig);
debug("sep24Config:", sep24Config);
this.appConfig = appConfig;
Expand All @@ -116,6 +112,7 @@ public Sep24Service(
this.interactiveUrlConstructor = interactiveUrlConstructor;
this.moreInfoUrlConstructor = moreInfoUrlConstructor;
this.custodyConfig = custodyConfig;
this.sep38QuoteStore = sep38QuoteStore;
info("Sep24Service initialized.");
}

Expand Down Expand Up @@ -248,6 +245,16 @@ public InteractiveTransactionResponse withdraw(
builder.refundMemoType(memoTypeString(memoType(refundMemo)));
}

this.validatedAndPopulateQuote(
builder,
WITHDRAWAL.toString(),
txnId,
withdrawRequest.get("quote_id"),
assetCode,
assetIssuer,
withdrawRequest.get("destination_asset"),
strAmount);

Sep24Transaction txn = builder.build();
txnStore.save(txn);

Expand Down Expand Up @@ -383,7 +390,7 @@ public InteractiveTransactionResponse deposit(Sep10Jwt token, Map<String, String
new Sep24TransactionBuilder(txnStore)
.transactionId(txnId)
.status(INCOMPLETE.toString())
.kind(Sep24Transaction.Kind.DEPOSIT.toString())
.kind(DEPOSIT.toString())
.assetCode(assetCode)
.assetIssuer(depositRequest.get("asset_issuer"))
.startedAt(Instant.now())
Expand All @@ -408,6 +415,16 @@ public InteractiveTransactionResponse deposit(Sep10Jwt token, Map<String, String
builder.memoType(memoTypeString(memoType(memo)));
}

this.validatedAndPopulateQuote(
builder,
DEPOSIT.toString(),
txnId,
depositRequest.get("quote_id"),
assetCode,
assetIssuer,
depositRequest.get("source_asset"),
strAmount);

Sep24Transaction txn = builder.build();
txnStore.save(txn);

Expand Down Expand Up @@ -555,4 +572,74 @@ public InfoResponse getInfo() {
sep24Config.getFeatures().getClaimableBalances()))
.build();
}

private void validatedAndPopulateQuote(
Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote a utility class that does this validation for SEP-6. Do you think we can leverage it here? I think the logic should be the same. https://github.com/stellar/java-stellar-anchor-sdk/blob/8e595eab7aa01147d610cb39313afa0c18d75630/core/src/main/java/org/stellar/anchor/sep6/ExchangeAmountsCalculator.java#L31-L62

Sep24TransactionBuilder builder,
String kind,
String txnId,
String quoteId,
String assetCode,
String assetIssuer,
String sourceOrDestAsset,
String strAmount)
throws BadRequestException {
if (quoteId == null) {
return;
}

Sep38Quote quote = sep38QuoteStore.findByQuoteId(quoteId);
if (quote == null) {
infoF("Quote ({}) was not found", quoteId);
throw new BadRequestException(String.format("quote(id=%s) was not found.", quoteId));
}

String[] onChainAsset =
kind.equals(DEPOSIT.toString())
? quote.getBuyAsset().split(":")
: quote.getSellAsset().split(":");
String offChainAsset =
kind.equals(DEPOSIT.toString()) ? quote.getSellAsset() : quote.getBuyAsset();

// assetCode needs to match the on-chain asset code in the quote
if (!assetCode.equals(onChainAsset[1])) {
infoF("Quote ({}) does not match asset code ({})", quoteId, assetCode);
throw new BadRequestException(
String.format("quote(id=%s) does not match asset code (%s).", quoteId, assetCode));
}

// issuer, if provided, needs to match the on-chain asset issuer in the quote, except for native
if (assetIssuer != null
&& !assetCode.equals("native")
&& !assetIssuer.equals(onChainAsset[2])) {
infoF("Quote ({}) does not match asset issuer ({})", quoteId, assetIssuer);
throw new BadRequestException(
String.format("quote(id=%s) does not match asset issuer (%s).", quoteId, assetIssuer));
}

// source or destination asset, if provided, needs to match the off-chain asset in the quote
if (sourceOrDestAsset != null && !sourceOrDestAsset.equals(offChainAsset)) {
infoF(
"Quote ({}) does not match source or destination asset ({})", quoteId, sourceOrDestAsset);
throw new BadRequestException(
String.format(
"quote(id=%s) does not match source or destination asset (%s).",
quoteId, sourceOrDestAsset));
}

// amount, if provided, needs to match the sell_amount in the quote
if (strAmount != null && !(decimal(strAmount).equals(decimal(quote.getSellAmount())))) {
infoF("Quote ({}) does not match source amount ({})", quoteId, strAmount);
throw new BadRequestException(
String.format("quote(id=%s) does not match amount (%s).", quoteId, strAmount));
}

debugF("Updating transaction ({}) with quote ({})", txnId, quoteId);
builder.quoteId(quoteId);
builder.amountExpected(quote.getSellAmount());
if (kind.equals(DEPOSIT.toString())) {
builder.sourceAsset(offChainAsset);
} else {
builder.destinationAsset(offChainAsset);
}
}
}
12 changes: 12 additions & 0 deletions core/src/main/java/org/stellar/anchor/sep24/Sep24Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ public interface Sep24Transaction extends SepTransaction {

String getMessage();

String getQuoteId();
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

The docs would need to be updated to include quote_id under the SEP-24 transaction as well. https://developers.stellar.org/api/anchor-platform/resources/get-transaction


void setQuoteId(String quoteId);

String getSourceAsset();

void setSourceAsset(String sourceAsset);

String getDestinationAsset();

void setDestinationAsset(String sourceAsset);

enum Kind {
DEPOSIT("deposit"),
WITHDRAWAL("withdrawal");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ public Sep24TransactionBuilder refundMemoType(String refundMemoType) {
return this;
}

public void quoteId(String quoteId) {
txn.setQuoteId(quoteId);
}

public void sourceAsset(String sourceAsset) {
txn.setSourceAsset(sourceAsset);
}

public void destinationAsset(String destinationAsset) {
txn.setDestinationAsset(destinationAsset);
}

public Sep24Transaction build() {
return txn;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ public class PojoSep24Transaction implements Sep24Transaction {
String message;
String refundMemo;
String refundMemoType;
String quoteId;
String sourceAsset;
String destinationAsset;
}
62 changes: 59 additions & 3 deletions core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import org.stellar.anchor.auth.Sep10Jwt
import org.stellar.anchor.auth.Sep24InteractiveUrlJwt
import org.stellar.anchor.config.*
import org.stellar.anchor.event.EventService
import org.stellar.anchor.sep31.Sep31ServiceTest
import org.stellar.anchor.sep38.PojoSep38Quote
import org.stellar.anchor.sep38.Sep38QuoteStore
import org.stellar.anchor.util.GsonUtils
import org.stellar.anchor.util.MemoHelper.makeMemo
import org.stellar.sdk.MemoHash
Expand All @@ -53,6 +56,42 @@ internal class Sep24ServiceTest {
const val TEST_SEP24_MORE_INFO_URL = "https://test-anchor.stellar.org/more_info_url"
val TEST_STARTED_AT: Instant = Instant.now()
val TEST_COMPLETED_AT: Instant = Instant.now().plusSeconds(100)
val DEPOSIT_QUOTE_JSON =
JiahuiWho marked this conversation as resolved.
Show resolved Hide resolved
"""
{
"id": "test-deposit-quote-id",
"expires_at": "2021-04-30T07:42:23",
"total_price": "5.42",
"price": "5.00",
"sell_asset": "iso4217:BRL",
"sell_amount": "542",
"buy_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP",
"buy_amount": "100",
"fee": {
"total": "42.00",
"asset": "iso4217:BRL"
}
}
"""
.trimIndent()
val WITHDRAW_QUOTE_JSON =
"""
{
"id": "test-withdraw-quote-id",
"expires_at": "2021-04-30T07:42:23",
"total_price": "0.542",
"price": "0.5",
"sell_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP",
"sell_amount": "542",
"buy_asset": "iso4217:BRL",
"buy_amount": "1000",
"fee": {
"total": "42",
"asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"
}
}
"""
.trimIndent()
}

@MockK(relaxed = true) lateinit var appConfig: AppConfig
Expand All @@ -78,11 +117,15 @@ internal class Sep24ServiceTest {

@MockK(relaxed = true) lateinit var clientConfig: ClientsConfig.ClientConfig

@MockK(relaxed = true) lateinit var sep38QuoteStore: Sep38QuoteStore

private val assetService: AssetService = DefaultAssetService.fromJsonResource("test_assets.json")

private lateinit var jwtService: JwtService
private lateinit var sep24Service: Sep24Service
private lateinit var testInteractiveUrlJwt: Sep24InteractiveUrlJwt
private lateinit var depositQuote: PojoSep38Quote
private lateinit var withdrawQuote: PojoSep38Quote

private val gson = GsonUtils.getInstance()

Expand Down Expand Up @@ -115,8 +158,11 @@ internal class Sep24ServiceTest {
eventService,
interactiveUrlConstructor,
moreInfoUrlConstructor,
custodyConfig
custodyConfig,
sep38QuoteStore
)
depositQuote = Sep31ServiceTest.gson.fromJson(DEPOSIT_QUOTE_JSON, PojoSep38Quote::class.java)
withdrawQuote = Sep31ServiceTest.gson.fromJson(WITHDRAW_QUOTE_JSON, PojoSep38Quote::class.java)
}

@Test
Expand All @@ -128,8 +174,13 @@ internal class Sep24ServiceTest {
val slotTxn = slot<Sep24Transaction>()

every { txnStore.save(capture(slotTxn)) } returns null
every { sep38QuoteStore.findByQuoteId(any()) } returns withdrawQuote

val response = sep24Service.withdraw(createTestSep10JwtToken(), createTestTransactionRequest())
val response =
sep24Service.withdraw(
createTestSep10JwtToken(),
createTestTransactionRequest(withdrawQuote.id)
)

verify(exactly = 1) { txnStore.save(any()) }

Expand All @@ -146,6 +197,8 @@ internal class Sep24ServiceTest {
)
assertEquals(TEST_ACCOUNT, slotTxn.captured.fromAccount)
assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain)
assertEquals(withdrawQuote.id, slotTxn.captured.quoteId)
assertEquals(withdrawQuote.buyAsset, slotTxn.captured.destinationAsset)

val params = URLEncodedUtils.parse(URI(response.url), Charset.forName("UTF-8"))
val tokenStrings = params.filter { pair -> pair.name.equals("token") }
Expand Down Expand Up @@ -257,8 +310,9 @@ internal class Sep24ServiceTest {
val slotTxn = slot<Sep24Transaction>()

every { txnStore.save(capture(slotTxn)) } returns null
every { sep38QuoteStore.findByQuoteId(any()) } returns depositQuote

val request = createTestTransactionRequest()
val request = createTestTransactionRequest(depositQuote.id)
request["claimable_balance_supported"] = claimableBalanceSupported
val response = sep24Service.deposit(createTestSep10JwtToken(), request)

Expand All @@ -274,6 +328,8 @@ internal class Sep24ServiceTest {
assertEquals(TEST_ASSET_ISSUER_ACCOUNT_ID, slotTxn.captured.requestAssetIssuer)
assertEquals(TEST_ACCOUNT, slotTxn.captured.toAccount)
assertEquals(TEST_CLIENT_DOMAIN, slotTxn.captured.clientDomain)
assertEquals(depositQuote.id, slotTxn.captured.quoteId)
assertEquals(depositQuote.sellAsset, slotTxn.captured.sourceAsset)
JiahuiWho marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package org.stellar.anchor.sep24

import org.stellar.anchor.TestConstants

fun createTestTransactionRequest(): MutableMap<String, String> {
return mutableMapOf(
"lang" to "en",
"asset_code" to TestConstants.TEST_ASSET,
"asset_issuer" to TestConstants.TEST_ASSET_ISSUER_ACCOUNT_ID,
"account" to TestConstants.TEST_ACCOUNT,
"amount" to "123.4",
"email_address" to "[email protected]",
"first_name" to "Jamie",
"last_name" to "Li"
)
fun createTestTransactionRequest(
quoteID: String? = null,
): MutableMap<String, String> {
val request =
mutableMapOf(
"lang" to "en",
"asset_code" to TestConstants.TEST_ASSET,
"asset_issuer" to TestConstants.TEST_ASSET_ISSUER_ACCOUNT_ID,
"account" to TestConstants.TEST_ACCOUNT,
"amount" to "542",
"email_address" to "[email protected]",
"first_name" to "Jamie",
"last_name" to "Li",
)
if (quoteID != null) {
request["quote_id"] = quoteID
}
return request
}

fun createTestTransaction(kind: String): Sep24Transaction {
Expand Down
Loading
Loading