diff --git a/BTCPayServer.Client/BTCPayServerClient.StoreRatesConfiguration.cs b/BTCPayServer.Client/BTCPayServerClient.StoreRatesConfiguration.cs index c36028136..80c1ec7d5 100644 --- a/BTCPayServer.Client/BTCPayServerClient.StoreRatesConfiguration.cs +++ b/BTCPayServer.Client/BTCPayServerClient.StoreRatesConfiguration.cs @@ -37,7 +37,7 @@ namespace BTCPayServer.Client return await HandleResponse(response); } - public virtual async Task> PreviewUpdateStoreRateConfiguration(string storeId, + public virtual async Task> PreviewUpdateStoreRateConfiguration(string storeId, StoreRateConfiguration request, string[] currencyPair, CancellationToken token = default) @@ -47,7 +47,18 @@ namespace BTCPayServer.Client queryPayload: new Dictionary() { { "currencyPair", currencyPair } }, method: HttpMethod.Post), token); - return await HandleResponse>(response); + return await HandleResponse>(response); + } + + public virtual async Task> GetStoreRates(string storeId, string[] currencyPair, + CancellationToken token = default) + { + using var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/rates", + queryPayload: new Dictionary() { { "currencyPair", currencyPair } }, + method: HttpMethod.Get), + token); + return await HandleResponse>(response); } } } diff --git a/BTCPayServer.Client/Models/StoreRatePreviewResult.cs b/BTCPayServer.Client/Models/StoreRatePreviewResult.cs deleted file mode 100644 index 937c291da..000000000 --- a/BTCPayServer.Client/Models/StoreRatePreviewResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Client.Models; - -public class StoreRatePreviewResult -{ - public string CurrencyPair { get; set; } - public decimal? Rate { get; set; } - public List Errors { get; set; } -} diff --git a/BTCPayServer.Client/Models/StoreRateResult.cs b/BTCPayServer.Client/Models/StoreRateResult.cs index 26bcf330d..eddc22714 100644 --- a/BTCPayServer.Client/Models/StoreRateResult.cs +++ b/BTCPayServer.Client/Models/StoreRateResult.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; + namespace BTCPayServer.Client.Models; public class StoreRateResult { public string CurrencyPair { get; set; } - public decimal Rate { get; set; } + public decimal? Rate { get; set; } + public List Errors { get; set; } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 855f77e29..a4333b510 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -3594,6 +3594,9 @@ namespace BTCPayServer.Tests new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m, })) .IsCustomScript); + Assert.Equal(0.9m, + Assert.Single(await clientBasic.GetStoreRates(user.StoreId, new[] { "BTC_XYZ" })).Rate); + config = await clientBasic.GetStoreRateConfiguration(user.StoreId); Assert.NotNull(config); Assert.NotNull(config.EffectiveScript); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs index 1e345cde3..bb006fff9 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs @@ -85,24 +85,31 @@ namespace BTCPayServer.Controllers.GreenField [HttpPost("preview")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public async Task PreviewUpdateStoreRateConfiguration( - StoreRateConfiguration configuration, [FromQuery] string[] currencyPair) + StoreRateConfiguration configuration, [FromQuery] string[]? currencyPair) { var data = HttpContext.GetStoreData(); var blob = data.GetStoreBlob(); var parsedCurrencyPairs = new HashSet(); - - foreach (var pair in currencyPair ?? Array.Empty()) + if (currencyPair?.Any() is true) { - if (!CurrencyPair.TryParse(pair, out var currencyPairParsed)) + foreach (var pair in currencyPair) { - ModelState.AddModelError(nameof(currencyPair), - $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); - break; - } + if (!CurrencyPair.TryParse(pair, out var currencyPairParsed)) + { + ModelState.AddModelError(nameof(currencyPair), + $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); + break; + } - parsedCurrencyPairs.Add(currencyPairParsed); + parsedCurrencyPairs.Add(currencyPairParsed); + } } + else + { + parsedCurrencyPairs = blob.DefaultCurrencyPairs.ToHashSet(); + } + ValidateAndSanitizeConfiguration(configuration, blob); if (!ModelState.IsValid) return this.CreateValidationError(ModelState); @@ -113,12 +120,12 @@ namespace BTCPayServer.Controllers.GreenField var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None); await Task.WhenAll(rateTasks.Values); - var result = new List(); + var result = new List(); foreach (var rateTask in rateTasks) { var rateTaskResult = rateTask.Value.Result; - result.Add(new StoreRatePreviewResult() + result.Add(new StoreRateResult() { CurrencyPair = rateTask.Key.ToString(), Errors = rateTaskResult.Errors.Select(errors => errors.ToString()).ToList(), diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs new file mode 100644 index 000000000..00c781de8 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs @@ -0,0 +1,84 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Route("api/v1/stores/{storeId}/rates")] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public class GreenfieldStoreRatesController : ControllerBase + { + private readonly RateFetcher _rateProviderFactory; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + + public GreenfieldStoreRatesController( + RateFetcher rateProviderFactory, + BTCPayNetworkProvider btcPayNetworkProvider) + { + _rateProviderFactory = rateProviderFactory; + _btcPayNetworkProvider = btcPayNetworkProvider; + } + + [HttpGet("")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task GetStoreRates([FromQuery] string[]? currencyPair) + { + var data = HttpContext.GetStoreData(); + var blob = data.GetStoreBlob(); + var parsedCurrencyPairs = new HashSet(); + + if (currencyPair?.Any() is true) + { + foreach (var pair in currencyPair) + { + if (!CurrencyPair.TryParse(pair, out var currencyPairParsed)) + { + ModelState.AddModelError(nameof(currencyPair), + $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); + break; + } + + parsedCurrencyPairs.Add(currencyPairParsed); + } + } + else + { + parsedCurrencyPairs = blob.DefaultCurrencyPairs.ToHashSet(); + } + + + var rules = blob.GetRateRules(_btcPayNetworkProvider); + + + var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None); + await Task.WhenAll(rateTasks.Values); + var result = new List(); + foreach (var rateTask in rateTasks) + { + var rateTaskResult = rateTask.Value.Result; + + result.Add(new StoreRateResult() + { + CurrencyPair = rateTask.Key.ToString(), + Errors = rateTaskResult.Errors.Select(errors => errors.ToString()).ToList(), + Rate = rateTaskResult.Errors.Any() ? null : rateTaskResult.BidAsk.Bid + }); + } + + return Ok(result); + } + + } +} diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 2b1964d4a..a328fbf15 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -1223,12 +1223,18 @@ namespace BTCPayServer.Controllers.Greenfield return Task.FromResult(GetFromActionResult(GetController().GetStoreRateConfiguration())); } - public override async Task> PreviewUpdateStoreRateConfiguration(string storeId, + public override async Task> GetStoreRates (string storeId, + string[] currencyPair, CancellationToken token = default) + { + return GetFromActionResult>(await GetController().GetStoreRates(currencyPair)); + } + + public override async Task> PreviewUpdateStoreRateConfiguration(string storeId, StoreRateConfiguration request, string[] currencyPair, CancellationToken token = default) { - return GetFromActionResult>( + return GetFromActionResult>( await GetController().PreviewUpdateStoreRateConfiguration(request, currencyPair)); } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json index 51ecfd772..da1bae3f7 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json @@ -165,7 +165,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/StoreRatePreviewResult" + "$ref": "#/components/schemas/StoreRateResult" } } } @@ -220,13 +220,14 @@ } } }, - "StoreRatePreviewResult": { + "StoreRateResult": { "type": "object", "additionalProperties": false, "properties": { "currencyPair": { "type": "string", - "description": "Currency pair in the format of BTC_USD" + "example": "BTC_USD", + "description": "Currency pair in the format of `BTC_USD`" }, "errors": { "type": "array", @@ -237,8 +238,9 @@ "description": "Errors relating to this currency pair fetching based on your config" }, "rate": { - "type": "string", - "description": "the rate fetched based on th currency pair" + "type": "number", + "example": 24520.23, + "description": "the rate fetched based on the currency pair" } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates.json new file mode 100644 index 000000000..db82a98f1 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates.json @@ -0,0 +1,84 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/rates": { + "get": { + "tags": [ + "Stores (Rates)" + ], + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "currencyPair", + "description": "The currency pairs to fetch rates for", + "example": [ "BTC_USD", "BTC_EUR" ], + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "summary": "Get rates", + "description": "Get rates on the store", + "operationId": "Stores_GetStoreRates", + + "responses": { + "200": { + "description": "The settings were executed and a preview was returned", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StoreRateResult" + } + } + } + } + }, + "400": { + "description": "A list of errors that occurred when previewing the settings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to modify the store" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.store.canviewstoresettings" + ], + "Basic": [] + } + ] + } + } + }, + "tags": [ + { + "name": "Stores (Rates)", + "description": "Store Rates operations" + } + ] +}