Skip to content

Commit

Permalink
Implement SEP-6 deposit for contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
philipliu committed Feb 17, 2025
1 parent f21f62f commit c883d21
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 36 deletions.
26 changes: 21 additions & 5 deletions core/src/main/java/org/stellar/anchor/sep6/RequestValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import org.stellar.anchor.api.exception.*;
import org.stellar.anchor.asset.AssetService;
import org.stellar.anchor.util.StringHelper;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Address;
import org.stellar.sdk.MuxedAccount;
import org.stellar.sdk.scval.Scv;

/** SEP-6 request validations */
@RequiredArgsConstructor
Expand Down Expand Up @@ -113,10 +115,24 @@ public void validateTypes(String requestType, String assetCode, List<String> val
* @throws SepValidationException if the account is invalid
*/
public void validateAccount(String account) throws AnchorException {
try {
KeyPair.fromAccountId(account);
} catch (IllegalArgumentException ex) {
throw new SepValidationException(String.format("invalid account %s", account));
switch (account.charAt(0)) {
case 'G':
case 'C':
try {
Address.fromSCAddress(Scv.fromAddress(Scv.toAddress(account)).toSCAddress());
} catch (RuntimeException ex) {
throw new SepValidationException(String.format("invalid account %s", account));
}
break;
case 'M':
try {
new MuxedAccount(account);
} catch (RuntimeException ex) {
throw new SepValidationException(String.format("invalid account %s", account));
}
break;
default:
throw new SepValidationException(String.format("invalid account %s", account));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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.asset.DepositWithdrawOperation
import org.stellar.anchor.api.asset.Sep6Info
Expand Down Expand Up @@ -152,9 +151,17 @@ class RequestValidatorTest {
}
}

@Test
fun `test validateAccount`() {
requestValidator.validateAccount(TEST_ACCOUNT)
@ValueSource(
strings =
[
"GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP",
"MBFZNZTFSI6TWLVAID7VOLCIFX2PMUOS2X7U6H4TNK4PAPSHPWMMUAAAAAAAAAPCIA2IM",
"CAASCQKVVBSLREPEUGPOTQZ4BC2NDBY2MW7B2LGIGFUPIY4Z3XUZRVTX",
]
)
@ParameterizedTest
fun `test validateAccount`(account: String) {
requestValidator.validateAccount(account)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.anchor.platform.e2etest

import io.ktor.http.*
import java.net.URI
import kotlin.test.DefaultAsserter.fail
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.seconds
Expand All @@ -10,14 +11,22 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.stellar.anchor.api.sep.SepTransactionStatus
import org.stellar.anchor.api.sep.SepTransactionStatus.*
import org.stellar.anchor.api.sep.sep12.Sep12PutCustomerRequest
import org.stellar.anchor.api.sep.sep12.Sep12Status
import org.stellar.anchor.api.sep.sep45.ChallengeRequest
import org.stellar.anchor.api.sep.sep6.GetTransactionResponse
import org.stellar.anchor.api.shared.InstructionField
import org.stellar.anchor.client.Sep12Client
import org.stellar.anchor.client.Sep45Client
import org.stellar.anchor.client.Sep6Client
import org.stellar.anchor.platform.AbstractIntegrationTests
import org.stellar.anchor.platform.CLIENT_SMART_WALLET_ACCOUNT
import org.stellar.anchor.platform.CLIENT_WALLET_SECRET
import org.stellar.anchor.platform.TestConfig
import org.stellar.anchor.util.GsonUtils
import org.stellar.anchor.util.Log
import org.stellar.reference.wallet.WalletServerClient
import org.stellar.sdk.SorobanServer
import org.stellar.walletsdk.anchor.MemoType
import org.stellar.walletsdk.anchor.auth
import org.stellar.walletsdk.anchor.customer
Expand All @@ -28,6 +37,7 @@ import org.stellar.walletsdk.horizon.sign
open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
private val maxTries = 30
private val walletServerClient = WalletServerClient(Url(config.env["wallet.server.url"]!!))
private val gson = GsonUtils.getInstance()

companion object {
private val USDC =
Expand Down Expand Up @@ -126,11 +136,76 @@ open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
listOf(
Sep12Status.ACCEPTED, // initial customer status before SEP-6 transaction
Sep12Status.NEEDS_INFO, // SEP-6 transaction requires additional info
Sep12Status.ACCEPTED // additional info provided
Sep12Status.ACCEPTED, // additional info provided
)
assertWalletReceivedCustomerStatuses(customer.id, expectedCustomerStatuses)
}

@Test
fun `test typical deposit to contract account end-to-end`() = runBlocking {
val webAuthDomain = toml.getString("WEB_AUTH_FOR_CONTRACTS_ENDPOINT")
val homeDomain = "http://${URI.create(webAuthDomain).authority}"

val sep45Client =
Sep45Client(
toml.getString("WEB_AUTH_FOR_CONTRACTS_ENDPOINT"),
SorobanServer("https://soroban-testnet.stellar.org"),
CLIENT_WALLET_SECRET,
)
val challenge =
sep45Client.getChallenge(
ChallengeRequest.builder()
.account(CLIENT_SMART_WALLET_ACCOUNT)
.homeDomain(homeDomain)
.build()
)
val token = sep45Client.validate(sep45Client.sign(challenge)).token
val sep12Client = Sep12Client(toml.getString("KYC_SERVER"), token)
val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token)

val customerRequest =
gson.fromJson(
gson.toJson(basicInfoFields.associateWith { customerInfo[it]!! }),
Sep12PutCustomerRequest::class.java,
)
val customer = sep12Client.putCustomer(customerRequest)!!

val deposit =
sep6Client.deposit(
mapOf(
"asset_code" to USDC.code,
"account" to CLIENT_SMART_WALLET_ACCOUNT,
"amount" to "1",
"type" to "SWIFT",
)
)
Log.info("Deposit initiated: ${deposit.id}")

val additionalRequiredFields =
sep12Client
.getCustomer(customer.id, "sep-6", deposit.id)!!
.fields
?.filter { it.key != null && it.value?.optional == false }
?.map { it.key!! }
.orEmpty()
val additionalCustomerRequest =
gson.fromJson(
gson.toJson(additionalRequiredFields.associateWith { customerInfo[it]!! }),
Sep12PutCustomerRequest::class.java,
)
sep12Client.putCustomer(additionalCustomerRequest)

Log.info("Bank transfer complete")
waitStatus(deposit.id, COMPLETED, sep6Client)

val completedDepositTxn = sep6Client.getTransaction(mapOf("id" to deposit.id))
val transactionByStellarId: GetTransactionResponse =
sep6Client.getTransaction(
mapOf("stellar_transaction_id" to completedDepositTxn.transaction.stellarTransactionId)
)
assertEquals(completedDepositTxn.transaction.id, transactionByStellarId.transaction.id)
}

@Test
fun `test typical deposit-exchange without quote end-to-end flow`() = runBlocking {
val memo = (20000..30000).random().toULong()
Expand Down Expand Up @@ -206,11 +281,78 @@ open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
listOf(
Sep12Status.ACCEPTED, // initial customer status before SEP-6 transaction
Sep12Status.NEEDS_INFO, // SEP-6 transaction requires additional info
Sep12Status.ACCEPTED // additional info provided
Sep12Status.ACCEPTED, // additional info provided
)
assertWalletReceivedCustomerStatuses(customer.id, expectedCustomerStatuses)
}

@Test
fun `test typical deposit-exchange to contract account end-to-end`() = runBlocking {
val webAuthDomain = toml.getString("WEB_AUTH_FOR_CONTRACTS_ENDPOINT")
val homeDomain = "http://${URI.create(webAuthDomain).authority}"

val sep45Client =
Sep45Client(
toml.getString("WEB_AUTH_FOR_CONTRACTS_ENDPOINT"),
SorobanServer("https://soroban-testnet.stellar.org"),
CLIENT_WALLET_SECRET,
)
val challenge =
sep45Client.getChallenge(
ChallengeRequest.builder()
.account(CLIENT_SMART_WALLET_ACCOUNT)
.homeDomain(homeDomain)
.build()
)
val token = sep45Client.validate(sep45Client.sign(challenge)).token
val sep12Client = Sep12Client(toml.getString("KYC_SERVER"), token)
val sep6Client = Sep6Client("${config.env["anchor.domain"]}/sep6", token)

val customerRequest =
gson.fromJson(
gson.toJson(basicInfoFields.associateWith { customerInfo[it]!! }),
Sep12PutCustomerRequest::class.java,
)
val customer = sep12Client.putCustomer(customerRequest)!!

val deposit =
sep6Client.deposit(
mapOf(
"destination_asset" to USDC.code,
"source_asset" to "iso4217:CAD",
"amount" to "1",
"account" to CLIENT_SMART_WALLET_ACCOUNT,
"type" to "SWIFT",
),
exchange = true,
)
Log.info("Deposit initiated: ${deposit.id}")

val additionalRequiredFields =
sep12Client
.getCustomer(customer.id, "sep-6", deposit.id)!!
.fields
?.filter { it.key != null && it.value?.optional == false }
?.map { it.key!! }
.orEmpty()
val additionalCustomerRequest =
gson.fromJson(
gson.toJson(additionalRequiredFields.associateWith { customerInfo[it]!! }),
Sep12PutCustomerRequest::class.java,
)
sep12Client.putCustomer(additionalCustomerRequest)

Log.info("Bank transfer complete")
waitStatus(deposit.id, COMPLETED, sep6Client)

val completedDepositTxn = sep6Client.getTransaction(mapOf("id" to deposit.id))
val transactionByStellarId: GetTransactionResponse =
sep6Client.getTransaction(
mapOf("stellar_transaction_id" to completedDepositTxn.transaction.stellarTransactionId)
)
assertEquals(completedDepositTxn.transaction.id, transactionByStellarId.transaction.id)
}

@Test
fun `test typical withdraw end-to-end flow`() = runBlocking {
val memo = (40000..50000).random().toULong()
Expand Down Expand Up @@ -275,7 +417,7 @@ open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
listOf(
Sep12Status.ACCEPTED, // initial customer status before SEP-6 transaction
Sep12Status.NEEDS_INFO, // SEP-6 transaction requires additional info
Sep12Status.ACCEPTED // additional info provided
Sep12Status.ACCEPTED, // additional info provided
)
assertWalletReceivedCustomerStatuses(customer.id, expectedCustomerStatuses)
}
Expand Down Expand Up @@ -350,7 +492,7 @@ open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
listOf(
Sep12Status.ACCEPTED, // initial customer status before SEP-6 transaction
Sep12Status.NEEDS_INFO, // SEP-6 transaction requires additional info
Sep12Status.ACCEPTED // additional info provided
Sep12Status.ACCEPTED, // additional info provided
)
assertWalletReceivedCustomerStatuses(customer.id, expectedCustomerStatuses)
}
Expand All @@ -364,15 +506,15 @@ open class Sep6End2EndTest : AbstractIntegrationTests(TestConfig()) {
"sep6",
txnId,
expected.size,
GetTransactionResponse::class.java
GetTransactionResponse::class.java,
)
val statuses = callbacks.map { it.transaction.status }
assertEquals(expected.map { it.status }, statuses)
}

private suspend fun assertWalletReceivedCustomerStatuses(
id: String,
expected: List<Sep12Status>
expected: List<Sep12Status>,
) {
val callbacks = walletServerClient.pollCustomerCallbacks(id, expected.size)
val statuses: List<Sep12Status> = callbacks.map { it.status }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class AppSettings(
val isTest: Boolean,
val port: Int,
val horizonEndpoint: String,
val rpcEndpoint: String,
val platformApiEndpoint: String,
val distributionWallet: String,
val distributionWalletMemo: String,
Expand All @@ -40,13 +41,13 @@ data class AuthSettings(
enum class Type {
NONE,
API_KEY,
JWT
JWT,
}
}

data class DataSettings(
val url: String,
val database: String,
val user: String,
val password: String
val password: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@ object EventConsumerContainer {
Sep6EventProcessor(
config,
ServiceContainer.horizon,
ServiceContainer.rpc,
ServiceContainer.platform,
ServiceContainer.customerService,
ServiceContainer.sepHelper,
)
private val sep31EventProcessor =
Sep31EventProcessor(
config,
ServiceContainer.platform,
)
private val sep31EventProcessor = Sep31EventProcessor(config, ServiceContainer.platform)
private val noOpEventProcessor = NoOpEventProcessor()
private val processor =
AnchorEventProcessor(sep6EventProcessor, sep31EventProcessor, noOpEventProcessor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.stellar.reference.sep24.WithdrawalService
import org.stellar.reference.service.SepHelper
import org.stellar.reference.service.sep31.ReceiveService
import org.stellar.sdk.Server
import org.stellar.sdk.SorobanServer

object ServiceContainer {
private val config = ConfigContainer.getInstance().config
Expand All @@ -37,6 +38,7 @@ object ServiceContainer {
val customerService = CustomerService(customerRepo, transactionKYCRepo, sepHelper)
val rateService = RateService(quotesRepo)
val horizon = Server(config.appSettings.horizonEndpoint)
val rpc = SorobanServer(config.appSettings.rpcEndpoint)
val platform =
PlatformClient(
HttpClient {
Expand Down
Loading

0 comments on commit c883d21

Please sign in to comment.