Greenfield: Add store rates api (#4550)

* Add store rates api

* Improve doc

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2023-01-31 06:42:25 +01:00 committed by GitHub
parent f821e35cb0
commit aad06c583e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 31 deletions

View File

@ -37,7 +37,7 @@ namespace BTCPayServer.Client
return await HandleResponse<StoreRateConfiguration>(response);
}
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
public virtual async Task<List<StoreRateResult>> PreviewUpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
string[] currencyPair,
CancellationToken token = default)
@ -47,7 +47,18 @@ namespace BTCPayServer.Client
queryPayload: new Dictionary<string, object>() { { "currencyPair", currencyPair } },
method: HttpMethod.Post),
token);
return await HandleResponse<List<StoreRatePreviewResult>>(response);
return await HandleResponse<List<StoreRateResult>>(response);
}
public virtual async Task<List<StoreRateResult>> GetStoreRates(string storeId, string[] currencyPair,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates",
queryPayload: new Dictionary<string, object>() { { "currencyPair", currencyPair } },
method: HttpMethod.Get),
token);
return await HandleResponse<List<StoreRateResult>>(response);
}
}
}

View File

@ -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<string> Errors { get; set; }
}

View File

@ -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<string> Errors { get; set; }
}

View File

@ -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);

View File

@ -85,24 +85,31 @@ namespace BTCPayServer.Controllers.GreenField
[HttpPost("preview")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> PreviewUpdateStoreRateConfiguration(
StoreRateConfiguration configuration, [FromQuery] string[] currencyPair)
StoreRateConfiguration configuration, [FromQuery] string[]? currencyPair)
{
var data = HttpContext.GetStoreData();
var blob = data.GetStoreBlob();
var parsedCurrencyPairs = new HashSet<CurrencyPair>();
foreach (var pair in currencyPair ?? Array.Empty<string>())
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<StoreRatePreviewResult>();
var result = new List<StoreRateResult>();
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(),

View File

@ -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<IActionResult> GetStoreRates([FromQuery] string[]? currencyPair)
{
var data = HttpContext.GetStoreData();
var blob = data.GetStoreBlob();
var parsedCurrencyPairs = new HashSet<CurrencyPair>();
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<StoreRateResult>();
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);
}
}
}

View File

@ -1223,12 +1223,18 @@ namespace BTCPayServer.Controllers.Greenfield
return Task.FromResult(GetFromActionResult<StoreRateConfiguration>(GetController<GreenfieldStoreRateConfigurationController>().GetStoreRateConfiguration()));
}
public override async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
public override async Task<List<StoreRateResult>> GetStoreRates (string storeId,
string[] currencyPair, CancellationToken token = default)
{
return GetFromActionResult<List<StoreRateResult>>(await GetController<GreenfieldStoreRatesController>().GetStoreRates(currencyPair));
}
public override async Task<List<StoreRateResult>> PreviewUpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
string[] currencyPair,
CancellationToken token = default)
{
return GetFromActionResult<List<StoreRatePreviewResult>>(
return GetFromActionResult<List<StoreRateResult>>(
await GetController<GreenfieldStoreRateConfigurationController>().PreviewUpdateStoreRateConfiguration(request,
currencyPair));
}

View File

@ -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"
}
}
}

View File

@ -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"
}
]
}