diff --git a/README.md b/README.md index c38c8ec2..78895f15 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ To integrate stellar SDK into your Xcode project using CocoaPods, specify it in use_frameworks! target '' do - pod 'stellar-ios-mac-sdk', '~> 2.5.3' + pod 'stellar-ios-mac-sdk', '~> 2.5.4' end ``` @@ -44,7 +44,7 @@ $ brew install carthage To integrate stellar-ios-mac-sdk into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "soneso/stellar-ios-mac-sdk" ~> 2.5.3 +github "soneso/stellar-ios-mac-sdk" ~> 2.5.4 ``` Run `carthage update` to build the framework and drag the build `stellar-ios-mac-sdk.framework` into your Xcode project. @@ -52,7 +52,7 @@ Run `carthage update` to build the framework and drag the build `stellar-ios-mac ### Swift Package Manager ```swift -.package(name: "stellarsdk", url: "git@github.com:Soneso/stellar-ios-mac-sdk.git", from: "2.5.3"), +.package(name: "stellarsdk", url: "git@github.com:Soneso/stellar-ios-mac-sdk.git", from: "2.5.4"), ``` ### Manual @@ -666,6 +666,7 @@ Our SDK is also used by the [LOBSTR Wallet](https://lobstr.co). - [SEP-0012](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - Anchor/Client customer info transfer - [SEP-0024](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) - Hosted Deposit and Withdrawal - [SEP-0030](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md) - Account Recovery +- [SEP-0038](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md) - Anchor RFQ API ## Soroban support diff --git a/docs/SEP-0030.md b/docs/SEP-0030.md index 2e57bef0..1ff66d47 100644 --- a/docs/SEP-0030.md +++ b/docs/SEP-0030.md @@ -5,7 +5,7 @@ Enables an individual (e.g., a user or wallet) to regain access to a Stellar acc [SEP-0030: Account Recovery](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md). -## Create a SEP30RecoveryService instance +## Create a RecoveryService instance **By providing the recovery server url directly via the constructor:** diff --git a/docs/SEP-0038.md b/docs/SEP-0038.md new file mode 100644 index 00000000..4f9b3e61 --- /dev/null +++ b/docs/SEP-0038.md @@ -0,0 +1,157 @@ + +# SEP-0038 - Anchor RFQ API + +The [SEP-38](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md) standard defines a way for anchors to provide quotes for the exchange of an off-chain asset and a different on-chain asset, and vice versa. +Quotes may be [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) or [firm](https://www.investopedia.com/terms/f/firmquote.asp) ones. +When either is used is explained in the sections below. + + +## Create a `QuoteService` instance + +**By providing the quote server url directly via the constructor:** + +```swift +quoteService = QuoteService(serviceAddress: "http://api.stellar-anchor.org/quote") +``` + +## Authentication + +Authentication is done using the [Sep-10 WebAuth Service](https://github.com/Soneso/stellar_flutter_sdk/blob/master/documentation/sdk_examples/sep-0010-webauth.md), and we will use the authentication token in the SEP-38 requests. + +## Get Anchor Information + +First, let's get information about the anchor's support for [SEP-38](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md). The response gives what stellar on-chain assets and off-chain assets are available for trading. + +```swift +quoteService.info(jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + let assets = response.assets; + // ... + case .failure(let err): + // ... + } +} +``` + +## Asset Identification Format + +Before calling other endpoints we should understand the scheme used to identify assets in this protocol. The following format is used: + +`:` + +The currently accepted scheme values are `stellar` for Stellar assets, and `iso4217` for fiat currencies. + +For example to identify USDC on Stellar we would use: + +`stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` + +And to identify fiat USD we would use: + +`iso4217:USD` + +Further explanation can be found in [SEP-38 specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#asset-identification-format). + +## Get Prices + +Now let's get [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) prices from the anchor in exchange for a given asset. This is an indicative price. The actual price will be calculated at conversion time once the Anchor receives the funds from a user. + +In our example we're getting prices for selling 5 fiat USD. + +```swift +quoteService.prices(sellAsset: "iso4217:USD", + sellAmount: "5", + jwt: jwtToken) { (response) -> (Void) in + + switch response { + case .success(let response): + let buyAssets = response.buyAssets; + // ... + case .failure(let err): + // ... + } +} +``` + +The response gives the asset prices for exchanging the requested sell asset. + +## Get Prices + +Next, let's get an [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) price for a certain pair. + +Once again this is an indicative value. The actual price will be calculated at conversion time once the Anchor receives the funds from a User. + +Either a `sellAmount` or `buyAmount` value must be given, but not both. And `context` refers to what Stellar SEP context this will be used for (ie. `sep6`, `sep24`, or `sep31``). + +```swift +quoteService.price( + context:"sep6", + sellAsset: "iso4217:USD", + buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", + sellAmount: "5", + jwt: jwtToken) { (response) -> (Void) in + + switch response { + case .success(let response): + let totalPrice = response.totalPrice + // ... + case .failure(let err): + // ... + } +} +``` + +The response gives information for exchanging these assets. + +## Post Quote + +Now let's get a [firm](https://www.investopedia.com/terms/f/firmquote.asp) quote from the anchor. +As opposed to the earlier endpoints, this quote is stored by the anchor for a certain period of time. +We will show how we can grab the quote again later. + +```swift +var request = Sep38PostQuoteRequest( + context: "sep31", + sellAsset: "iso4217:USD", + buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B") + +request.sellAmount = "5" + + +quoteService.postQuote(request: request, jwt: jwtToken) { (response) -> (Void) in + + switch response { + case .success(let response): + let quoteId = response.id; + let expirationDate = response.expiresAt; + let totalPrice = response.totalPrice; + // ... + case .failure(let err): + // ... + } +} +// ... +``` +However now the response gives an `id` that we can use to identify the quote. The `expiresAt` field tells us how long the anchor will wait to receive funds for this quote. + +## Get Quote + +Now let's get the previously requested quote. To do that we use the `id` from the `.postQuote()` response. + +```swift +quoteService.getQuote(id: quoteId, jwt: jwtToken) { (response) -> (Void) in + + switch response { + case .success(let response): + let totalPrice = response.totalPrice; + // ... + case .failure(let err): + // ... + } +} +``` +The response should match the one given from `.postQuote()` we made earlier. + +### Further readings + +For more info, see also the class [QuoteService](https://github.com/Soneso/stellar-ios-mac-sdk/blob/master/stellarsdk/stellarsdk/quote/QuoteService.swift) and the SDK's [SEP-38 test cases](https://github.com/Soneso/stellar-ios-mac-sdk/blob/master/stellarsdk/stellarsdkTests/quote/QuoteServiceTestCase.swift). diff --git a/docs/seps.md b/docs/seps.md index 74570b96..6752022c 100644 --- a/docs/seps.md +++ b/docs/seps.md @@ -14,7 +14,8 @@ This SDK provides implementations of following SEPs: - [SEP-0011](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md) - Txrep - [SEP-0012](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - Anchor/Client customer info transfer - [SEP-0024](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) - Hosted Deposit and Withdrawal - +- [SEP-0030](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md) - Account Recovery +- [SEP-0038](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md) - Anchor RFQ API (Quotes) # Stellar Info File - SEP-0001 @@ -402,4 +403,12 @@ You can find more source code examples regarding SEP-0011 in this [SDK test case # Hosted Deposit and Withdrawal -see [SEP-24 - interactive](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs/SEP-0024.md) \ No newline at end of file +see [SEP-24 - interactive](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs/SEP-0024.md) + +# Account Recovery + +see [SEP-30 - Account Recovery](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs/SEP-0030.md) + +# Quotes + +see [SEP-38 - Anchor RFQ API](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs/SEP-0038.md) \ No newline at end of file diff --git a/stellar-ios-mac-sdk.podspec b/stellar-ios-mac-sdk.podspec index 0a0bbb54..fb08ac36 100644 --- a/stellar-ios-mac-sdk.podspec +++ b/stellar-ios-mac-sdk.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "stellar-ios-mac-sdk" - s.version = "2.5.3" + s.version = "2.5.4" s.summary = "Fully featured iOS and macOS SDK that provides APIs to build transactions and connect to Horizon server for the Stellar ecosystem." s.module_name = 'stellarsdk' s.swift_version = '5.0' diff --git a/stellarsdk/stellarsdk.xcodeproj/project.pbxproj b/stellarsdk/stellarsdk.xcodeproj/project.pbxproj index 01e5033d..71c82134 100644 --- a/stellarsdk/stellarsdk.xcodeproj/project.pbxproj +++ b/stellarsdk/stellarsdk.xcodeproj/project.pbxproj @@ -774,6 +774,26 @@ 83379C2C2280BE2A00BD5F8E /* CreatePassiveSellOfferOperationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83379C262280B64500BD5F8E /* CreatePassiveSellOfferOperationResponse.swift */; }; 83379C2E2280CF9C00BD5F8E /* CreatePassiveSellOfferOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83379C2D2280CF9C00BD5F8E /* CreatePassiveSellOfferOperation.swift */; }; 83379C2F2280CF9C00BD5F8E /* CreatePassiveSellOfferOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83379C2D2280CF9C00BD5F8E /* CreatePassiveSellOfferOperation.swift */; }; + 833C67282B84C85000E034C6 /* QuoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67272B84C85000E034C6 /* QuoteService.swift */; }; + 833C67292B84C85000E034C6 /* QuoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67272B84C85000E034C6 /* QuoteService.swift */; }; + 833C672C2B84C97000E034C6 /* Sep38Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C672B2B84C97000E034C6 /* Sep38Responses.swift */; }; + 833C672D2B84C97000E034C6 /* Sep38Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C672B2B84C97000E034C6 /* Sep38Responses.swift */; }; + 833C67302B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C672F2B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift */; }; + 833C67312B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C672F2B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift */; }; + 833C67342B84D57800E034C6 /* QuoteServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67332B84D57800E034C6 /* QuoteServiceError.swift */; }; + 833C67352B84D57800E034C6 /* QuoteServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67332B84D57800E034C6 /* QuoteServiceError.swift */; }; + 833C67382B84E38F00E034C6 /* QuoteServiceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67372B84E38F00E034C6 /* QuoteServiceTestCase.swift */; }; + 833C67392B84E38F00E034C6 /* QuoteServiceTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67372B84E38F00E034C6 /* QuoteServiceTestCase.swift */; }; + 833C673B2B84E54E00E034C6 /* Sep38InfoResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C673A2B84E54E00E034C6 /* Sep38InfoResponseMock.swift */; }; + 833C673C2B84E54E00E034C6 /* Sep38InfoResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C673A2B84E54E00E034C6 /* Sep38InfoResponseMock.swift */; }; + 833C673E2B84EE0200E034C6 /* Sep38PricesResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C673D2B84EE0200E034C6 /* Sep38PricesResponseMock.swift */; }; + 833C673F2B84EE0200E034C6 /* Sep38PricesResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C673D2B84EE0200E034C6 /* Sep38PricesResponseMock.swift */; }; + 833C67412B84F2FF00E034C6 /* Sep38PriceResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67402B84F2FF00E034C6 /* Sep38PriceResponseMock.swift */; }; + 833C67422B84F2FF00E034C6 /* Sep38PriceResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67402B84F2FF00E034C6 /* Sep38PriceResponseMock.swift */; }; + 833C67442B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67432B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift */; }; + 833C67452B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67432B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift */; }; + 833C67472B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67462B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift */; }; + 833C67482B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C67462B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift */; }; 833CBF8A2033510D00C23CFE /* DecoratedSignatureXDR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833CBF892033510D00C23CFE /* DecoratedSignatureXDR.swift */; }; 833CBF8C2033514C00C23CFE /* OperationMetaXDR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833CBF8B2033514C00C23CFE /* OperationMetaXDR.swift */; }; 833CBF8E203351B200C23CFE /* TimeBoundsXDR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833CBF8D203351B200C23CFE /* TimeBoundsXDR.swift */; }; @@ -1620,6 +1640,16 @@ 83379C242280AE9C00BD5F8E /* ManageBuyOfferOperationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageBuyOfferOperationResponse.swift; sourceTree = ""; }; 83379C262280B64500BD5F8E /* CreatePassiveSellOfferOperationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePassiveSellOfferOperationResponse.swift; sourceTree = ""; }; 83379C2D2280CF9C00BD5F8E /* CreatePassiveSellOfferOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePassiveSellOfferOperation.swift; sourceTree = ""; }; + 833C67272B84C85000E034C6 /* QuoteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteService.swift; sourceTree = ""; }; + 833C672B2B84C97000E034C6 /* Sep38Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38Responses.swift; sourceTree = ""; }; + 833C672F2B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38PostQuoteRequest.swift; sourceTree = ""; }; + 833C67332B84D57800E034C6 /* QuoteServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteServiceError.swift; sourceTree = ""; }; + 833C67372B84E38F00E034C6 /* QuoteServiceTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteServiceTestCase.swift; sourceTree = ""; }; + 833C673A2B84E54E00E034C6 /* Sep38InfoResponseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38InfoResponseMock.swift; sourceTree = ""; }; + 833C673D2B84EE0200E034C6 /* Sep38PricesResponseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38PricesResponseMock.swift; sourceTree = ""; }; + 833C67402B84F2FF00E034C6 /* Sep38PriceResponseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38PriceResponseMock.swift; sourceTree = ""; }; + 833C67432B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38PostQuoteResponseMock.swift; sourceTree = ""; }; + 833C67462B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sep38GetQuoteResponseMock.swift; sourceTree = ""; }; 833CBF892033510D00C23CFE /* DecoratedSignatureXDR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecoratedSignatureXDR.swift; sourceTree = ""; }; 833CBF8B2033514C00C23CFE /* OperationMetaXDR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMetaXDR.swift; sourceTree = ""; }; 833CBF8D203351B200C23CFE /* TimeBoundsXDR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeBoundsXDR.swift; sourceTree = ""; }; @@ -2365,6 +2395,7 @@ 7445E9FE201B106500470E0A /* stellarsdk */ = { isa = PBXGroup; children = ( + 833C67262B84C82800E034C6 /* quote */, 835F0CDD2976016000793708 /* soroban */, 8398C40724D8CA6500D6916C /* txrep */, 74723334219D97EC00343E25 /* web_authentication */, @@ -2401,6 +2432,7 @@ 7467C40221426FAB00412FE1 /* transfer_server_protocol */, 83DB2AF02ACEA948008B04D6 /* interactive */, 832C2B312AEC1097005BB7B8 /* recovery */, + 833C67362B84E34400E034C6 /* quote */, 83F4E61C2A27912900DD42A9 /* kyc */, 744FC6FE212E941F00ADFC88 /* federation */, EC34A15A20504353000FB042 /* wallet */, @@ -2944,6 +2976,54 @@ path = recovery; sourceTree = ""; }; + 833C67262B84C82800E034C6 /* quote */ = { + isa = PBXGroup; + children = ( + 833C67322B84D56300E034C6 /* errors */, + 833C672E2B84CF1D00E034C6 /* request */, + 833C672A2B84C94200E034C6 /* responses */, + 833C67272B84C85000E034C6 /* QuoteService.swift */, + ); + path = quote; + sourceTree = ""; + }; + 833C672A2B84C94200E034C6 /* responses */ = { + isa = PBXGroup; + children = ( + 833C672B2B84C97000E034C6 /* Sep38Responses.swift */, + ); + path = responses; + sourceTree = ""; + }; + 833C672E2B84CF1D00E034C6 /* request */ = { + isa = PBXGroup; + children = ( + 833C672F2B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift */, + ); + path = request; + sourceTree = ""; + }; + 833C67322B84D56300E034C6 /* errors */ = { + isa = PBXGroup; + children = ( + 833C67332B84D57800E034C6 /* QuoteServiceError.swift */, + ); + path = errors; + sourceTree = ""; + }; + 833C67362B84E34400E034C6 /* quote */ = { + isa = PBXGroup; + children = ( + 833C67372B84E38F00E034C6 /* QuoteServiceTestCase.swift */, + 833C673A2B84E54E00E034C6 /* Sep38InfoResponseMock.swift */, + 833C673D2B84EE0200E034C6 /* Sep38PricesResponseMock.swift */, + 833C67402B84F2FF00E034C6 /* Sep38PriceResponseMock.swift */, + 833C67432B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift */, + 833C67462B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift */, + ); + path = quote; + sourceTree = ""; + }; 833CBFBD20337D1600C23CFE /* sdk */ = { isa = PBXGroup; children = ( @@ -3874,6 +3954,7 @@ 745DF5192052FFC3000D3660 /* Constants.swift in Sources */, 745DF51A2052FFC3000D3660 /* DataTypes.swift in Sources */, 83852AB326678E020013EA2A /* FeeRequest.swift in Sources */, + 833C672D2B84C97000E034C6 /* Sep38Responses.swift in Sources */, 745DF51B2052FFC3000D3660 /* EventSource.swift in Sources */, 745DF51C2052FFC3000D3660 /* StellarSDKLog.swift in Sources */, 745DF51D2052FFC3000D3660 /* TransactionsStreamItem.swift in Sources */, @@ -3918,6 +3999,7 @@ 745DF5332052FFC3000D3660 /* StellarSDK.swift in Sources */, 745DF4222052FF9B000D3660 /* Account.swift in Sources */, 83D408EA26EB88A2003A9C0F /* LiquidityPoolsService.swift in Sources */, + 833C67312B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift in Sources */, 8345D41A251E323500E3F6DA /* HashIDPreimageXDR.swift in Sources */, C67811B72148E311001525D8 /* URISchemeValidator.swift in Sources */, 745DF4232052FF9B000D3660 /* TransactionAccount.swift in Sources */, @@ -4063,6 +4145,7 @@ 83DB2AD22ACD661F008B04D6 /* InteractiveServiceError.swift in Sources */, 836658F52A6E777800E8B8D9 /* RestoreFootprintResultXDR.swift in Sources */, 745DF4702052FF9B000D3660 /* PaymentResultXDR.swift in Sources */, + 833C67352B84D57800E034C6 /* QuoteServiceError.swift in Sources */, 83B4CB552527308500EEE55E /* ClaimClaimableBalanceOperation.swift in Sources */, 745DF4712052FF9B000D3660 /* OperationResultXDR.swift in Sources */, 745DF4722052FF9B000D3660 /* CreateAccountResultXDR.swift in Sources */, @@ -4121,6 +4204,7 @@ 830E7CCD252912FB0059A4B4 /* BeginSponsoringFutureReservesOpXDR.swift in Sources */, 835F0CE729770B8B00793708 /* SorobanRpcRequestError.swift in Sources */, 744A84CC21451F560086B8FC /* URLRequest+MultipartFormData.swift in Sources */, + 833C67292B84C85000E034C6 /* QuoteService.swift in Sources */, 830DAB4429AD540200ED4E66 /* GetEventsResponse.swift in Sources */, 7467C3FA2142528700412FE1 /* DepositRequest.swift in Sources */, 745DF4922052FF9C000D3660 /* SignerKeyXDR.swift in Sources */, @@ -4353,6 +4437,7 @@ files = ( 8310BDDD26EB6E75004D3EF2 /* AmmTestCase.swift in Sources */, 743D9C1A21B57246004C56BD /* PostCallbackMock.swift in Sources */, + 833C67392B84E38F00E034C6 /* QuoteServiceTestCase.swift in Sources */, 744FC701212E978500ADFC88 /* FederationTestCase.swift in Sources */, 832C2B492AEC2AC7005BB7B8 /* Sep30ListAccountsResponseMock.swift in Sources */, 83D6D2EB2A27BAD700555110 /* PutVerificationResponseMock.swift in Sources */, @@ -4368,6 +4453,7 @@ 83DB2AFF2ACEC6C8008B04D6 /* Sep24WithdrawResponseMock.swift in Sources */, 745DF56C205304AC000D3660 /* AccountRemoteTestCase.swift in Sources */, 745DF56D205304AC000D3660 /* DataForAccountLocalTestCase.swift in Sources */, + 833C673C2B84E54E00E034C6 /* Sep38InfoResponseMock.swift in Sources */, 83C54D072945F23700CC1E8A /* QuickStartTest.swift in Sources */, 83FE811323F0856600F4A13D /* FeeStatsRemoteTestCase.swift in Sources */, 746D7905219AE2BE0008AA79 /* TomlResponseSignatureMismatchMock.swift in Sources */, @@ -4381,6 +4467,8 @@ 745DF572205304AC000D3660 /* AssetsRemoteTestCase.swift in Sources */, 745DF573205304AC000D3660 /* EffectsLocalTestCase.swift in Sources */, 745DF574205304AC000D3660 /* EffectsResponsesMock.swift in Sources */, + 833C67452B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift in Sources */, + 833C67422B84F2FF00E034C6 /* Sep38PriceResponseMock.swift in Sources */, 7472333B219EE40200343E25 /* WebAuthenticatorTestCase.swift in Sources */, 744A84BE214403480086B8FC /* AnchorInfoResponseMock.swift in Sources */, 745DF575205304AC000D3660 /* EffectsRemoteTestCase.swift in Sources */, @@ -4429,8 +4517,10 @@ 832C2B3D2AEC21CE005BB7B8 /* Sep30UpdateIdentitiesResponseMock.swift in Sources */, 745DF589205304AC000D3660 /* PaymentsResponsesMock.swift in Sources */, 745DF58A205304AC000D3660 /* PaymentsLocalTestCase.swift in Sources */, + 833C67482B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift in Sources */, 745DF58B205304AC000D3660 /* TradesTestCase.swift in Sources */, 745DF58C205304AC000D3660 /* TradesLocalTestCase.swift in Sources */, + 833C673F2B84EE0200E034C6 /* Sep38PricesResponseMock.swift in Sources */, 83707A9D2AB33DEB001790B0 /* WebAuthRemoteTestCase.swift in Sources */, 83BD1EE92949F67D00F6C041 /* IssueAssetTest.swift in Sources */, 745DF58D205304AC000D3660 /* TradesResponsesMock.swift in Sources */, @@ -4532,6 +4622,7 @@ EC34A1602050D24C000FB042 /* Data+Hash.swift in Sources */, 43E34B612031CF09003104A9 /* OfferResponse.swift in Sources */, 83852AB226678E020013EA2A /* FeeRequest.swift in Sources */, + 833C672C2B84C97000E034C6 /* Sep38Responses.swift in Sources */, 836972122028EF8A00398A98 /* AccountThresholdsUpdatedEffectResponse.swift in Sources */, 836971E62024F58E00398A98 /* DataForAccountResponse.swift in Sources */, EC34A13020503CC5000FB042 /* BatchedCollection.swift in Sources */, @@ -4576,6 +4667,7 @@ 744148FA202E0635006590BE /* TransactionXDR.swift in Sources */, 746AD2C020341E0A00A9CF21 /* TransactionResultXDR.swift in Sources */, 83D408E926EB88A2003A9C0F /* LiquidityPoolsService.swift in Sources */, + 833C67302B84CF2F00E034C6 /* Sep38PostQuoteRequest.swift in Sources */, 8345D419251E323500E3F6DA /* HashIDPreimageXDR.swift in Sources */, EC34A11C20503CC5000FB042 /* Int+Extension.swift in Sources */, EC34A12620503CC5000FB042 /* Blowfish+Foundation.swift in Sources */, @@ -4721,6 +4813,7 @@ 83DB2AD12ACD661F008B04D6 /* InteractiveServiceError.swift in Sources */, 836658F42A6E777800E8B8D9 /* RestoreFootprintResultXDR.swift in Sources */, EC34A12B20503CC5000FB042 /* Array+Foundation.swift in Sources */, + 833C67342B84D57800E034C6 /* QuoteServiceError.swift in Sources */, 83B4CB542527308500EEE55E /* ClaimClaimableBalanceOperation.swift in Sources */, 83DD2C6B202DE3DF00713FEE /* ErrorResponse.swift in Sources */, 43D35B0D20348AEA00C8BDBC /* PaymentPathsService.swift in Sources */, @@ -4779,6 +4872,7 @@ 830E7CCC252912FB0059A4B4 /* BeginSponsoringFutureReservesOpXDR.swift in Sources */, 835F0CE629770B8B00793708 /* SorobanRpcRequestError.swift in Sources */, 833CBFE32036F6D500C23CFE /* ChangeTrustOperation.swift in Sources */, + 833C67282B84C85000E034C6 /* QuoteService.swift in Sources */, 830DAB4329AD540200ED4E66 /* GetEventsResponse.swift in Sources */, 743DF56A2020530A00713DE7 /* verify.c in Sources */, 83FE810823F071CE00F4A13D /* FeeStatsResponse.swift in Sources */, @@ -5018,6 +5112,7 @@ 8369723C202A707600398A98 /* ServerMock.swift in Sources */, 8331520B203E2635007F8F84 /* TransactionsLocalTestCase.swift in Sources */, 7472333D219EE4F700343E25 /* WebAuthenticatorTomlResponseMock.swift in Sources */, + 833C67412B84F2FF00E034C6 /* Sep38PriceResponseMock.swift in Sources */, 744C4DC2202A328100B46CDE /* OperationsRemoteTestCase.swift in Sources */, 744E75E42028DBD500170D57 /* EffectsRemoteTestCase.swift in Sources */, 437D8A92202F15DC0024D031 /* TradesTestCase.swift in Sources */, @@ -5034,12 +5129,15 @@ 83F4E61E2A2792C600DD42A9 /* KycServerTestCase.swift in Sources */, 83DD2C9D202F24CD00713FEE /* PaymentsTestCase.swift in Sources */, 43464797203D56AF0082B96A /* OperationsResponsesMock.swift in Sources */, + 833C67442B85152C00E034C6 /* Sep38PostQuoteResponseMock.swift in Sources */, 83AFD636234FC59400A574D2 /* PathPaymentStrictReceiveOperation.swift in Sources */, C62F97E52199BE5B000A835A /* URISchemeTestCase.swift in Sources */, 83BD1EE82949F67D00F6C041 /* IssueAssetTest.swift in Sources */, + 833C673B2B84E54E00E034C6 /* Sep38InfoResponseMock.swift in Sources */, 7472333A219EE40200343E25 /* WebAuthenticatorTestCase.swift in Sources */, 83F4E6212A27ABC300DD42A9 /* GetCustomerResponseMock.swift in Sources */, EC34A15C205043AB000FB042 /* MnemonicGeneration.swift in Sources */, + 833C67472B8517E200E034C6 /* Sep38GetQuoteResponseMock.swift in Sources */, 744A84BD214403480086B8FC /* AnchorInfoResponseMock.swift in Sources */, 83DB2B042ACED370008B04D6 /* Sep24TransactionResponseMock.swift in Sources */, 8357A911203AE7DF00A144A9 /* AccountRemoteTestCase.swift in Sources */, @@ -5050,6 +5148,7 @@ 744A84C821441CC50086B8FC /* Date.swift in Sources */, 832C2B332AEC10C0005BB7B8 /* RecoveryServiceTestCase.swift in Sources */, 7467C40421426FF700412FE1 /* TransferServerTestCase.swift in Sources */, + 833C673E2B84EE0200E034C6 /* Sep38PricesResponseMock.swift in Sources */, 4367E600203ED42D00C0AB4F /* OffersLocalTestCase.swift in Sources */, 83697206202631D600398A98 /* LedgersRemoteTestCase.swift in Sources */, 830A8F0B29AE916300B30483 /* SorobanAtomicSwapTest.swift in Sources */, @@ -5065,6 +5164,7 @@ 835F0CE3297613EF00793708 /* SorobanTest.swift in Sources */, 4367E5FC203EA6ED00C0AB4F /* PaymentsLocalTestCase.swift in Sources */, 43464795203D553B0082B96A /* OperationsLocalTestCase.swift in Sources */, + 833C67382B84E38F00E034C6 /* QuoteServiceTestCase.swift in Sources */, 8357A915203B097D00A144A9 /* AssetsLocalTestCase.swift in Sources */, 832C2B362AEC117C005BB7B8 /* Sep30RegisterAccountResponseMock.swift in Sources */, 830A8F1129AEC3F800B30483 /* SorobanEventsTest.swift in Sources */, diff --git a/stellarsdk/stellarsdk/Info.plist b/stellarsdk/stellarsdk/Info.plist index 5d898b40..b008c2de 100644 --- a/stellarsdk/stellarsdk/Info.plist +++ b/stellarsdk/stellarsdk/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.5.3 + 2.5.4 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/stellarsdk/stellarsdk/kyc/KycAmlFields.swift b/stellarsdk/stellarsdk/kyc/KycAmlFields.swift index c45ae87d..855f887a 100644 --- a/stellarsdk/stellarsdk/kyc/KycAmlFields.swift +++ b/stellarsdk/stellarsdk/kyc/KycAmlFields.swift @@ -68,7 +68,7 @@ public enum KYCNaturalPersonFieldsEnum { /// Date of birth, e.g. 1976-07-04 case birthDate(Date) /// Place of birth (city, state, country; as on passport) - case birthPlace(Date) + case birthPlace(String) /// ISO Code of country of birth case birthCountryCode(String) /// Tax identifier of user in their country (social security number in US) @@ -138,7 +138,7 @@ public enum KYCNaturalPersonFieldsEnum { case .birthDate(let value): return (KYCNaturalPersonFieldKey.birthDate, DateFormatter.iso8601.string(from: value).data(using: .utf8)!) case .birthPlace(let value): - return (KYCNaturalPersonFieldKey.birthPlace, DateFormatter.iso8601.string(from: value).data(using: .utf8)!) + return (KYCNaturalPersonFieldKey.birthPlace, value.data(using: .utf8)!) case .birthCountryCode(let value): return (KYCNaturalPersonFieldKey.birthCountryCode, value.data(using: .utf8)!) case .taxId(let value): diff --git a/stellarsdk/stellarsdk/quote/QuoteService.swift b/stellarsdk/stellarsdk/quote/QuoteService.swift new file mode 100644 index 00000000..95f918e1 --- /dev/null +++ b/stellarsdk/stellarsdk/quote/QuoteService.swift @@ -0,0 +1,266 @@ +// +// QuoteService.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +public enum Sep38InfoResponseEnum { + case success(response: Sep38InfoResponse) + case failure(error: QuoteServiceError) +} + +public enum Sep38PricesResponseEnum { + case success(response: Sep38PricesResponse) + case failure(error: QuoteServiceError) +} + +public enum Sep38PriceResponseEnum { + case success(response: Sep38PriceResponse) + case failure(error: QuoteServiceError) +} + +public enum Sep38QuoteResponseEnum { + case success(response: Sep38QuoteResponse) + case failure(error: QuoteServiceError) +} + +public typealias Sep38InfoResponseClosure = (_ response:Sep38InfoResponseEnum) -> (Void) +public typealias Sep38PricesResponseClosure = (_ response:Sep38PricesResponseEnum) -> (Void) +public typealias Sep38PriceResponseClosure = (_ response:Sep38PriceResponseEnum) -> (Void) +public typealias Sep38QuoteResponseClosure = (_ response:Sep38QuoteResponseEnum) -> (Void) + + +/** + Implements SEP-0038 - Anchor RFQ API + See Anchor RFQ API. + */ +public class QuoteService: NSObject { + + public var serviceAddress: String + private let serviceHelper: ServiceHelper + private let jsonDecoder = JSONDecoder() + + public init(serviceAddress:String) { + self.serviceAddress = serviceAddress + serviceHelper = ServiceHelper(baseURL: serviceAddress) + jsonDecoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) + } + + /** + This endpoint returns the supported Stellar assets and off-chain assets available for trading. + See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#get-info + - Parameter jwt: optional jwt token obtained before with SEP-0010. + */ + public func info(jwt:String? = nil, completion:@escaping Sep38InfoResponseClosure) { + + serviceHelper.GETRequestWithPath(path: "/info", jwtToken: jwt) { (result) -> (Void) in + switch result { + case .success(let data): + do { + let response = try self.jsonDecoder.decode(Sep38InfoResponse.self, from: data) + completion(.success(response:response)) + } catch { + completion(.failure(error: .parsingResponseFailed(message: error.localizedDescription))) + } + case .failure(let error): + completion(.failure(error: self.errorFor(horizonError: error))) + } + } + } + + /** + This endpoint can be used to fetch the indicative prices of available off-chain assets in exchange for a Stellar asset and vice versa. + See GET prices + + - Parameter sellAsset: The asset you want to sell, using the Asset Identification Format. + - Parameter sellAmount: The amount of sell_asset the client would exchange for each of the buy_assets. + - Parameter sellDeliveryMethod: Optional, one of the name values specified by the sell_delivery_methods array for the associated asset returned from GET /info. Can be provided if the user is delivering an off-chain asset to the anchor but is not strictly required. + - Parameter buyDeliveryMethod: Optional, one of the name values specified by the buy_delivery_methods array for the associated asset returned from GET /info. Can be provided if the user intends to receive an off-chain asset from the anchor but is not strictly required. + - Parameter countryCode: Optional, The ISO 3166-2 or ISO-3166-1 alpha-2 code of the user's current address. Should be provided if there are two or more country codes available for the desired asset in GET /info. + - Parameter jwt: optional jwt token obtained before with SEP-0010. + */ + public func prices(sellAsset:String, + sellAmount:String, + sellDeliveryMethod:String? = nil, + buyDeliveryMethod:String? = nil, + countryCode:String? = nil, + jwt:String? = nil, + completion:@escaping Sep38PricesResponseClosure) { + + var requestPath = "/prices?sell_asset=\(sellAsset)&sell_amount=\(sellAmount)" + if let value = sellDeliveryMethod { + requestPath += "&sell_delivery_method=\(value)" + } + if let value = buyDeliveryMethod { + requestPath += "&buy_delivery_method=\(value)" + } + if let value = countryCode { + requestPath += "&country_code=\(value)" + } + + serviceHelper.GETRequestWithPath(path: requestPath, jwtToken: jwt) { (result) -> (Void) in + switch result { + case .success(let data): + do { + let response = try self.jsonDecoder.decode(Sep38PricesResponse.self, from: data) + completion(.success(response:response)) + } catch { + completion(.failure(error: .parsingResponseFailed(message: error.localizedDescription))) + } + case .failure(let error): + completion(.failure(error: self.errorFor(horizonError: error))) + } + } + } + + /** + This endpoint can be used to fetch the indicative price for a given asset pair. + See GET price + + - Parameter context: The context for what this quote will be used for. Must be one of 'sep6' or 'sep31'. + - Parameter sellAsset: The asset the client would like to sell. Ex. stellar:USDC:G..., iso4217:ARS + - Parameter buyAsset: The asset the client would like to exchange for sellAsset. + - Parameter sellAmount: optional, the amount of sellAsset the client would like to exchange for buyAsset. + - Parameter buyAmount: optional, the amount of buyAsset the client would like to exchange for sellAsset. + - Parameter sellDeliveryMethod: optional, one of the name values specified by the sell_delivery_methods array for the associated asset returned from GET /info. Can be provided if the user is delivering an off-chain asset to the anchor but is not strictly required. + - Parameter buyDeliveryMethod: optional, one of the name values specified by the buy_delivery_methods array for the associated asset returned from GET /info. Can be provided if the user intends to receive an off-chain asset from the anchor but is not strictly required. + - Parameter countryCode: optional, The ISO 3166-2 or ISO-3166-1 alpha-2 code of the user's current address. Should be provided if there are two or more country codes available for the desired asset in GET /info. + - Parameter jwt: optional jwt token obtained before with SEP-0010. + */ + public func price(context:String, + sellAsset:String, + buyAsset:String, + sellAmount:String? = nil, + buyAmount:String? = nil, + sellDeliveryMethod:String? = nil, + buyDeliveryMethod:String? = nil, + countryCode:String? = nil, + jwt:String? = nil, + completion:@escaping Sep38PriceResponseClosure) { + + // The caller must provide either sellAmount or buyAmount, but not both. + if ((sellAmount != nil && buyAmount != nil) || (sellAmount == nil && buyAmount == nil)) { + completion(.failure(error: .invalidArgument(message: "The caller must provide either sellAmount or buyAmount, but not both"))) + return + } + + var requestPath = "/price?sell_asset=\(sellAsset)&buy_asset=\(buyAsset)&context=\(context)" + if let value = sellAmount { + requestPath += "&sell_amount=\(value)" + } + if let value = buyAmount { + requestPath += "&buy_amount=\(value)" + } + if let value = sellDeliveryMethod { + requestPath += "&sell_delivery_method=\(value)" + } + if let value = buyDeliveryMethod { + requestPath += "&buy_delivery_method=\(value)" + } + if let value = countryCode { + requestPath += "&country_code=\(value)" + } + + serviceHelper.GETRequestWithPath(path: requestPath, jwtToken: jwt) { (result) -> (Void) in + switch result { + case .success(let data): + do { + let response = try self.jsonDecoder.decode(Sep38PriceResponse.self, from: data) + completion(.success(response:response)) + } catch { + completion(.failure(error: .parsingResponseFailed(message: error.localizedDescription))) + } + case .failure(let error): + completion(.failure(error: self.errorFor(horizonError: error))) + } + } + } + + /** + This endpoint can be used to request a firm quote for a Stellar asset and off-chain asset pair. + See POST quote + + - Parameter request: the request data. + - Parameter jwt: jwt token obtained before with SEP-0010. + */ + public func postQuote(request: Sep38PostQuoteRequest, jwt:String, completion:@escaping Sep38QuoteResponseClosure) { + + // The caller must provide either sellAmount or buyAmount, but not both. + if ((request.sellAmount != nil && request.buyAmount != nil) || + (request.sellAmount == nil && request.buyAmount == nil)) { + completion(.failure(error: .invalidArgument(message: "The caller must provide either sellAmount or buyAmount, but not both"))) + return + } + + let requestData = try! JSONSerialization.data(withJSONObject: request.toJson()) + serviceHelper.POSTRequestWithPath(path: "/quote", jwtToken: jwt, body: requestData, contentType: "application/json") { (result) -> (Void) in + switch result { + case .success(let data): + do { + let response = try self.jsonDecoder.decode(Sep38QuoteResponse.self, from: data) + completion(.success(response:response)) + } catch { + completion(.failure(error: .parsingResponseFailed(message: error.localizedDescription))) + } + case .failure(let error): + completion(.failure(error: self.errorFor(horizonError: error))) + } + } + } + + /** + This endpoint can be used to fetch a previously-provided firm quote by id. + See GET quote + + - Parameter id: the id of the quote. + - Parameter jwt: jwt token obtained before with SEP-0010. + */ + public func getQuote(id:String, jwt:String? = nil, completion:@escaping Sep38QuoteResponseClosure) { + + let requestPath = "/quote/\(id)" + + serviceHelper.GETRequestWithPath(path: requestPath, jwtToken: jwt) { (result) -> (Void) in + switch result { + case .success(let data): + do { + let response = try self.jsonDecoder.decode(Sep38QuoteResponse.self, from: data) + completion(.success(response:response)) + } catch { + completion(.failure(error: .parsingResponseFailed(message: error.localizedDescription))) + } + case .failure(let error): + completion(.failure(error: self.errorFor(horizonError: error))) + } + } + } + + private func errorFor(horizonError:HorizonRequestError) -> QuoteServiceError { + switch horizonError { + case .badRequest(let message, _): + return .badRequest(message: extractErrorMessage(message: message)) + case .forbidden(let message, _): + return .permissionDenied(message: extractErrorMessage(message: message)) + case .notFound(let message, _): + return .notFound(message: extractErrorMessage(message: message)) + default: + return .horizonError(error: horizonError) + } + } + + private func extractErrorMessage(message:String) -> String { + if let data = message.data(using: .utf8) { + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let error = json["error"] as? String { + return error + } + } catch { + return message + } + } + return message + } +} diff --git a/stellarsdk/stellarsdk/quote/errors/QuoteServiceError.swift b/stellarsdk/stellarsdk/quote/errors/QuoteServiceError.swift new file mode 100644 index 00000000..ad637fd7 --- /dev/null +++ b/stellarsdk/stellarsdk/quote/errors/QuoteServiceError.swift @@ -0,0 +1,18 @@ +// +// QuoteServiceError.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +public enum QuoteServiceError: Error { + case invalidArgument(message:String) + case badRequest(message:String) + case permissionDenied(message:String) + case notFound(message:String) + case parsingResponseFailed(message:String) + case horizonError(error: HorizonRequestError) +} diff --git a/stellarsdk/stellarsdk/quote/request/Sep38PostQuoteRequest.swift b/stellarsdk/stellarsdk/quote/request/Sep38PostQuoteRequest.swift new file mode 100644 index 00000000..d9f5128d --- /dev/null +++ b/stellarsdk/stellarsdk/quote/request/Sep38PostQuoteRequest.swift @@ -0,0 +1,57 @@ +// +// Sep38PostQuoteRequest.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +public struct Sep38PostQuoteRequest { + + public var context:String + public var sellAsset:String + public var buyAsset:String + public var sellAmount:String? + public var buyAmount:String? + public var expireAfter:Date? + public var sellDeliveryMethod:String? + public var buyDeliveryMethod:String? + public var countryCode:String? + + + public init(context:String, sellAsset:String, buyAsset:String) { + self.context = context + self.sellAsset = sellAsset + self.buyAsset = buyAsset + } + + public func toJson() -> [String : Any] { + var result = [String : Any](); + result["context"] = context; + result["sell_asset"] = sellAsset; + result["buy_asset"] = buyAsset; + + if let value = sellAmount { + result["sell_amount"] = value; + } + if let value = buyAmount { + result["buy_amount"] = value; + } + if let value = expireAfter { + result["expire_after"] = DateFormatter.iso8601.string(from: value); + } + if let value = sellDeliveryMethod { + result["sell_delivery_method"] = value; + } + if let value = buyDeliveryMethod { + result["buy_delivery_method"] = value; + } + if let value = countryCode { + result["country_code"] = value; + } + return result + + } +} diff --git a/stellarsdk/stellarsdk/quote/responses/Sep38Responses.swift b/stellarsdk/stellarsdk/quote/responses/Sep38Responses.swift new file mode 100644 index 00000000..a5d686bd --- /dev/null +++ b/stellarsdk/stellarsdk/quote/responses/Sep38Responses.swift @@ -0,0 +1,278 @@ +// +// Sep38Responses.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +public struct Sep38InfoResponse: Decodable { + + public var assets: [Sep38Asset] + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case assets + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + assets = try values.decode([Sep38Asset].self, forKey: .assets) + } +} + +public struct Sep38PricesResponse: Decodable { + + public var buyAssets: [Sep38BuyAsset] + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case buyAssets = "buy_assets" + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + buyAssets = try values.decode([Sep38BuyAsset].self, forKey: .buyAssets) + } +} + +public struct Sep38QuoteResponse: Decodable { + + public var id: String + public var expiresAt: Date + public var totalPrice: String + public var price: String + public var sellAsset: String + public var sellAmount: String + public var buyAsset: String + public var buyAmount: String + public var fee: Sep38Fee + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case id + case expiresAt = "expires_at" + case totalPrice = "total_price" + case price + case sellAsset = "sell_asset" + case sellAmount = "sell_amount" + case buyAsset = "buy_asset" + case buyAmount = "buy_amount" + case fee + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + expiresAt = try values.decode(Date.self, forKey: .expiresAt) + totalPrice = try values.decode(String.self, forKey: .totalPrice) + price = try values.decode(String.self, forKey: .price) + sellAsset = try values.decode(String.self, forKey: .sellAsset) + sellAmount = try values.decode(String.self, forKey: .sellAmount) + buyAsset = try values.decode(String.self, forKey: .buyAsset) + buyAmount = try values.decode(String.self, forKey: .buyAmount) + fee = try values.decode(Sep38Fee.self, forKey: .fee) + } +} + +public struct Sep38PriceResponse: Decodable { + + public var totalPrice: String + public var price: String + public var sellAmount: String + public var buyAmount: String + public var fee: Sep38Fee + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case totalPrice = "total_price" + case price + case sellAmount = "sell_amount" + case buyAmount = "buy_amount" + case fee + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + totalPrice = try values.decode(String.self, forKey: .totalPrice) + price = try values.decode(String.self, forKey: .price) + sellAmount = try values.decode(String.self, forKey: .sellAmount) + buyAmount = try values.decode(String.self, forKey: .buyAmount) + fee = try values.decode(Sep38Fee.self, forKey: .fee) + } +} + +public struct Sep38Fee: Decodable { + + public var total: String + public var asset: String + public var details: [Sep38FeeDetails]? + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case total + case asset + case details + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + total = try values.decode(String.self, forKey: .total) + asset = try values.decode(String.self, forKey: .asset) + details = try values.decodeIfPresent([Sep38FeeDetails].self, forKey: .details) + } +} + +public struct Sep38FeeDetails: Decodable { + + public var name: String + public var amount: String + public var description: String? + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case name + case amount + case description + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) + amount = try values.decode(String.self, forKey: .amount) + description = try values.decodeIfPresent(String.self, forKey: .description) + } +} + +public struct Sep38SellDeliveryMethod: Decodable { + + public var name: String + public var description: String + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case name + case description + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) + description = try values.decode(String.self, forKey: .description) + } +} + +public struct Sep38BuyDeliveryMethod: Decodable { + + public var name: String + public var description: String + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case name + case description + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) + description = try values.decode(String.self, forKey: .description) + } +} + +public struct Sep38Asset: Decodable { + + public var asset: String + public var sellDeliveryMethods: [Sep38SellDeliveryMethod]? + public var buyDeliveryMethods: [Sep38BuyDeliveryMethod]? + public var countryCodes: [String]? + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case asset + case sellDeliveryMethods = "sell_delivery_methods" + case buyDeliveryMethods = "buy_delivery_methods" + case countryCodes = "country_codes" + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + asset = try values.decode(String.self, forKey: .asset) + sellDeliveryMethods = try values.decodeIfPresent([Sep38SellDeliveryMethod].self, forKey: .sellDeliveryMethods) + buyDeliveryMethods = try values.decodeIfPresent([Sep38BuyDeliveryMethod].self, forKey: .buyDeliveryMethods) + countryCodes = try values.decodeIfPresent([String].self, forKey: .countryCodes) + } +} + +public struct Sep38BuyAsset: Decodable { + + public var asset: String + public var price: String + public var decimals: Int + + /// Properties to encode and decode + private enum CodingKeys: String, CodingKey { + case asset + case price + case decimals + } + + /** + Initializer - creates a new instance by decoding from the given decoder. + + - Parameter decoder: The decoder containing the data + */ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + asset = try values.decode(String.self, forKey: .asset) + price = try values.decode(String.self, forKey: .price) + decimals = try values.decode(Int.self, forKey: .decimals) + } +} diff --git a/stellarsdk/stellarsdkTests/quote/QuoteServiceTestCase.swift b/stellarsdk/stellarsdkTests/quote/QuoteServiceTestCase.swift new file mode 100644 index 00000000..727e147c --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/QuoteServiceTestCase.swift @@ -0,0 +1,272 @@ +// +// QuoteServiceTestCase.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import XCTest +import stellarsdk + +class QuoteServiceTestCase: XCTestCase { + let quoteServer = "127.0.0.1" + let jwtToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJHQTZVSVhYUEVXWUZJTE5VSVdBQzM3WTRRUEVaTVFWREpIREtWV0ZaSjJLQ1dVQklVNUlYWk5EQSIsImp0aSI6IjE0NGQzNjdiY2IwZTcyY2FiZmRiZGU2MGVhZTBhZDczM2NjNjVkMmE2NTg3MDgzZGFiM2Q2MTZmODg1MTkwMjQiLCJpc3MiOiJodHRwczovL2ZsYXBweS1iaXJkLWRhcHAuZmlyZWJhc2VhcHAuY29tLyIsImlhdCI6MTUzNDI1Nzk5NCwiZXhwIjoxNTM0MzQ0Mzk0fQ.8nbB83Z6vGBgC1X9r3N6oQCFTBzDiITAfCJasRft0z0"; + + var quoteService: QuoteService! + var sep38InfoMock: Sep38InfoResponseMock! + var sep38PricesMock: Sep38PricesResponseMock! + var sep38PriceMock: Sep38PriceResponseMock! + var sep38PostQuoteMock: Sep38PostQuoteResponseMock! + var sep38GetQuoteMock: Sep38GetQuoteResponseMock! + + override func setUp() { + super.setUp() + + URLProtocol.registerClass(ServerMock.self) + sep38InfoMock = Sep38InfoResponseMock(host: quoteServer) + sep38PricesMock = Sep38PricesResponseMock(host: quoteServer) + sep38PriceMock = Sep38PriceResponseMock(host: quoteServer) + sep38PostQuoteMock = Sep38PostQuoteResponseMock(host: quoteServer) + sep38GetQuoteMock = Sep38GetQuoteResponseMock(host: quoteServer) + quoteService = QuoteService(serviceAddress: "http://\(quoteServer)") + + } + + func testGetInfo() { + let expectation = XCTestExpectation(description: "Test sep38 get info") + + quoteService.info(jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + XCTAssertEqual(3, response.assets.count) + let assets = response.assets + XCTAssertEqual("stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", assets[0].asset) + XCTAssertEqual("stellar:BRL:GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP", assets[1].asset) + XCTAssertEqual("iso4217:BRL", assets[2].asset) + XCTAssertNotNil(assets[2].countryCodes) + XCTAssertEqual(1, assets[2].countryCodes?.count) + XCTAssertEqual("BRA", assets[2].countryCodes?[0]) + XCTAssertNotNil(assets[2].sellDeliveryMethods) + XCTAssertEqual(3, assets[2].sellDeliveryMethods?.count) + XCTAssertEqual("cash", assets[2].sellDeliveryMethods?[0].name) + XCTAssertEqual("Deposit cash BRL at one of our agent locations.", assets[2].sellDeliveryMethods?[0].description) + XCTAssertEqual("ACH", assets[2].sellDeliveryMethods?[1].name) + XCTAssertEqual("Send BRL directly to the Anchor's bank account.", assets[2].sellDeliveryMethods?[1].description) + XCTAssertEqual("PIX", assets[2].sellDeliveryMethods?[2].name) + XCTAssertEqual("Send BRL directly to the Anchor's bank account.", assets[2].sellDeliveryMethods?[2].description) + XCTAssertNotNil(assets[2].buyDeliveryMethods) + XCTAssertEqual(3, assets[2].buyDeliveryMethods?.count) + XCTAssertEqual("ACH", assets[2].buyDeliveryMethods?[1].name) + XCTAssertEqual("Have BRL sent directly to your bank account.", assets[2].buyDeliveryMethods?[1].description) + XCTAssertEqual("PIX", assets[2].buyDeliveryMethods?[2].name) + XCTAssertEqual("Have BRL sent directly to the account of your choice.", assets[2].buyDeliveryMethods?[2].description) + case .failure(let err): + XCTFail(err.localizedDescription) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + func testGetPrices() { + let expectation = XCTestExpectation(description: "Test sep38 get prices") + + let sellAsset = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + let sellAmount = "100" + let countryCode = "BRA" + let buyDeliveryMethod = "ACH" + + quoteService.prices(sellAsset: sellAsset, + sellAmount: sellAmount, + buyDeliveryMethod: buyDeliveryMethod, + countryCode: countryCode, + jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + XCTAssertEqual(1, response.buyAssets.count) + let buyAssets = response.buyAssets + XCTAssertEqual(1, buyAssets.count) + XCTAssertEqual("iso4217:BRL", buyAssets[0].asset) + XCTAssertEqual("0.18", buyAssets[0].price) + XCTAssertEqual(2, buyAssets[0].decimals) + case .failure(let err): + XCTFail(err.localizedDescription) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + func testGetPrice() { + let expectation = XCTestExpectation(description: "Test sep38 get price") + + let sellAsset = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + let buyAsset = "iso4217:BRL" + let buyAmount = "500" + let buyDeliveryMethod = "PIX" + let countryCode = "BRA" + let context = "sep31" + + quoteService.price(context:context, + sellAsset: sellAsset, + buyAsset: buyAsset, + buyAmount: buyAmount, + buyDeliveryMethod: buyDeliveryMethod, + countryCode: countryCode, + jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + XCTAssertEqual("0.20", response.totalPrice) + XCTAssertEqual("0.18", response.price) + XCTAssertEqual("100", response.sellAmount) + XCTAssertEqual("500", response.buyAmount) + XCTAssertNotNil(response.fee) + let fee = response.fee + XCTAssertEqual("10.00", fee.total) + XCTAssertEqual("stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", fee.asset) + XCTAssertNotNil(fee.details) + let feeDetails = fee.details + XCTAssertEqual(2, feeDetails?.count) + XCTAssertEqual("Service fee", feeDetails?[0].name) + XCTAssertNil(feeDetails?[0].description) + XCTAssertEqual("5.00", feeDetails?[0].amount) + XCTAssertEqual("PIX fee", feeDetails?[1].name) + XCTAssertEqual("Fee charged in order to process the outgoing BRL PIX transaction.", feeDetails?[1].description) + XCTAssertEqual("5.00", feeDetails?[1].amount) + case .failure(let err): + XCTFail(err.localizedDescription) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + func testGetPriceErr1() { + let expectation = XCTestExpectation(description: "Test sep38 get price") + + let sellAsset = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + let buyAsset = "iso4217:BRL" + let buyAmount = "500" + let buyDeliveryMethod = "PIX" + let countryCode = "BRA" + let context = "sep31" + + quoteService.price(context:context, + sellAsset: sellAsset, + buyAsset: buyAsset, + sellAmount: "100", + buyAmount: buyAmount, + buyDeliveryMethod: buyDeliveryMethod, + countryCode: countryCode, + jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(_): + XCTFail("should not succeed") + case .failure(let err): + switch err{ + case .invalidArgument(let message): + XCTAssertEqual("The caller must provide either sellAmount or buyAmount, but not both", message) + default: + XCTFail() + } + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + func testGetPriceErr2() { + let expectation = XCTestExpectation(description: "Test sep38 get price") + + let sellAsset = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + let buyAsset = "iso4217:BRL" + let buyDeliveryMethod = "PIX" + let countryCode = "BRA" + let context = "sep31" + + quoteService.price(context:context, + sellAsset: sellAsset, + buyAsset: buyAsset, + buyDeliveryMethod: buyDeliveryMethod, + countryCode: countryCode, + jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(_): + XCTFail("should not succeed") + case .failure(let err): + switch err{ + case .invalidArgument(let message): + XCTAssertEqual("The caller must provide either sellAmount or buyAmount, but not both", message) + default: + XCTFail() + } + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + func testPostQuote() { + let expectation = XCTestExpectation(description: "Test sep38 post quote") + + let sellAsset = "iso4217:BRL" + let buyAsset = "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + + + var request = Sep38PostQuoteRequest(context: "sep31", sellAsset: sellAsset, buyAsset: buyAsset) + request.buyAmount = "100" + request.expireAfter = Date.now + request.sellDeliveryMethod = "PIX" + request.countryCode = "BRA" + + quoteService.postQuote(request: request, jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + XCTAssertEqual("de762cda-a193-4961-861e-57b31fed6eb3", response.id) + XCTAssertEqual("5.42", response.totalPrice) + XCTAssertEqual("5.00", response.price) + XCTAssertEqual(request.sellAsset, response.sellAsset) + XCTAssertEqual(request.buyAsset, response.buyAsset) + XCTAssertEqual("542", response.sellAmount) + XCTAssertEqual(request.buyAmount, response.buyAmount) + + case .failure(let err): + XCTFail(err.localizedDescription) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } + + + func testGetQuote() { + let expectation = XCTestExpectation(description: "Test sep38 get quote") + + quoteService.getQuote(id: "de762cda-a193-4961-861e-57b31fed6eb3", jwt: jwtToken) { (response) -> (Void) in + switch response { + case .success(let response): + XCTAssertEqual("de762cda-a193-4961-861e-57b31fed6eb3", response.id) + XCTAssertEqual("5.42", response.totalPrice) + XCTAssertEqual("5.00", response.price) + XCTAssertEqual("iso4217:BRL", response.sellAsset) + XCTAssertEqual("stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", response.buyAsset) + XCTAssertEqual("542", response.sellAmount) + XCTAssertEqual("100", response.buyAmount) + + case .failure(let err): + XCTFail(err.localizedDescription) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 15.0) + } +} diff --git a/stellarsdk/stellarsdkTests/quote/Sep38GetQuoteResponseMock.swift b/stellarsdk/stellarsdkTests/quote/Sep38GetQuoteResponseMock.swift new file mode 100644 index 00000000..d129dcfc --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/Sep38GetQuoteResponseMock.swift @@ -0,0 +1,65 @@ +// +// Sep38GetQuoteResponseMock.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +class Sep38GetQuoteResponseMock: ResponsesMock { + var host: String + + init(host:String) { + self.host = host + + super.init() + } + + override func requestMock() -> RequestMock { + let handler: MockHandler = { [weak self] mock, request in + mock.statusCode = 200 + return self?.success + } + + return RequestMock(host: host, + path: "/quote/de762cda-a193-4961-861e-57b31fed6eb3", + httpMethod: "GET", + mockHandler: handler) + } + + let success = """ + { + "id": "de762cda-a193-4961-861e-57b31fed6eb3", + "expires_at": "2021-04-30T07:42:23", + "total_price": "5.42", + "price": "5.00", + "sell_asset": "iso4217:BRL", + "sell_amount": "542", + "buy_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "buy_amount": "100", + "fee": { + "total": "42.00", + "asset": "iso4217:BRL", + "details": [ + { + "name": "PIX fee", + "description": "Fee charged in order to process the outgoing PIX transaction.", + "amount": "12.00" + }, + { + "name": "Brazilian conciliation fee", + "description": "Fee charged in order to process conciliation costs with intermediary banks.", + "amount": "15.00" + }, + { + "name": "Service fee", + "amount": "15.00" + } + ] + } + } + """ + +} diff --git a/stellarsdk/stellarsdkTests/quote/Sep38InfoResponseMock.swift b/stellarsdk/stellarsdkTests/quote/Sep38InfoResponseMock.swift new file mode 100644 index 00000000..fcffe5c0 --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/Sep38InfoResponseMock.swift @@ -0,0 +1,77 @@ +// +// Sep38InfoResponseMock.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +class Sep38InfoResponseMock: ResponsesMock { + var host: String + + init(host:String) { + self.host = host + + super.init() + } + + override func requestMock() -> RequestMock { + let handler: MockHandler = { [weak self] mock, request in + mock.statusCode = 200 + return self?.success + } + + return RequestMock(host: host, + path: "/info", + httpMethod: "GET", + mockHandler: handler) + } + + let success = """ + { + "assets": [ + { + "asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + { + "asset": "stellar:BRL:GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP" + }, + { + "asset": "iso4217:BRL", + "country_codes": ["BRA"], + "sell_delivery_methods": [ + { + "name": "cash", + "description": "Deposit cash BRL at one of our agent locations." + }, + { + "name": "ACH", + "description": "Send BRL directly to the Anchor's bank account." + }, + { + "name": "PIX", + "description": "Send BRL directly to the Anchor's bank account." + } + ], + "buy_delivery_methods": [ + { + "name": "cash", + "description": "Pick up cash BRL at one of our payout locations." + }, + { + "name": "ACH", + "description": "Have BRL sent directly to your bank account." + }, + { + "name": "PIX", + "description": "Have BRL sent directly to the account of your choice." + } + ] + } + ] + } + """ + +} diff --git a/stellarsdk/stellarsdkTests/quote/Sep38PostQuoteResponseMock.swift b/stellarsdk/stellarsdkTests/quote/Sep38PostQuoteResponseMock.swift new file mode 100644 index 00000000..72cfe2b5 --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/Sep38PostQuoteResponseMock.swift @@ -0,0 +1,69 @@ +// +// Sep38PostQuoteResponseMock.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +class Sep38PostQuoteResponseMock: ResponsesMock { + var host: String + + init(host:String) { + self.host = host + + super.init() + } + + override func requestMock() -> RequestMock { + let handler: MockHandler = { [weak self] mock, request in + if let data = request.httpBodyStream?.readfully() { + let body = String(decoding: data, as: UTF8.self) + print(body) + } + mock.statusCode = 200 + return self?.success + } + + return RequestMock(host: host, + path: "/quote", + httpMethod: "POST", + mockHandler: handler) + } + + let success = """ + { + "id": "de762cda-a193-4961-861e-57b31fed6eb3", + "expires_at": "2021-04-30T07:42:23", + "total_price": "5.42", + "price": "5.00", + "sell_asset": "iso4217:BRL", + "sell_amount": "542", + "buy_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "buy_amount": "100", + "fee": { + "total": "42.00", + "asset": "iso4217:BRL", + "details": [ + { + "name": "PIX fee", + "description": "Fee charged in order to process the outgoing PIX transaction.", + "amount": "12.00" + }, + { + "name": "Brazilian conciliation fee", + "description": "Fee charged in order to process conciliation costs with intermediary banks.", + "amount": "15.00" + }, + { + "name": "Service fee", + "amount": "15.00" + } + ] + } + } + """ + +} diff --git a/stellarsdk/stellarsdkTests/quote/Sep38PriceResponseMock.swift b/stellarsdk/stellarsdkTests/quote/Sep38PriceResponseMock.swift new file mode 100644 index 00000000..8c523e8d --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/Sep38PriceResponseMock.swift @@ -0,0 +1,55 @@ +// +// Sep38PriceResponseMock.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +class Sep38PriceResponseMock: ResponsesMock { + var host: String + + init(host:String) { + self.host = host + + super.init() + } + + override func requestMock() -> RequestMock { + let handler: MockHandler = { [weak self] mock, request in + mock.statusCode = 200 + return self?.success + } + + return RequestMock(host: host, + path: "/price", + httpMethod: "GET", + mockHandler: handler) + } + + let success = """ + { + "total_price": "0.20", + "price": "0.18", + "sell_amount": "100", + "buy_amount": "500", + "fee": { + "total": "10.00", + "asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "details": [ + { + "name": "Service fee", + "amount": "5.00" + }, + { + "name": "PIX fee", + "description": "Fee charged in order to process the outgoing BRL PIX transaction.", + "amount": "5.00" + } + ] + } + } + """ +} diff --git a/stellarsdk/stellarsdkTests/quote/Sep38PricesResponseMock.swift b/stellarsdk/stellarsdkTests/quote/Sep38PricesResponseMock.swift new file mode 100644 index 00000000..0af25e3f --- /dev/null +++ b/stellarsdk/stellarsdkTests/quote/Sep38PricesResponseMock.swift @@ -0,0 +1,43 @@ +// +// Sep38PricesResponseMock.swift +// stellarsdk +// +// Created by Christian Rogobete on 20.02.24. +// Copyright © 2024 Soneso. All rights reserved. +// + +import Foundation + +class Sep38PricesResponseMock: ResponsesMock { + var host: String + + init(host:String) { + self.host = host + + super.init() + } + + override func requestMock() -> RequestMock { + let handler: MockHandler = { [weak self] mock, request in + mock.statusCode = 200 + return self?.success + } + + return RequestMock(host: host, + path: "/prices", + httpMethod: "GET", + mockHandler: handler) + } + + let success = """ + { + "buy_assets": [ + { + "asset": "iso4217:BRL", + "price": "0.18", + "decimals": 2 + } + ] + } + """ +}