diff --git a/core/src/main/java/org/stellar/anchor/config/ClientsConfig.java b/core/src/main/java/org/stellar/anchor/config/ClientsConfig.java new file mode 100644 index 0000000000..b7abd5e048 --- /dev/null +++ b/core/src/main/java/org/stellar/anchor/config/ClientsConfig.java @@ -0,0 +1,27 @@ +package org.stellar.anchor.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public interface ClientsConfig { + @Data + @AllArgsConstructor + @NoArgsConstructor + class ClientConfig { + String name; + ClientType type; + String signingKey; + String domain; + String callbackUrl; + } + + enum ClientType { + CUSTODIAL, + NONCUSTODIAL + } + + ClientConfig getClientConfigBySigningKey(String signingKey); + + ClientConfig getClientConfigByDomain(String domain); +} diff --git a/core/src/main/java/org/stellar/anchor/config/Sep10Config.java b/core/src/main/java/org/stellar/anchor/config/Sep10Config.java index 317b258ca4..fc46aca399 100644 --- a/core/src/main/java/org/stellar/anchor/config/Sep10Config.java +++ b/core/src/main/java/org/stellar/anchor/config/Sep10Config.java @@ -67,22 +67,18 @@ public interface Sep10Config { boolean isClientAttributionRequired(); /** - * Get the list of allowed client domains if the client attribution is required. + * Get the list of allowed client domains. * * @return the list of allowed client domains. */ - List getClientAttributionAllowList(); + List getAllowedClientDomains(); /** - * Whether to require authenticating clients to be in the list of known custodial accounts. # # If - * the flag is set to true, the client must be one of the custodial clients defined in the clients - * section # of this configuration file. + * Get the list of allowed client names. * - *

The flag is only relevant for custodial wallets. - * - * @return true if known custodial account is required. + * @return the list of allowed client names. */ - boolean isKnownCustodialAccountRequired(); + List getAllowedClientNames(); /** * Set the list of known custodial accounts. diff --git a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java index 980d4296cb..cc142e1963 100644 --- a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java +++ b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java @@ -76,17 +76,9 @@ public ChallengeResponse createChallenge(ChallengeRequest challengeRequest) thro sep10Config.getKnownCustodialAccountList().contains(challengeRequest.getAccount().trim()); } - if (sep10Config.isKnownCustodialAccountRequired() && !custodialWallet) { - // validate that requesting account is allowed access - infoF("requesting account: {} is not in allow list", challengeRequest.getAccount().trim()); - throw new SepNotAuthorizedException("unable to process"); - } - - if (custodialWallet) { - if (challengeRequest.getClientDomain() != null) { - throw new SepValidationException( - "client_domain must not be specified if the account is an custodial-wallet account"); - } + if (custodialWallet && challengeRequest.getClientDomain() != null) { + throw new SepValidationException( + "client_domain must not be specified if the account is an custodial-wallet account"); } if (!custodialWallet && sep10Config.isClientAttributionRequired()) { @@ -95,7 +87,7 @@ public ChallengeResponse createChallenge(ChallengeRequest challengeRequest) thro throw new SepValidationException("client_domain is required"); } - List allowList = sep10Config.getClientAttributionAllowList(); + List allowList = sep10Config.getAllowedClientDomains(); if (!allowList.contains(challengeRequest.getClientDomain())) { infoF( "client_domain provided ({}) is not in the configured allow list", diff --git a/core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java b/core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java index a127f9c01e..55b241167d 100644 --- a/core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java +++ b/core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java @@ -13,7 +13,6 @@ 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.*; import static org.stellar.anchor.util.SepHelper.amountEquals; import static org.stellar.anchor.util.SepHelper.generateSepTransactionId; import static org.stellar.anchor.util.SepHelper.validateAmount; @@ -64,7 +63,10 @@ import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.auth.Sep10Jwt; import org.stellar.anchor.config.AppConfig; +import org.stellar.anchor.config.ClientsConfig; +import org.stellar.anchor.config.ClientsConfig.ClientConfig; import org.stellar.anchor.config.CustodyConfig; +import org.stellar.anchor.config.Sep10Config; import org.stellar.anchor.config.Sep31Config; import org.stellar.anchor.custody.CustodyService; import org.stellar.anchor.event.EventService; @@ -75,12 +77,13 @@ import org.stellar.anchor.util.TransactionHelper; public class Sep31Service { - private final AppConfig appConfig; + private final Sep10Config sep10Config; private final Sep31Config sep31Config; private final Sep31TransactionStore sep31TransactionStore; private final Sep31DepositInfoGenerator sep31DepositInfoGenerator; private final Sep38QuoteStore sep38QuoteStore; + private final ClientsConfig clientsConfig; private final AssetService assetService; private final FeeIntegration feeIntegration; private final CustomerIntegration customerIntegration; @@ -93,10 +96,12 @@ public class Sep31Service { public Sep31Service( AppConfig appConfig, + Sep10Config sep10Config, Sep31Config sep31Config, Sep31TransactionStore sep31TransactionStore, Sep31DepositInfoGenerator sep31DepositInfoGenerator, Sep38QuoteStore sep38QuoteStore, + ClientsConfig clientsConfig, AssetService assetService, FeeIntegration feeIntegration, CustomerIntegration customerIntegration, @@ -106,10 +111,12 @@ public Sep31Service( debug("appConfig:", appConfig); debug("sep31Config:", sep31Config); this.appConfig = appConfig; + this.sep10Config = sep10Config; this.sep31Config = sep31Config; this.sep31TransactionStore = sep31TransactionStore; this.sep31DepositInfoGenerator = sep31DepositInfoGenerator; this.sep38QuoteStore = sep38QuoteStore; + this.clientsConfig = clientsConfig; this.assetService = assetService; this.feeIntegration = feeIntegration; this.customerIntegration = customerIntegration; @@ -557,13 +564,28 @@ void updateFee() throws SepValidationException, AnchorException { .receiveAmount(null) .senderId(request.getSenderId()) .receiverId(request.getReceiverId()) - .clientId(token.getAccount()) + .clientId(getClientName()) .build()) .getFee(); infoF("Fee for request ({}) is ({})", request, fee); Context.get().setFee(fee); } + String getClientName() throws BadRequestException { + return getClientName(Context.get().getSep10Jwt().getAccount()); + } + + String getClientName(String account) throws BadRequestException { + ClientConfig client = clientsConfig.getClientConfigBySigningKey(account); + if (sep10Config.isClientAttributionRequired() && client == null) { + throw new BadRequestException("Client not found"); + } + if (client != null && !sep10Config.getAllowedClientDomains().contains(client.getDomain())) { + client = null; + } + return client == null ? null : client.getName(); + } + /** * validateSenderAndReceiver will validate if the SEP-31 sender and receiver exist and their * status is ACCEPTED. @@ -704,7 +726,6 @@ private static Sep31InfoResponse sep31InfoResponseFromAssetInfoList(List { sep10Service.createChallenge(cr) } - verify(exactly = 1) { sep10Config.isKnownCustodialAccountRequired } - verify(exactly = 2) { sep10Config.knownCustodialAccountList } - assertInstanceOf(SepNotAuthorizedException::class.java, ex) - assertEquals("unable to process", ex.message) - } } diff --git a/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt index f8cbefd2a3..641806be1f 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt @@ -9,12 +9,16 @@ import java.time.Instant import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import java.util.* +import java.util.stream.Stream import org.apache.commons.lang3.StringUtils import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* 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.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.skyscreamer.jsonassert.JSONAssert import org.stellar.anchor.TestConstants import org.stellar.anchor.TestHelper @@ -266,6 +270,25 @@ class Sep31ServiceTest { } } """ + + private val lobstrClientConfig = + ClientsConfig.ClientConfig( + "lobstr", + ClientsConfig.ClientType.NONCUSTODIAL, + "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", + "lobstr.co", + "https://callback.lobstr.co/api/v2/anchor/callback" + ) + + @JvmStatic + fun generateGetClientNameTestConfig(): Stream { + return Stream.of( + Arguments.of(listOf(), false, null, false), + Arguments.of(listOf(), true, null, true), + Arguments.of(listOf(lobstrClientConfig.domain), false, lobstrClientConfig.name, false), + Arguments.of(listOf(lobstrClientConfig.domain), true, lobstrClientConfig.name, true), + ) + } } private val assetService: AssetService = DefaultAssetService.fromJsonResource("test_assets.json") @@ -275,6 +298,8 @@ class Sep31ServiceTest { @MockK(relaxed = true) lateinit var appConfig: AppConfig @MockK(relaxed = true) lateinit var secretConfig: SecretConfig @MockK(relaxed = true) lateinit var custodySecretConfig: CustodySecretConfig + @MockK(relaxed = true) lateinit var clientsConfig: ClientsConfig + @MockK(relaxed = true) lateinit var sep10Config: Sep10Config @MockK(relaxed = true) lateinit var sep31Config: Sep31Config @MockK(relaxed = true) lateinit var sep31DepositInfoGenerator: Sep31DepositInfoGenerator @MockK(relaxed = true) lateinit var quoteStore: Sep38QuoteStore @@ -287,7 +312,6 @@ class Sep31ServiceTest { private lateinit var jwtService: JwtService private lateinit var sep31Service: Sep31Service - private lateinit var request: Sep31PostTransactionRequest private lateinit var txn: Sep31Transaction private lateinit var fee: Amount @@ -305,15 +329,18 @@ class Sep31ServiceTest { every { txnStore.newTransaction() } returns PojoSep31Transaction() every { custodyConfig.type } returns NONE every { eventService.createSession(any(), TRANSACTION) } returns eventSession + jwtService = spyk(JwtService(secretConfig, custodySecretConfig)) sep31Service = Sep31Service( appConfig, + sep10Config, sep31Config, txnStore, sep31DepositInfoGenerator, quoteStore, + clientsConfig, assetService, feeIntegration, customerIntegration, @@ -367,10 +394,12 @@ class Sep31ServiceTest { val ex: AnchorException = assertThrows { Sep31Service( appConfig, + sep10Config, sep31Config, txnStore, sep31DepositInfoGenerator, quoteStore, + clientsConfig, assetServiceQuotesNotSupported, feeIntegration, customerIntegration, @@ -888,10 +917,12 @@ class Sep31ServiceTest { sep31Service = Sep31Service( appConfig, + sep10Config, sep31Config, txnStore, sep31DepositInfoGenerator, quoteStore, + clientsConfig, assetServiceQuotesNotSupported, feeIntegration, customerIntegration, @@ -1045,4 +1076,31 @@ class Sep31ServiceTest { val ex = assertThrows { sep31Service.updateFee() } assertEquals("Quote is missing the 'fee' field", ex.message) } + + @ParameterizedTest + @MethodSource("generateGetClientNameTestConfig") + fun `test getClientName when`( + allowedClientDomains: List, + isClientAttributionRequired: Boolean, + expectedClientName: String?, + shouldThrowExceptionWithInvalidInput: Boolean, + ) { + every { sep10Config.allowedClientDomains } returns allowedClientDomains + every { sep10Config.isClientAttributionRequired } returns isClientAttributionRequired + every { clientsConfig.getClientConfigBySigningKey(lobstrClientConfig.signingKey) } returns + lobstrClientConfig + + // client name should be returned for valid input + val clientName = sep31Service.getClientName(lobstrClientConfig.signingKey) + assertEquals(expectedClientName, clientName) + + // exception maybe thrown for invalid input + every { clientsConfig.getClientConfigBySigningKey("Invalid Public Key") } returns null + if (!shouldThrowExceptionWithInvalidInput) { + val clientNameNotFound = sep31Service.getClientName("Invalid Public Key") + assertNull(clientNameNotFound) + } else { + assertThrows { sep31Service.getClientName("Invalid Public Key") } + } + } } diff --git a/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt index 9dc0238153..877bcfb0c9 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep38/Sep38ServiceTest.kt @@ -47,6 +47,7 @@ class Sep38ServiceTest { companion object { private const val PUBLIC_KEY = "GBJDSMTMG4YBP27ZILV665XBISBBNRP62YB7WZA2IQX2HIPK7ABLF4C2" + private const val CLIENT_ID = "1234" private const val stellarUSDC = "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" private const val fiatUSD = "iso4217:USD" @@ -56,29 +57,27 @@ class Sep38ServiceTest { private lateinit var sep38Service: Sep38Service private val sep38Config = PropertySep38Config() + private val assetService = DefaultAssetService.fromJsonResource("test_assets.json") - // store/db related: - @MockK(relaxed = true) private lateinit var quoteStore: Sep38QuoteStore - // events @MockK(relaxed = true) private lateinit var eventService: EventService @MockK(relaxed = true) private lateinit var eventSession: Session - // sep10 @MockK(relaxed = true) private lateinit var secretConfig: SecretConfig - // sep38 + @MockK(relaxed = true) private lateinit var mockQuoteStore: Sep38QuoteStore + @MockK(relaxed = true) private lateinit var mockRateIntegration: MockRateIntegration @BeforeEach fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) - val assetService = DefaultAssetService.fromJsonResource("test_assets.json") val assets = assetService.listAllAssets() - this.sep38Service = Sep38Service(sep38Config, assetService, null, null, eventService) + this.sep38Service = + Sep38Service(sep38Config, assetService, mockRateIntegration, mockQuoteStore, eventService) assertEquals(4, assets.size) // sep10 related: every { secretConfig.sep10JwtSecretKey } returns "secret" // store/db related: - every { quoteStore.newInstance() } returns PojoSep38Quote() + every { mockQuoteStore.newInstance() } returns PojoSep38Quote() // events related: every { eventService.createSession(any(), TRANSACTION) } returns eventSession } @@ -137,12 +136,10 @@ class Sep38ServiceTest { @Test fun `Test GET prices failure`() { // empty rateIntegration should throw an error + this.sep38Service = Sep38Service(sep38Config, assetService, null, null, eventService) var ex: AnchorException = assertThrows { sep38Service.getPrices(null, null, null, null, null) } assertInstanceOf(ServerErrorException::class.java, ex) assertEquals("internal server error", ex.message) - - // mock rate integration - val mockRateIntegration = mockk() sep38Service = Sep38Service(sep38Config, sep38Service.assetService, mockRateIntegration, null, eventService) @@ -190,7 +187,6 @@ class Sep38ServiceTest { @Test fun `test get prices with minimum parameters`() { // mock rate integration - val mockRateIntegration = mockk() val getRateReq1 = GetRateRequest.builder() .type(INDICATIVE) @@ -225,7 +221,6 @@ class Sep38ServiceTest { @Test fun `test get prices with all parameters`() { // mock rate integration - val mockRateIntegration = mockk() val getRateReq1 = GetRateRequest.builder() .type(INDICATIVE) @@ -263,8 +258,6 @@ class Sep38ServiceTest { @Test fun `Test get prices filter with buy delivery method`() { - // mock rate integration - val mockRateIntegration = mockk() val getRateReq1 = GetRateRequest.builder() .type(INDICATIVE) @@ -293,122 +286,122 @@ class Sep38ServiceTest { var getPriceRequestBuilder = Sep38GetPriceRequest.builder() // empty rateIntegration should throw an error - var ex: AnchorException = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } - var wantException: AnchorException = ServerErrorException("internal server error") + var ex: AnchorException = assertThrows { + sep38Service.getPrice(null, getPriceRequestBuilder.build()) + } + var wantException: AnchorException = BadRequestException("internal server error") assertEquals(wantException, ex) - // mock rate integration - val mockRateIntegration = mockk() sep38Service = Sep38Service(sep38Config, sep38Service.assetService, mockRateIntegration, null, eventService) // empty sell_asset - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } wantException = BadRequestException("sell_asset cannot be empty") assertEquals(wantException, ex) // nonexistent sell_asset getPriceRequestBuilder = getPriceRequestBuilder.sellAssetName("foo:bar") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("sell_asset not found", ex.message) // empty buy_asset getPriceRequestBuilder = getPriceRequestBuilder.sellAssetName(fiatUSD) - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } wantException = BadRequestException("buy_asset cannot be empty") assertEquals(wantException, ex) // nonexistent buy_asset getPriceRequestBuilder = getPriceRequestBuilder.buyAssetName("foo:bar") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("buy_asset not found", ex.message) // both sell_amount & buy_amount are empty getPriceRequestBuilder = getPriceRequestBuilder.buyAssetName(stellarUSDC) - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Please provide either sell_amount or buy_amount", ex.message) // both sell_amount & buy_amount are filled getPriceRequestBuilder = getPriceRequestBuilder.sellAmount("100").buyAmount("100") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Please provide either sell_amount or buy_amount", ex.message) // invalid (not a number) sell_amount getPriceRequestBuilder = getPriceRequestBuilder.sellAmount("foo").buyAmount(null) - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount is invalid", ex.message) // sell_amount should be positive getPriceRequestBuilder = getPriceRequestBuilder.sellAmount("-0.01") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount should be positive", ex.message) // sell_amount should be positive getPriceRequestBuilder = getPriceRequestBuilder.sellAmount("0") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount should be positive", ex.message) // invalid (not a number) buy_amount getPriceRequestBuilder = getPriceRequestBuilder.sellAmount(null).buyAmount("bar") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("buy_amount is invalid", ex.message) // buy_amount should be positive getPriceRequestBuilder = getPriceRequestBuilder.buyAmount("-0.02") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("buy_amount should be positive", ex.message) // buy_amount should be positive getPriceRequestBuilder = getPriceRequestBuilder.buyAmount("0") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("buy_amount should be positive", ex.message) // unsupported sell_delivery_method getPriceRequestBuilder = getPriceRequestBuilder.sellAmount("1.23").buyAmount(null) getPriceRequestBuilder = getPriceRequestBuilder.sellDeliveryMethod("FOO") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Unsupported sell delivery method", ex.message) // unsupported buy_delivery_method getPriceRequestBuilder = getPriceRequestBuilder.sellDeliveryMethod("WIRE") getPriceRequestBuilder = getPriceRequestBuilder.buyDeliveryMethod("BAR") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Unsupported buy delivery method", ex.message) // unsupported country_code getPriceRequestBuilder = getPriceRequestBuilder.buyDeliveryMethod(null) getPriceRequestBuilder = getPriceRequestBuilder.countryCode("BRA") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Unsupported country code", ex.message) // unsupported (null) context getPriceRequestBuilder = getPriceRequestBuilder.countryCode("USA") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("Unsupported context. Should be one of [sep6, sep31].", ex.message) // sell_amount should be within limit getPriceRequestBuilder = getPriceRequestBuilder.context(SEP31).sellAmount("100000000") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount exceeds max limit", ex.message) // sell_amount should be positive getPriceRequestBuilder = getPriceRequestBuilder.context(SEP31).sellAmount("0.5") - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount less than min limit", ex.message) @@ -417,7 +410,7 @@ class Sep38ServiceTest { getPriceRequestBuilder = getPriceRequestBuilder.buyAssetName(stellarUSDC).buyAmount("100000000") every { mockRateIntegration.getRate(any()) } returns GetRateResponse.indicativePrice("1.02", "102000000", "100000000", mockSellAssetFee(fiatUSD)) - ex = assertThrows { sep38Service.getPrice(getPriceRequestBuilder.build()) } + ex = assertThrows { sep38Service.getPrice(null, getPriceRequestBuilder.build()) } assertInstanceOf(BadRequestException::class.java, ex) assertEquals("sell_amount exceeds max limit", ex.message) } @@ -434,8 +427,6 @@ class Sep38ServiceTest { fun `test GET price with minimum parameters and sell amount`() { val mockFee = mockSellAssetFee(fiatUSD) - // mock rate integration - val mockRateIntegration = mockk() val getRateReq = GetRateRequest.builder() .type(INDICATIVE) @@ -457,7 +448,7 @@ class Sep38ServiceTest { .context(SEP31) .build() var gotResponse: GetPriceResponse? = null - assertDoesNotThrow { gotResponse = sep38Service.getPrice(getPriceRequest) } + assertDoesNotThrow { gotResponse = sep38Service.getPrice(null, getPriceRequest) } val wantResponse = GetPriceResponse.builder() .price("1.02") @@ -473,8 +464,6 @@ class Sep38ServiceTest { fun `test get price with minimum parameters and buy amount`() { val mockFee = mockSellAssetFee(fiatUSD) - // mock rate integration - val mockRateIntegration = mockk() val getRateReq = GetRateRequest.builder() .type(INDICATIVE) @@ -496,7 +485,7 @@ class Sep38ServiceTest { .context(SEP31) .build() var gotResponse: GetPriceResponse? = null - assertDoesNotThrow { gotResponse = sep38Service.getPrice(getPriceRequest) } + assertDoesNotThrow { gotResponse = sep38Service.getPrice(null, getPriceRequest) } val wantResponse = GetPriceResponse.builder() .price("1.02") @@ -512,8 +501,6 @@ class Sep38ServiceTest { fun `test GET price all parameters with sell amount`() { val mockFee = mockSellAssetFee(fiatUSD) - // mock rate integration - val mockRateIntegration = mockk() val getRateReq = GetRateRequest.builder() .type(INDICATIVE) @@ -539,7 +526,7 @@ class Sep38ServiceTest { .context(SEP6) .build() var gotResponse: GetPriceResponse? = null - assertDoesNotThrow { gotResponse = sep38Service.getPrice(getPriceRequest) } + assertDoesNotThrow { gotResponse = sep38Service.getPrice(null, getPriceRequest) } val wantResponse = GetPriceResponse.builder() .price("1.02") @@ -554,9 +541,6 @@ class Sep38ServiceTest { @Test fun `test GET price all parameters with buy amount`() { val mockFee = mockSellAssetFee(fiatUSD) - - // mock rate integration - val mockRateIntegration = mockk() val getRateReq = GetRateRequest.builder() .type(INDICATIVE) @@ -583,7 +567,7 @@ class Sep38ServiceTest { .context(SEP31) .build() var gotResponse: GetPriceResponse? = null - assertDoesNotThrow { gotResponse = sep38Service.getPrice(getPriceRequest) } + assertDoesNotThrow { gotResponse = sep38Service.getPrice(null, getPriceRequest) } val wantResponse = GetPriceResponse.builder() .price("1.02345678901") @@ -598,14 +582,14 @@ class Sep38ServiceTest { @Test fun `test POST quote failures`() { // empty rateIntegration should throw an error + this.sep38Service = Sep38Service(sep38Config, assetService, null, mockQuoteStore, eventService) + var ex: AnchorException = assertThrows { sep38Service.postQuote(null, Sep38PostQuoteRequest.builder().build()) } assertInstanceOf(ServerErrorException::class.java, ex) assertEquals("internal server error", ex.message) - // mock rate integration - val mockRateIntegration = mockk() sep38Service = Sep38Service(sep38Config, sep38Service.assetService, mockRateIntegration, null, eventService) @@ -620,7 +604,7 @@ class Sep38ServiceTest { sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService ) @@ -898,19 +882,7 @@ class Sep38ServiceTest { @Test fun `test POST quote with minimum parameters and sell amount`() { val mockFee = mockSellAssetFee(fiatUSD) - - // mock rate integration - val mockRateIntegration = mockk() - val getRateReq = - GetRateRequest.builder() - .type(FIRM) - .sellAsset(fiatUSD) - .sellAmount("103") - .buyAsset(stellarUSDC) - .clientId(PUBLIC_KEY) - .build() val tomorrow = Instant.now().plus(1, ChronoUnit.DAYS) - val rate = GetRateResponse.Rate.builder() .id("123") @@ -920,19 +892,28 @@ class Sep38ServiceTest { .expiresAt(tomorrow) .fee(mockFee) .build() + val getRateReq = + GetRateRequest.builder() + .type(FIRM) + .sellAsset(fiatUSD) + .sellAmount("103") + .buyAsset(stellarUSDC) + .clientId(PUBLIC_KEY) + .build() val wantRateResponse = GetRateResponse(rate) every { mockRateIntegration.getRate(getRateReq) } returns wantRateResponse + sep38Service = Sep38Service( sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService ) val slotQuote = slot() - every { quoteStore.save(capture(slotQuote)) } returns null + every { mockQuoteStore.save(capture(slotQuote)) } returns null // Mock event service val slotEvent = slot() @@ -968,7 +949,7 @@ class Sep38ServiceTest { assertEquals(wantResponse, gotResponse) // verify the saved quote - verify(exactly = 1) { quoteStore.save(any()) } + verify(exactly = 1) { mockQuoteStore.save(any()) } val savedQuote = slotQuote.captured assertEquals("123", savedQuote.id) assertEquals(tomorrow, savedQuote.expiresAt) @@ -1009,18 +990,6 @@ class Sep38ServiceTest { @Test fun `test POST quote with minimum parameters and buy amount`() { val mockFee = mockSellAssetFee(fiatUSD) - - // mock rate integration - val mockRateIntegration = mockk() - val getRateReq = - GetRateRequest.builder() - .type(FIRM) - .sellAsset(fiatUSD) - .buyAsset(stellarUSDC) - .buyAmount("100") - .clientId(PUBLIC_KEY) - .build() - val tomorrow = Instant.now().plus(1, ChronoUnit.DAYS) val rate = GetRateResponse.Rate.builder() @@ -1032,18 +1001,18 @@ class Sep38ServiceTest { .fee(mockFee) .build() val wantRateResponse = GetRateResponse(rate) - every { mockRateIntegration.getRate(getRateReq) } returns wantRateResponse + every { mockRateIntegration.getRate(any()) } returns wantRateResponse sep38Service = Sep38Service( sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService, ) val slotQuote = slot() - every { quoteStore.save(capture(slotQuote)) } returns null + every { mockQuoteStore.save(capture(slotQuote)) } returns null // Mock event service val slotEvent = slot() @@ -1079,7 +1048,7 @@ class Sep38ServiceTest { assertEquals(wantResponse, gotResponse) // verify the saved quote - verify(exactly = 1) { quoteStore.save(any()) } + verify(exactly = 1) { mockQuoteStore.save(any()) } val savedQuote = slotQuote.captured assertEquals("456", savedQuote.id) assertEquals(tomorrow, savedQuote.expiresAt) @@ -1120,21 +1089,6 @@ class Sep38ServiceTest { val mockFee = mockSellAssetFee(fiatUSD) val now = Instant.now() val tomorrow = now.plus(1, ChronoUnit.DAYS) - - // mock rate integration - val mockRateIntegration = mockk() - val getRateReq = - GetRateRequest.builder() - .type(FIRM) - .sellAsset(fiatUSD) - .sellAmount("100") - .sellDeliveryMethod("WIRE") - .buyAsset(stellarUSDC) - .countryCode("USA") - .clientId(PUBLIC_KEY) - .expireAfter(now.toString()) - .build() - val rate = GetRateResponse.Rate.builder() .id("123") @@ -1145,18 +1099,18 @@ class Sep38ServiceTest { .fee(mockFee) .build() val wantRateResponse = GetRateResponse(rate) - every { mockRateIntegration.getRate(getRateReq) } returns wantRateResponse + every { mockRateIntegration.getRate(any()) } returns wantRateResponse sep38Service = Sep38Service( sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService ) val slotQuote = slot() - every { quoteStore.save(capture(slotQuote)) } returns null + every { mockQuoteStore.save(capture(slotQuote)) } returns null // Mock event service val slotEvent = slot() @@ -1195,7 +1149,7 @@ class Sep38ServiceTest { assertEquals(wantResponse, gotResponse) // verify the saved quote - verify(exactly = 1) { quoteStore.save(any()) } + verify(exactly = 1) { mockQuoteStore.save(any()) } val savedQuote = slotQuote.captured assertEquals("123", savedQuote.id) assertEquals(tomorrow, savedQuote.expiresAt) @@ -1237,21 +1191,6 @@ class Sep38ServiceTest { val mockFee = mockSellAssetFee(fiatUSD) val now = Instant.now() val tomorrow = now.plus(1, ChronoUnit.DAYS) - - // mock rate integration - val mockRateIntegration = mockk() - val getRateReq = - GetRateRequest.builder() - .type(FIRM) - .sellAsset(fiatUSD) - .sellDeliveryMethod("WIRE") - .buyAsset(stellarUSDC) - .buyAmount("100") - .countryCode("USA") - .clientId(PUBLIC_KEY) - .expireAfter(now.toString()) - .build() - val rate = GetRateResponse.Rate.builder() .id("456") @@ -1262,18 +1201,18 @@ class Sep38ServiceTest { .fee(mockFee) .build() val wantRateResponse = GetRateResponse(rate) - every { mockRateIntegration.getRate(getRateReq) } returns wantRateResponse + every { mockRateIntegration.getRate(any()) } returns wantRateResponse sep38Service = Sep38Service( sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService ) val slotQuote = slot() - every { quoteStore.save(capture(slotQuote)) } returns null + every { mockQuoteStore.save(capture(slotQuote)) } returns null // Mock event service val slotEvent = slot() @@ -1312,7 +1251,7 @@ class Sep38ServiceTest { assertEquals(wantResponse, gotResponse) // verify the saved quote - verify(exactly = 1) { quoteStore.save(any()) } + verify(exactly = 1) { mockQuoteStore.save(any()) } val savedQuote = slotQuote.captured assertEquals("456", savedQuote.id) assertEquals(tomorrow, savedQuote.expiresAt) @@ -1362,21 +1301,6 @@ class Sep38ServiceTest { val anchorCalculatedBuyAmount = "100" val anchorCalculatedSellAmount = "123" val anchorCalculatedPrice = "1.23" - - // mock rate integration - val mockRateIntegration = mockk() - val getRateReq = - GetRateRequest.builder() - .type(FIRM) - .sellAsset(fiatUSD) - .sellDeliveryMethod("WIRE") - .buyAsset(stellarUSDC) - .buyAmount(requestBuyAmount) - .countryCode("USA") - .clientId(PUBLIC_KEY) - .expireAfter(now.toString()) - .build() - val rate = GetRateResponse.Rate.builder() .id("456") @@ -1387,18 +1311,18 @@ class Sep38ServiceTest { .fee(mockFee) .build() val wantRateResponse = GetRateResponse(rate) - every { mockRateIntegration.getRate(getRateReq) } returns wantRateResponse + every { mockRateIntegration.getRate(any()) } returns wantRateResponse sep38Service = Sep38Service( sep38Config, sep38Service.assetService, mockRateIntegration, - quoteStore, + mockQuoteStore, eventService ) val slotQuote = slot() - every { quoteStore.save(capture(slotQuote)) } returns null + every { mockQuoteStore.save(capture(slotQuote)) } returns null // Mock event service val slotEvent = slot() @@ -1437,7 +1361,7 @@ class Sep38ServiceTest { assertEquals(wantResponse, gotResponse) // verify the saved quote - verify(exactly = 1) { quoteStore.save(any()) } + verify(exactly = 1) { mockQuoteStore.save(any()) } val savedQuote = slotQuote.captured assertEquals("456", savedQuote.id) assertEquals(tomorrow, savedQuote.expiresAt) @@ -1478,13 +1402,16 @@ class Sep38ServiceTest { @Test fun `test GET quote failure`() { // empty sep38QuoteStore should throw an error + this.sep38Service = + Sep38Service(sep38Config, assetService, mockRateIntegration, null, eventService) + var ex: AnchorException = assertThrows { sep38Service.getQuote(null, null) } assertInstanceOf(ServerErrorException::class.java, ex) assertEquals("internal server error", ex.message) // mocked quote store sep38Service = - Sep38Service(sep38Config, sep38Service.assetService, null, quoteStore, eventService) + Sep38Service(sep38Config, sep38Service.assetService, null, mockQuoteStore, eventService) // empty token ex = assertThrows { sep38Service.getQuote(null, null) } @@ -1507,7 +1434,7 @@ class Sep38ServiceTest { val now = Instant.now() val tomorrow = now.plus(1, ChronoUnit.DAYS) val mockQuoteBuilder: () -> Sep38QuoteBuilder = { - Sep38QuoteBuilder(quoteStore) + Sep38QuoteBuilder(mockQuoteStore) .id("123") .expiresAt(tomorrow) .price("1.02") @@ -1523,40 +1450,40 @@ class Sep38ServiceTest { val wrongAccount = "GB3MX4G2RSN5UC2GXLIQI7YAY3G5SH3TJZHT2WEDHGJLU5UW6IVXKGLL" var mockQuote = mockQuoteBuilder().creatorAccountId(wrongAccount).build() var slotQuoteId = slot() - every { quoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote + every { mockQuoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote ex = assertThrows { sep38Service.getQuote(token, "123") } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("quote not found", ex.message) - verify(exactly = 1) { quoteStore.findByQuoteId(any()) } + verify(exactly = 1) { mockQuoteStore.findByQuoteId(any()) } assertEquals("123", slotQuoteId.captured) // jwt token memo is different from quote memo mockQuote = mockQuoteBuilder().creatorAccountId(PUBLIC_KEY).creatorMemo("wrong memo!").build() slotQuoteId = slot() - every { quoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote + every { mockQuoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote ex = assertThrows { sep38Service.getQuote(token, "123") } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("quote not found", ex.message) - verify(exactly = 2) { quoteStore.findByQuoteId(any()) } + verify(exactly = 2) { mockQuoteStore.findByQuoteId(any()) } assertEquals("123", slotQuoteId.captured) // jwt token memo is different from quote memo mockQuote = mockQuoteBuilder().creatorAccountId(PUBLIC_KEY).creatorMemoType("wrong memoType!").build() slotQuoteId = slot() - every { quoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote + every { mockQuoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote ex = assertThrows { sep38Service.getQuote(token, "123") } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("quote not found", ex.message) - verify(exactly = 3) { quoteStore.findByQuoteId(any()) } + verify(exactly = 3) { mockQuoteStore.findByQuoteId(any()) } assertEquals("123", slotQuoteId.captured) // quote not found - every { quoteStore.findByQuoteId(capture(slotQuoteId)) } returns null + every { mockQuoteStore.findByQuoteId(capture(slotQuoteId)) } returns null ex = assertThrows { sep38Service.getQuote(token, "444") } assertInstanceOf(NotFoundException::class.java, ex) assertEquals("quote not found", ex.message) - verify(exactly = 4) { quoteStore.findByQuoteId(any()) } + verify(exactly = 4) { mockQuoteStore.findByQuoteId(any()) } assertEquals("444", slotQuoteId.captured) } @@ -1566,13 +1493,13 @@ class Sep38ServiceTest { // mocked quote store sep38Service = - Sep38Service(sep38Config, sep38Service.assetService, null, quoteStore, eventService) + Sep38Service(sep38Config, sep38Service.assetService, null, mockQuoteStore, eventService) // mock quote store response val now = Instant.now() val tomorrow = now.plus(1, ChronoUnit.DAYS) val mockQuote = - Sep38QuoteBuilder(quoteStore) + Sep38QuoteBuilder(mockQuoteStore) .id("123") .expiresAt(tomorrow) .price("1.02") @@ -1587,7 +1514,7 @@ class Sep38ServiceTest { .fee(mockFee) .build() val slotQuoteId = slot() - every { quoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote + every { mockQuoteStore.findByQuoteId(capture(slotQuoteId)) } returns mockQuote // execute request val token = createSep10Jwt() @@ -1595,7 +1522,7 @@ class Sep38ServiceTest { assertDoesNotThrow { gotQuoteResponse = sep38Service.getQuote(token, "123") } // verify the store response was called as expected - verify(exactly = 1) { quoteStore.findByQuoteId(any()) } + verify(exactly = 1) { mockQuoteStore.findByQuoteId(any()) } assertEquals("123", slotQuoteId.captured) // verify results @@ -1613,4 +1540,22 @@ class Sep38ServiceTest { .build() assertEquals(wantQuoteResponse, gotQuoteResponse) } + + @Test + fun `test clientId passed to getRate in Callback API`() { + val slotGetRateRequest = slot() + every { mockRateIntegration.getRate(capture(slotGetRateRequest)) } returns + GetRateResponse.indicativePrice("1", "100", "100", mockSellAssetFee(fiatUSD)) + + val token = createSep10Jwt(PUBLIC_KEY) + val getPriceRequest = + Sep38GetPriceRequest.builder() + .sellAssetName(fiatUSD) + .buyAssetName(stellarUSDC) + .buyAmount("100") + .context(SEP31) + .build() + sep38Service.getPrice(token, getPriceRequest) + assertEquals(PUBLIC_KEY, slotGetRateRequest.captured.clientId) + } } diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/fee/FeeService.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/fee/FeeService.kt index 10ad2b5484..50301ee845 100644 --- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/fee/FeeService.kt +++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/fee/FeeService.kt @@ -26,10 +26,6 @@ class FeeService(private val customerRepository: CustomerRepository) { throw BadRequestException("Receive asset must be provided") } - if (request.clientId == null) { - throw BadRequestException("Client id must be provided") - } - if (request.sendAmount == null && request.receiveAmount == null) { throw BadRequestException("Either send or receive amount must be provided") } diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/eventprocessor/EventProcessorBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/eventprocessor/EventProcessorBeans.java index 602f0d1dbd..d85029d523 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/eventprocessor/EventProcessorBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/eventprocessor/EventProcessorBeans.java @@ -6,8 +6,8 @@ import org.stellar.anchor.config.SecretConfig; import org.stellar.anchor.event.EventService; import org.stellar.anchor.platform.config.CallbackApiConfig; -import org.stellar.anchor.platform.config.ClientsConfig; import org.stellar.anchor.platform.config.EventProcessorConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.platform.event.EventProcessorManager; import org.stellar.anchor.sep24.MoreInfoUrlConstructor; import org.stellar.anchor.sep24.Sep24TransactionStore; @@ -21,7 +21,7 @@ EventProcessorManager eventProcessorManager( SecretConfig secretConfig, EventProcessorConfig eventProcessorConfig, CallbackApiConfig callbackApiConfig, - ClientsConfig clientsConfig, + PropertyClientsConfig clientsConfig, EventService eventService, AssetService assetService, Sep24TransactionStore sep24TransactionStore, diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index baf70bb9e0..afbbb1dd46 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -30,15 +30,7 @@ import org.stellar.anchor.horizon.Horizon; import org.stellar.anchor.platform.apiclient.CustodyApiClient; import org.stellar.anchor.platform.condition.ConditionalOnAllSepsEnabled; -import org.stellar.anchor.platform.config.CallbackApiConfig; -import org.stellar.anchor.platform.config.ClientsConfig; -import org.stellar.anchor.platform.config.PropertySep10Config; -import org.stellar.anchor.platform.config.PropertySep12Config; -import org.stellar.anchor.platform.config.PropertySep1Config; -import org.stellar.anchor.platform.config.PropertySep24Config; -import org.stellar.anchor.platform.config.PropertySep31Config; -import org.stellar.anchor.platform.config.PropertySep38Config; -import org.stellar.anchor.platform.config.PropertySep6Config; +import org.stellar.anchor.platform.config.*; import org.stellar.anchor.platform.observer.stellar.PaymentObservingAccountsManager; import org.stellar.anchor.platform.service.Sep31DepositInfoApiGenerator; import org.stellar.anchor.platform.service.Sep31DepositInfoCustodyGenerator; @@ -81,7 +73,7 @@ Sep6Config sep6Config() { @Bean @ConfigurationProperties(prefix = "sep10") Sep10Config sep10Config( - AppConfig appConfig, SecretConfig secretConfig, ClientsConfig clientsConfig) { + AppConfig appConfig, SecretConfig secretConfig, PropertyClientsConfig clientsConfig) { return new PropertySep10Config(appConfig, clientsConfig, secretConfig); } @@ -182,7 +174,7 @@ Sep24Service sep24Service( @Bean InteractiveUrlConstructor interactiveUrlConstructor( - ClientsConfig clientsConfig, + PropertyClientsConfig clientsConfig, PropertySep24Config sep24Config, CustomerIntegration customerIntegration, JwtService jwtService) { @@ -217,10 +209,12 @@ Sep31DepositInfoGenerator sep31DepositInfoGenerator( @ConditionalOnAllSepsEnabled(seps = {"sep31"}) Sep31Service sep31Service( AppConfig appConfig, + Sep10Config sep10Config, Sep31Config sep31Config, Sep31TransactionStore sep31TransactionStore, Sep31DepositInfoGenerator sep31DepositInfoGenerator, Sep38QuoteStore sep38QuoteStore, + PropertyClientsConfig clientsConfig, AssetService assetService, FeeIntegration feeIntegration, CustomerIntegration customerIntegration, @@ -229,10 +223,12 @@ Sep31Service sep31Service( CustodyConfig custodyConfig) { return new Sep31Service( appConfig, + sep10Config, sep31Config, sep31TransactionStore, sep31DepositInfoGenerator, sep38QuoteStore, + clientsConfig, assetService, feeIntegration, customerIntegration, diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java index b9b53dd451..2aa783eb9d 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java @@ -3,13 +3,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.stellar.anchor.platform.config.ClientsConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; @Configuration public class ClientsBeans { @Bean @ConfigurationProperties(prefix = "") - ClientsConfig clientsConfig() { - return new ClientsConfig(); + PropertyClientsConfig clientsConfig() { + return new PropertyClientsConfig(); } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java index 6e077c4bf4..b55e5850cc 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java @@ -15,8 +15,8 @@ import org.stellar.anchor.config.SecretConfig; import org.stellar.anchor.healthcheck.HealthCheckable; import org.stellar.anchor.horizon.Horizon; -import org.stellar.anchor.platform.config.ClientsConfig; import org.stellar.anchor.platform.config.PropertyAppConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.platform.config.PropertySecretConfig; import org.stellar.anchor.platform.config.PropertySep24Config; import org.stellar.anchor.platform.service.HealthCheckService; @@ -46,7 +46,7 @@ AppConfig appConfig() { @Bean MoreInfoUrlConstructor moreInfoUrlConstructor( - ClientsConfig clientsConfig, PropertySep24Config sep24Config, JwtService jwtService) { + PropertyClientsConfig clientsConfig, PropertySep24Config sep24Config, JwtService jwtService) { return new SimpleMoreInfoUrlConstructor( clientsConfig, sep24Config.getMoreInfoUrl(), jwtService); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/ClientsConfig.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java similarity index 71% rename from platform/src/main/java/org/stellar/anchor/platform/config/ClientsConfig.java rename to platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java index 82701293ee..ca0b34e040 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/ClientsConfig.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java @@ -9,58 +9,43 @@ import java.net.URL; import java.util.List; import java.util.Map; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.stellar.anchor.api.exception.SepException; +import org.stellar.anchor.config.ClientsConfig; import org.stellar.anchor.sep10.Sep10Helper; @Data -public class ClientsConfig implements Validator { +public class PropertyClientsConfig implements ClientsConfig, Validator { List clients = Lists.newLinkedList(); Map clientMap = null; Map domainToClientNameMap = null; Map signingKeyToClientNameMap = null; - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class ClientConfig { - String name; - ClientType type; - String signingKey; - String domain; - String callbackUrl; - } - - public enum ClientType { - CUSTODIAL, - NONCUSTODIAL - } - + @Override public ClientConfig getClientConfigBySigningKey(String signingKey) { if (signingKeyToClientNameMap == null) { signingKeyToClientNameMap = Maps.newHashMap(); clients.forEach( clientConfig -> { - if (clientConfig.signingKey != null) { - signingKeyToClientNameMap.put(clientConfig.signingKey, clientConfig.name); + if (clientConfig.getSigningKey() != null) { + signingKeyToClientNameMap.put(clientConfig.getSigningKey(), clientConfig.getName()); } }); } return getClientConfigByName(signingKeyToClientNameMap.get(signingKey)); } + @Override public ClientConfig getClientConfigByDomain(String domain) { if (domainToClientNameMap == null) { domainToClientNameMap = Maps.newHashMap(); clients.forEach( clientConfig -> { - if (clientConfig.domain != null) { - domainToClientNameMap.put(clientConfig.domain, clientConfig.name); + if (clientConfig.getDomain() != null) { + domainToClientNameMap.put(clientConfig.getDomain(), clientConfig.getName()); } }); } @@ -70,28 +55,28 @@ public ClientConfig getClientConfigByDomain(String domain) { public ClientConfig getClientConfigByName(String name) { if (clientMap == null) { clientMap = Maps.newHashMap(); - clients.forEach(clientConfig -> clientMap.put(clientConfig.name, clientConfig)); + clients.forEach(clientConfig -> clientMap.put(clientConfig.getName(), clientConfig)); } return clientMap.get(name); } @Override public boolean supports(@NotNull Class clazz) { - return ClientsConfig.class.isAssignableFrom(clazz); + return PropertyClientsConfig.class.isAssignableFrom(clazz); } @Override public void validate(@NotNull Object target, @NotNull Errors errors) { - ClientsConfig configs = (ClientsConfig) target; + PropertyClientsConfig configs = (PropertyClientsConfig) target; configs.clients.forEach(clientConfig -> validateClient(clientConfig, errors)); } private void validateClient(ClientConfig clientConfig, Errors errors) { debugF("Validating client {}", clientConfig); - if (isEmpty(clientConfig.name)) { + if (isEmpty(clientConfig.getName())) { errors.reject("empty-client-name", "The client.name cannot be empty and must be defined"); } - if (clientConfig.type.equals(ClientType.CUSTODIAL)) { + if (clientConfig.getType().equals(ClientType.CUSTODIAL)) { validateCustodialClient(clientConfig, errors); } else { validateNonCustodialClient(clientConfig, errors); @@ -99,13 +84,13 @@ private void validateClient(ClientConfig clientConfig, Errors errors) { } void validateCustodialClient(ClientConfig clientConfig, Errors errors) { - if (isEmpty(clientConfig.signingKey)) { + if (isEmpty(clientConfig.getSigningKey())) { errors.reject( "empty-client-signing-key", "The client.signingKey cannot be empty and must be defined"); } - if (!isEmpty(clientConfig.callbackUrl)) { + if (!isEmpty(clientConfig.getCallbackUrl())) { try { - new URL(clientConfig.callbackUrl); + new URL(clientConfig.getCallbackUrl()); } catch (MalformedURLException e) { errors.reject("client-invalid-callback_url", "The client.callbackUrl is invalid"); } @@ -113,14 +98,15 @@ void validateCustodialClient(ClientConfig clientConfig, Errors errors) { } void validateNonCustodialClient(ClientConfig clientConfig, Errors errors) { - if (isEmpty(clientConfig.domain)) { + if (isEmpty(clientConfig.getDomain())) { errors.reject("empty-client-domain", "The client.domain cannot be empty and must be defined"); } - if (!isEmpty(clientConfig.signingKey)) { + if (!isEmpty(clientConfig.getSigningKey())) { try { - String clientSigningKey = Sep10Helper.fetchSigningKeyFromClientDomain(clientConfig.domain); - if (!clientConfig.signingKey.equals(clientSigningKey)) { + String clientSigningKey = + Sep10Helper.fetchSigningKeyFromClientDomain(clientConfig.getDomain()); + if (!clientConfig.getSigningKey().equals(clientSigningKey)) { errors.reject( "client-signing-key-does-not-match", "The client.signingKey does not matched any valid registered keys"); @@ -132,9 +118,9 @@ void validateNonCustodialClient(ClientConfig clientConfig, Errors errors) { } } - if (!isEmpty(clientConfig.callbackUrl)) { + if (!isEmpty(clientConfig.getCallbackUrl())) { try { - new URL(clientConfig.callbackUrl); + new URL(clientConfig.getCallbackUrl()); } catch (MalformedURLException e) { errors.reject("client-invalid-callback_url", "The client.callbackUrl is invalid"); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java index 162fea0190..d066ac60ce 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep10Config.java @@ -1,12 +1,11 @@ package org.stellar.anchor.platform.config; import static java.lang.String.format; -import static org.stellar.anchor.platform.config.ClientsConfig.ClientType.CUSTODIAL; -import static org.stellar.anchor.platform.config.ClientsConfig.ClientType.NONCUSTODIAL; +import static org.stellar.anchor.config.ClientsConfig.ClientType.CUSTODIAL; +import static org.stellar.anchor.config.ClientsConfig.ClientType.NONCUSTODIAL; import static org.stellar.anchor.util.StringHelper.isEmpty; import static org.stellar.anchor.util.StringHelper.isNotEmpty; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import javax.annotation.PostConstruct; @@ -15,10 +14,9 @@ import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.stellar.anchor.config.AppConfig; +import org.stellar.anchor.config.ClientsConfig.ClientConfig; import org.stellar.anchor.config.SecretConfig; import org.stellar.anchor.config.Sep10Config; -import org.stellar.anchor.platform.config.ClientsConfig.ClientConfig; -import org.stellar.anchor.util.ListHelper; import org.stellar.anchor.util.NetUtil; import org.stellar.anchor.util.StringHelper; import org.stellar.sdk.*; @@ -29,27 +27,19 @@ public class PropertySep10Config implements Sep10Config, Validator { private String webAuthDomain; private String homeDomain; private boolean clientAttributionRequired = false; - private List defaultAllowClientDomain; private List clientAllowList = null; private Integer authTimeout = 900; private Integer jwtTimeout = 86400; - private boolean knownCustodialAccountRequired = false; - private List knownCustodialAccountList = new ArrayList<>(); + private List knownCustodialAccountList; private AppConfig appConfig; - private final ClientsConfig clientsConfig; + private final PropertyClientsConfig clientsConfig; private SecretConfig secretConfig; public PropertySep10Config( - AppConfig appConfig, ClientsConfig clientsConfig, SecretConfig secretConfig) { + AppConfig appConfig, PropertyClientsConfig clientsConfig, SecretConfig secretConfig) { this.appConfig = appConfig; this.clientsConfig = clientsConfig; this.secretConfig = secretConfig; - this.defaultAllowClientDomain = - clientsConfig.clients.stream() - .filter( - cfg -> cfg.getType() == NONCUSTODIAL && StringHelper.isNotEmpty(cfg.getDomain())) - .map(ClientConfig::getDomain) - .collect(Collectors.toList()); this.knownCustodialAccountList = clientsConfig.clients.stream() .filter( @@ -164,24 +154,20 @@ void validateConfig(Errors errors) { void validateClientAttribution(Errors errors) { if (clientAttributionRequired) { - if (ListHelper.isEmpty(getDefaultAllowClientDomain())) { + List nonCustodialClientNames = + clientsConfig.clients.stream() + .filter(cfg -> cfg.getType() == NONCUSTODIAL && isNotEmpty(cfg.getDomain())) + .map(ClientConfig::getName) + .collect(Collectors.toList()); + + if (nonCustodialClientNames.isEmpty()) { errors.reject( "sep10-client-attribution-lists-empty", "sep10.client_attribution_required is set to true but no NONCUSTODIAL clients are defined in the clients: section of the configuration."); } - - if (!ListHelper.isEmpty(getDefaultAllowClientDomain())) { - for (String clientDomain : getDefaultAllowClientDomain()) { - if (!NetUtil.isServerPortValid(clientDomain, false)) { - errors.rejectValue( - "clientAttributionAllowList", - "sep10-client_attribution_allow_list_invalid", - format("%s is not a valid value for client domain.", clientDomain)); - } - } - } } + // Make sure all the names in the allow list is defined in the clients section. if (clientAllowList != null && !clientAllowList.isEmpty()) { for (String clientName : clientAllowList) { if (clientsConfig.getClientConfigByName(clientName) == null) { @@ -203,29 +189,41 @@ void validateCustodialAccounts(Errors errors) { format("Invalid custodial account:%s in clients:", account)); } } - - if (knownCustodialAccountRequired && ListHelper.isEmpty(getKnownCustodialAccountList())) { - errors.reject( - "sep10-custodial-account-list-empty", - "No custodial clients custodial while sep10.known_custodial_account_required is set to true"); - } } @Override - public List getClientAttributionAllowList() { + public List getAllowedClientDomains() { // if clientAllowList is not defined, all client domains from the clients section are allowed. - if (clientAllowList == null || clientAllowList.isEmpty()) return defaultAllowClientDomain; - // Look up the client domains from the clients section. + if (clientAllowList == null || clientAllowList.isEmpty()) { + return clientsConfig.clients.stream() + .map(ClientConfig::getDomain) + .filter(StringHelper::isNotEmpty) + .collect(Collectors.toList()); + } + + // If clientAllowList is defined, only the clients in the allow list are allowed. return clientAllowList.stream() .map( - clientName -> - (clientsConfig.getClientConfigByName(clientName) == null) + domain -> + (clientsConfig.getClientConfigByName(domain) == null) ? null - : clientsConfig.getClientConfigByName(clientName).getDomain()) + : clientsConfig.getClientConfigByName(domain).getDomain()) .filter(StringHelper::isNotEmpty) .collect(Collectors.toList()); } + @Override + public List getAllowedClientNames() { + // if clientAllowList is not defined, all clients from the clients section are allowed. + if (clientAllowList == null || clientAllowList.isEmpty()) { + return clientsConfig.clients.stream() + .map(ClientConfig::getName) + .filter(StringHelper::isNotEmpty) + .collect(Collectors.toList()); + } + return clientAllowList; + } + @Override public List getKnownCustodialAccountList() { return knownCustodialAccountList; diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java index 5495b1b76c..3521925ac8 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep38Controller.java @@ -12,6 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestClientException; +import org.stellar.anchor.api.exception.SepValidationException; import org.stellar.anchor.api.sep.SepExceptionResponse; import org.stellar.anchor.api.sep.sep38.*; import org.stellar.anchor.auth.Sep10Jwt; @@ -69,11 +70,18 @@ public GetPricesResponse getPrices( @RequestMapping( value = "/price", method = {RequestMethod.GET}) - public GetPriceResponse getPrice(@RequestParam Map params) { + public GetPriceResponse getPrice( + HttpServletRequest request, @RequestParam Map params) { debugF("GET /price params={}", params); Sep38GetPriceRequest getPriceRequest = gson.fromJson(gson.toJson(params), Sep38GetPriceRequest.class); - return sep38Service.getPrice(getPriceRequest); + Sep10Jwt sep10Jwt; + try { + sep10Jwt = getSep10Token(request); + } catch (SepValidationException svex) { + sep10Jwt = null; + } + return sep38Service.getPrice(sep10Jwt, getPriceRequest); } @SneakyThrows diff --git a/platform/src/main/java/org/stellar/anchor/platform/event/ClientStatusCallbackHandler.java b/platform/src/main/java/org/stellar/anchor/platform/event/ClientStatusCallbackHandler.java index 987ec9ceb6..be5a082e2f 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/event/ClientStatusCallbackHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/event/ClientStatusCallbackHandler.java @@ -25,10 +25,12 @@ import org.stellar.anchor.api.platform.GetTransactionResponse; import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse; import org.stellar.anchor.asset.AssetService; +import org.stellar.anchor.config.ClientsConfig.ClientConfig; import org.stellar.anchor.config.SecretConfig; -import org.stellar.anchor.platform.config.ClientsConfig.ClientConfig; import org.stellar.anchor.platform.data.*; import org.stellar.anchor.sep24.*; +import org.stellar.anchor.sep24.MoreInfoUrlConstructor; +import org.stellar.anchor.sep24.Sep24Transaction; import org.stellar.anchor.sep31.RefundPayment; import org.stellar.anchor.sep31.Sep31Refunds; import org.stellar.anchor.sep31.Sep31Transaction; diff --git a/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java b/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java index 08aaf91e96..213d05d9fd 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java +++ b/platform/src/main/java/org/stellar/anchor/platform/event/EventProcessorManager.java @@ -26,8 +26,8 @@ import org.stellar.anchor.event.EventService; import org.stellar.anchor.event.EventService.EventQueue; import org.stellar.anchor.platform.config.CallbackApiConfig; -import org.stellar.anchor.platform.config.ClientsConfig; import org.stellar.anchor.platform.config.EventProcessorConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.platform.utils.DaemonExecutors; import org.stellar.anchor.sep24.MoreInfoUrlConstructor; import org.stellar.anchor.sep24.Sep24TransactionStore; @@ -42,7 +42,7 @@ public class EventProcessorManager { private final SecretConfig secretConfig; private final EventProcessorConfig eventProcessorConfig; private final CallbackApiConfig callbackApiConfig; - private final ClientsConfig clientsConfig; + private final PropertyClientsConfig clientsConfig; private final EventService eventService; private final AssetService assetService; private final Sep24TransactionStore sep24TransactionStore; @@ -55,7 +55,7 @@ public EventProcessorManager( SecretConfig secretConfig, EventProcessorConfig eventProcessorConfig, CallbackApiConfig callbackApiConfig, - ClientsConfig clientsConfig, + PropertyClientsConfig clientsConfig, EventService eventService, AssetService assetService, Sep24TransactionStore sep24TransactionStore, @@ -87,7 +87,7 @@ public void start() { // Create a processor of the client status callback handler for each client defined in the // clientsConfig if (eventProcessorConfig.getClientStatusCallback().isEnabled()) { - for (ClientsConfig.ClientConfig clientConfig : clientsConfig.getClients()) { + for (PropertyClientsConfig.ClientConfig clientConfig : clientsConfig.getClients()) { if (clientConfig.getCallbackUrl().isEmpty()) { Log.info(String.format("Client status callback skipped: %s", json(clientConfig))); diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructor.java b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructor.java index 7a2402c248..691b0dfb86 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructor.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructor.java @@ -18,7 +18,7 @@ import org.stellar.anchor.api.exception.AnchorException; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.auth.Sep24InteractiveUrlJwt; -import org.stellar.anchor.platform.config.ClientsConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.platform.config.PropertySep24Config; import org.stellar.anchor.sep24.InteractiveUrlConstructor; import org.stellar.anchor.sep24.Sep24Transaction; @@ -27,13 +27,13 @@ public class SimpleInteractiveUrlConstructor extends InteractiveUrlConstructor { public static final String FORWARD_KYC_CUSTOMER_TYPE = "sep24-customer"; - private final ClientsConfig clientsConfig; + private final PropertyClientsConfig clientsConfig; private final PropertySep24Config sep24Config; private final CustomerIntegration customerIntegration; private final JwtService jwtService; public SimpleInteractiveUrlConstructor( - ClientsConfig clientsConfig, + PropertyClientsConfig clientsConfig, PropertySep24Config sep24Config, CustomerIntegration customerIntegration, JwtService jwtService) { @@ -71,7 +71,7 @@ public String construct(Sep24Transaction txn, Map request) { @SneakyThrows String constructToken(Sep24Transaction txn, Map request) { - ClientsConfig.ClientConfig clientConfig = + PropertyClientsConfig.ClientConfig clientConfig = UrlConstructorHelper.getClientConfig(clientsConfig, txn); debugF( diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java index 0f4db311fa..3c9cfecc80 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructor.java @@ -10,17 +10,17 @@ import org.apache.http.client.utils.URIBuilder; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.auth.Sep24MoreInfoUrlJwt; -import org.stellar.anchor.platform.config.ClientsConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.sep24.MoreInfoUrlConstructor; import org.stellar.anchor.sep24.Sep24Transaction; public class SimpleMoreInfoUrlConstructor extends MoreInfoUrlConstructor { - private final ClientsConfig clientsConfig; + private final PropertyClientsConfig clientsConfig; private final MoreInfoUrlConfig config; private final JwtService jwtService; public SimpleMoreInfoUrlConstructor( - ClientsConfig clientsConfig, MoreInfoUrlConfig config, JwtService jwtService) { + PropertyClientsConfig clientsConfig, MoreInfoUrlConfig config, JwtService jwtService) { this.clientsConfig = clientsConfig; this.config = config; this.jwtService = jwtService; @@ -29,7 +29,7 @@ public SimpleMoreInfoUrlConstructor( @Override @SneakyThrows public String construct(Sep24Transaction txn) { - ClientsConfig.ClientConfig clientConfig = + PropertyClientsConfig.ClientConfig clientConfig = UrlConstructorHelper.getClientConfig(clientsConfig, txn); Sep24MoreInfoUrlJwt token = diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/UrlConstructorHelper.java b/platform/src/main/java/org/stellar/anchor/platform/service/UrlConstructorHelper.java index 2cce20a4f7..8a61b6518e 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/UrlConstructorHelper.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/UrlConstructorHelper.java @@ -6,7 +6,7 @@ import java.util.Map; import org.apache.commons.beanutils.BeanUtils; import org.stellar.anchor.api.exception.SepValidationException; -import org.stellar.anchor.platform.config.ClientsConfig; +import org.stellar.anchor.platform.config.PropertyClientsConfig; import org.stellar.anchor.sep24.Sep24Transaction; import org.stellar.anchor.util.StringHelper; @@ -40,12 +40,13 @@ public static String getAccount(Sep24Transaction txn) { : txn.getSep10Account() + ":" + txn.getSep10AccountMemo(); } - public static ClientsConfig.ClientConfig getClientConfig( - ClientsConfig clientsConfig, Sep24Transaction txn) throws SepValidationException { - ClientsConfig.ClientConfig clientConfig; + public static PropertyClientsConfig.ClientConfig getClientConfig( + PropertyClientsConfig clientsConfig, Sep24Transaction txn) throws SepValidationException { + PropertyClientsConfig.ClientConfig clientConfig; if (isEmpty(txn.getClientDomain())) { clientConfig = clientsConfig.getClientConfigBySigningKey(txn.getSep10Account()); - if (clientConfig != null && clientConfig.getType() == ClientsConfig.ClientType.NONCUSTODIAL) { + if (clientConfig != null + && clientConfig.getType() == PropertyClientsConfig.ClientType.NONCUSTODIAL) { throw new SepValidationException("Non-custodial clients must specify a client_domain"); } } else { diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/ClientsConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/PropertyClientsConfigTest.kt similarity index 85% rename from platform/src/test/kotlin/org/stellar/anchor/platform/config/ClientsConfigTest.kt rename to platform/src/test/kotlin/org/stellar/anchor/platform/config/PropertyClientsConfigTest.kt index e942ba3375..f112c4c763 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/ClientsConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/PropertyClientsConfigTest.kt @@ -5,22 +5,24 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.validation.BindException import org.springframework.validation.Errors -import org.stellar.anchor.platform.config.ClientsConfig.ClientConfig +import org.stellar.anchor.config.ClientsConfig.ClientConfig +import org.stellar.anchor.config.ClientsConfig.ClientType.CUSTODIAL +import org.stellar.anchor.config.ClientsConfig.ClientType.NONCUSTODIAL -class ClientsConfigTest { - private lateinit var configs: ClientsConfig +class PropertyClientsConfigTest { + private lateinit var configs: PropertyClientsConfig private lateinit var errors: Errors @BeforeEach fun setUp() { - configs = ClientsConfig() + configs = PropertyClientsConfig() errors = BindException(configs, "config") } @Test fun `test valid custodial client`() { val config = ClientConfig() config.name = "circle" - config.type = ClientsConfig.ClientType.CUSTODIAL + config.type = CUSTODIAL config.signingKey = "GBI2IWJGR4UQPBIKPP6WG76X5PHSD2QTEBGIP6AZ3ZXWV46ZUSGNEGN2" config.callbackUrl = "https://callback.circle.com/api/v1/anchor/callback" configs.clients.add(config) @@ -44,7 +46,7 @@ class ClientsConfigTest { fun `test valid non-custodial client`() { val config = ClientConfig() config.name = "lobstr" - config.type = ClientsConfig.ClientType.NONCUSTODIAL + config.type = NONCUSTODIAL config.domain = "lobstr.co" config.callbackUrl = "https://callback.lobstr.co/api/v2/anchor/callback" config.signingKey = "GC4HAYCFQYQLJV5SE6FB3LGC37D6XGIXGMAXCXWNBLH7NWW2JH4OZLHQ" diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt index cec75ba3a6..7ea0720bcf 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep10ConfigTest.kt @@ -12,15 +12,16 @@ import org.junit.jupiter.params.provider.ValueSource import org.springframework.validation.BindException import org.springframework.validation.Errors import org.stellar.anchor.config.AppConfig -import org.stellar.anchor.platform.config.ClientsConfig.ClientType.CUSTODIAL -import org.stellar.anchor.platform.config.ClientsConfig.ClientType.NONCUSTODIAL +import org.stellar.anchor.config.ClientsConfig.ClientConfig +import org.stellar.anchor.config.ClientsConfig.ClientType.CUSTODIAL +import org.stellar.anchor.config.ClientsConfig.ClientType.NONCUSTODIAL class Sep10ConfigTest { lateinit var config: PropertySep10Config lateinit var errors: Errors private lateinit var secretConfig: PropertySecretConfig private lateinit var appConfig: AppConfig - private var clientsConfig = ClientsConfig() + private var clientsConfig = PropertyClientsConfig() @BeforeEach fun setUp() { @@ -28,7 +29,7 @@ class Sep10ConfigTest { appConfig = mockk() clientsConfig.clients.add( - ClientsConfig.ClientConfig( + ClientConfig( "unknown", CUSTODIAL, "GBI2IWJGR4UQPBIKPP6WG76X5PHSD2QTEBGIP6AZ3ZXWV46ZUSGNEGN2", @@ -38,7 +39,7 @@ class Sep10ConfigTest { ) clientsConfig.clients.add( - ClientsConfig.ClientConfig( + ClientConfig( "lobstr", NONCUSTODIAL, "GC4HAYCFQYQLJV5SE6FB3LGC37D6XGIXGMAXCXWNBLH7NWW2JH4OZLHQ", @@ -48,7 +49,7 @@ class Sep10ConfigTest { ) clientsConfig.clients.add( - ClientsConfig.ClientConfig( + ClientConfig( "circle", NONCUSTODIAL, "GCSGSR6KQQ5BP2FXVPWRL6SWPUSFWLVONLIBJZUKTVQB5FYJFVL6XOXE", @@ -81,7 +82,7 @@ class Sep10ConfigTest { @Test fun `test validation of empty client allow list when client attribution is required`() { - val config = PropertySep10Config(appConfig, ClientsConfig(), secretConfig) + val config = PropertySep10Config(appConfig, PropertyClientsConfig(), secretConfig) config.isClientAttributionRequired = true config.validateClientAttribution(errors) assertErrorCode(errors, "sep10-client-attribution-lists-empty") @@ -121,39 +122,30 @@ class Sep10ConfigTest { @Test fun `test when clientAllowList is not defined, clientAttributionAllowList equals to the list of all clients`() { val config = PropertySep10Config(appConfig, clientsConfig, secretConfig) - assertEquals(config.clientAttributionAllowList, listOf("lobstr.co", "circle.com")) + assertEquals(config.allowedClientDomains, listOf("lobstr.co", "circle.com")) } @Test fun `test when clientAllowList is defined, clientAttributionAllowList returns correct values`() { val config = PropertySep10Config(appConfig, clientsConfig, secretConfig) config.clientAllowList = listOf("lobstr") - assertEquals(config.clientAttributionAllowList, listOf("lobstr.co")) + assertEquals(config.allowedClientDomains, listOf("lobstr.co")) config.clientAllowList = listOf("circle") - assertEquals(config.clientAttributionAllowList, listOf("circle.com")) + assertEquals(config.allowedClientDomains, listOf("circle.com")) config.clientAllowList = listOf("invalid") config.validateClientAttribution(errors) assertErrorCode(errors, "sep10-client-allow-list-invalid") - assertTrue(config.clientAttributionAllowList.isEmpty()) + assertTrue(config.allowedClientDomains.isEmpty()) } @Test fun `test required known custodial account`() { - config.isKnownCustodialAccountRequired = true config.validateCustodialAccounts(errors) assertFalse(errors.hasErrors()) } - @Test - fun `test known custodial account required but no custodial clients not defined`() { - config = PropertySep10Config(appConfig, ClientsConfig(), secretConfig) - config.isKnownCustodialAccountRequired = true - config.validateCustodialAccounts(errors) - assertErrorCode(errors, "sep10-custodial-account-list-empty") - } - @ParameterizedTest @ValueSource(strings = ["stellar.org", "moneygram.com", "localhost", "127.0.0.1:80"]) fun `test valid home domains`(value: String) { diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/event/ClientStatusCallbackHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/event/ClientStatusCallbackHandlerTest.kt index b1d3b2fb0b..4e935030e0 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/event/ClientStatusCallbackHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/event/ClientStatusCallbackHandlerTest.kt @@ -14,7 +14,8 @@ import org.stellar.anchor.api.platform.GetTransactionResponse import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.sep.sep24.TransactionResponse import org.stellar.anchor.asset.AssetService -import org.stellar.anchor.platform.config.ClientsConfig +import org.stellar.anchor.config.ClientsConfig.* +import org.stellar.anchor.config.ClientsConfig.ClientType.* import org.stellar.anchor.platform.config.PropertySecretConfig import org.stellar.anchor.sep24.MoreInfoUrlConstructor import org.stellar.anchor.sep24.Sep24Helper @@ -27,7 +28,7 @@ import org.stellar.sdk.KeyPair class ClientStatusCallbackHandlerTest { private lateinit var handler: ClientStatusCallbackHandler private lateinit var secretConfig: PropertySecretConfig - private lateinit var clientConfig: ClientsConfig.ClientConfig + private lateinit var clientConfig: ClientConfig private lateinit var signer: KeyPair private lateinit var ts: String private lateinit var event: AnchorEvent @@ -39,8 +40,8 @@ class ClientStatusCallbackHandlerTest { @BeforeEach fun setUp() { - clientConfig = ClientsConfig.ClientConfig() - clientConfig.type = ClientsConfig.ClientType.CUSTODIAL + clientConfig = ClientConfig() + clientConfig.type = CUSTODIAL clientConfig.signingKey = "GBI2IWJGR4UQPBIKPP6WG76X5PHSD2QTEBGIP6AZ3ZXWV46ZUSGNEGN2" clientConfig.callbackUrl = "https://callback.circle.com/api/v1/anchor/callback" diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructorTest.kt index 631bf762e3..c987a9a823 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleInteractiveUrlConstructorTest.kt @@ -22,9 +22,12 @@ import org.stellar.anchor.api.callback.PutCustomerResponse import org.stellar.anchor.api.exception.SepValidationException import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.Sep24InteractiveUrlJwt +import org.stellar.anchor.config.ClientsConfig.ClientConfig +import org.stellar.anchor.config.ClientsConfig.ClientType +import org.stellar.anchor.config.ClientsConfig.ClientType.* import org.stellar.anchor.config.CustodySecretConfig import org.stellar.anchor.config.SecretConfig -import org.stellar.anchor.platform.config.ClientsConfig +import org.stellar.anchor.platform.config.PropertyClientsConfig import org.stellar.anchor.platform.config.PropertySep24Config import org.stellar.anchor.platform.data.JdbcSep24Transaction import org.stellar.anchor.platform.service.SimpleInteractiveUrlConstructor.FORWARD_KYC_CUSTOMER_TYPE @@ -40,11 +43,8 @@ class SimpleInteractiveUrlConstructorTest { } @MockK(relaxed = true) private lateinit var secretConfig: SecretConfig - + @MockK(relaxed = true) private lateinit var clientsConfig: PropertyClientsConfig @MockK(relaxed = true) private lateinit var custodySecretConfig: CustodySecretConfig - - @MockK(relaxed = true) private lateinit var clientsConfig: ClientsConfig - @MockK(relaxed = true) private lateinit var customerIntegration: CustomerIntegration private lateinit var jwtService: JwtService @@ -58,9 +58,9 @@ class SimpleInteractiveUrlConstructorTest { every { secretConfig.sep24InteractiveUrlJwtSecret } returns "sep24_jwt_secret" val clientConfig = - ClientsConfig.ClientConfig( + ClientConfig( "lobstr", - ClientsConfig.ClientType.NONCUSTODIAL, + NONCUSTODIAL, "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "lobstr.co", "https://callback.lobstr.co/api/v2/anchor/callback" @@ -74,9 +74,9 @@ class SimpleInteractiveUrlConstructorTest { "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" ) } returns - ClientsConfig.ClientConfig( + ClientConfig( "some-wallet", - ClientsConfig.ClientType.CUSTODIAL, + ClientType.CUSTODIAL, "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", null, null diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt index a2934b8aeb..a67f5a4766 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/SimpleMoreInfoUrlConstructorTest.kt @@ -13,9 +13,11 @@ import org.springframework.web.util.UriComponentsBuilder import org.stellar.anchor.api.exception.SepValidationException import org.stellar.anchor.auth.JwtService import org.stellar.anchor.auth.Sep24MoreInfoUrlJwt +import org.stellar.anchor.config.ClientsConfig.ClientConfig +import org.stellar.anchor.config.ClientsConfig.ClientType.* import org.stellar.anchor.config.CustodySecretConfig import org.stellar.anchor.config.SecretConfig -import org.stellar.anchor.platform.config.ClientsConfig +import org.stellar.anchor.platform.config.PropertyClientsConfig import org.stellar.anchor.platform.config.PropertySep24Config import org.stellar.anchor.platform.data.JdbcSep24Transaction import org.stellar.anchor.util.GsonUtils @@ -26,11 +28,9 @@ class SimpleMoreInfoUrlConstructorTest { } @MockK(relaxed = true) private lateinit var secretConfig: SecretConfig - + @MockK(relaxed = true) private lateinit var clientsConfig: PropertyClientsConfig @MockK(relaxed = true) private lateinit var custodySecretConfig: CustodySecretConfig - @MockK(relaxed = true) private lateinit var clientsConfig: ClientsConfig - private lateinit var jwtService: JwtService @BeforeEach @@ -39,9 +39,9 @@ class SimpleMoreInfoUrlConstructorTest { every { secretConfig.sep24MoreInfoUrlJwtSecret } returns "sep24_jwt_secret" val clientConfig = - ClientsConfig.ClientConfig( + ClientConfig( "lobstr", - ClientsConfig.ClientType.NONCUSTODIAL, + NONCUSTODIAL, "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "lobstr.co", "https://callback.lobstr.co/api/v2/anchor/callback" @@ -55,9 +55,9 @@ class SimpleMoreInfoUrlConstructorTest { "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" ) } returns - ClientsConfig.ClientConfig( + ClientConfig( "some-wallet", - ClientsConfig.ClientType.CUSTODIAL, + CUSTODIAL, "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", null, null diff --git a/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt index 6a0b5bae03..19be24d189 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/sep31/Sep31DepositInfoGeneratorTest.kt @@ -19,7 +19,9 @@ import org.stellar.anchor.api.shared.SepDepositInfo import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.config.AppConfig +import org.stellar.anchor.config.ClientsConfig import org.stellar.anchor.config.CustodyConfig +import org.stellar.anchor.config.Sep10Config import org.stellar.anchor.config.Sep31Config import org.stellar.anchor.custody.CustodyService import org.stellar.anchor.event.EventService @@ -53,9 +55,11 @@ class Sep31DepositInfoGeneratorTest { @MockK(relaxed = true) private lateinit var txnStore: Sep31TransactionStore @MockK(relaxed = true) private lateinit var appConfig: AppConfig + @MockK(relaxed = true) private lateinit var sep10Config: Sep10Config @MockK(relaxed = true) private lateinit var sep31Config: Sep31Config @MockK(relaxed = true) private lateinit var sep31DepositInfoGenerator: Sep31DepositInfoGenerator @MockK(relaxed = true) private lateinit var quoteStore: Sep38QuoteStore + @MockK(relaxed = true) private lateinit var clientsConfig: ClientsConfig @MockK(relaxed = true) private lateinit var feeIntegration: FeeIntegration @MockK(relaxed = true) private lateinit var customerIntegration: CustomerIntegration @MockK(relaxed = true) private lateinit var eventPublishService: EventService @@ -68,13 +72,19 @@ class Sep31DepositInfoGeneratorTest { @BeforeEach fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) + every { sep10Config.allowedClientDomains } returns listOf() + every { sep10Config.isClientAttributionRequired } returns false + every { clientsConfig.getClientConfigByDomain(any()) } returns null + every { clientsConfig.getClientConfigBySigningKey(any()) } returns null sep31Service = Sep31Service( appConfig, + sep10Config, sep31Config, txnStore, sep31DepositInfoGenerator, quoteStore, + clientsConfig, assetService, feeIntegration, customerIntegration, @@ -97,10 +107,12 @@ class Sep31DepositInfoGeneratorTest { sep31Service = Sep31Service( appConfig, + sep10Config, sep31Config, txnStore, Sep31DepositInfoSelfGenerator(), // set deposit info generator quoteStore, + clientsConfig, assetService, feeIntegration, customerIntegration, diff --git a/service-runner/src/main/resources/profiles/default/config.env b/service-runner/src/main/resources/profiles/default/config.env index 9efef89cf5..ebf4a9a296 100644 --- a/service-runner/src/main/resources/profiles/default/config.env +++ b/service-runner/src/main/resources/profiles/default/config.env @@ -48,4 +48,8 @@ clients[0].callback_url=http://wallet-server:8092/callbacks clients[1].name=referenceCustodial clients[1].type=custodial clients[1].signing_key=GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG -clients[1].callback_url=http://wallet-server:8092/callbacks \ No newline at end of file +clients[1].callback_url=http://wallet-server:8092/callbacks +clients[2].name=stellar_anchor_tests +clients[2].type=custodial +clients[2].signing_key=GDOHXZYP5ABGCTKAEROOJFN6X5GY7VQNXFNK2SHSAD32GSVMUJBPG75E +clients[2].callback_url=http://wallet-server:8092/callbacks \ No newline at end of file