mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Greenfield: Store Rates Config (#3931)
* Greenfield: Store Rates Config * FIX SWAGGER * rebase fix * Apply suggestions from code review Co-authored-by: d11n <mail@dennisreimann.de> * Update BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json Co-authored-by: d11n <mail@dennisreimann.de> * Fix: Spread isn't converted from/to percentage, rename some fields, and move some routes * Fix error handling Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
a2fa688cde
commit
434298cba6
12 changed files with 693 additions and 1 deletions
|
@ -0,0 +1,53 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<RateSource>> GetRateSources(
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"misc/rate-sources", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<List<RateSource>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", bodyPayload: request,
|
||||
method: HttpMethod.Put),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
string[] currencyPair,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration/preview", bodyPayload: request,
|
||||
queryPayload: new Dictionary<string, object>() {{"currencyPair", currencyPair}},
|
||||
method: HttpMethod.Post),
|
||||
token);
|
||||
return await HandleResponse<List<StoreRatePreviewResult>>(response);
|
||||
}
|
||||
}
|
||||
}
|
7
BTCPayServer.Client/Models/RateSource.cs
Normal file
7
BTCPayServer.Client/Models/RateSource.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class RateSource
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
10
BTCPayServer.Client/Models/StoreRateConfiguration.cs
Normal file
10
BTCPayServer.Client/Models/StoreRateConfiguration.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreRateConfiguration
|
||||
{
|
||||
public decimal Spread { get; set; }
|
||||
public bool IsCustomScript { get; set; }
|
||||
public string EffectiveScript { get; set; }
|
||||
public string PreferredSource { get; set; }
|
||||
}
|
||||
}
|
10
BTCPayServer.Client/Models/StoreRatePreviewResult.cs
Normal file
10
BTCPayServer.Client/Models/StoreRatePreviewResult.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
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; }
|
||||
}
|
7
BTCPayServer.Client/Models/StoreRateResult.cs
Normal file
7
BTCPayServer.Client/Models/StoreRateResult.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class StoreRateResult
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
|
@ -2701,8 +2701,67 @@ namespace BTCPayServer.Tests
|
|||
Assert.NotNull(custodians);
|
||||
Assert.NotEmpty(custodians);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreRateConfigTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(401, async () => await unauthClient.GetRateSources());
|
||||
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
var clientBasic = await user.CreateClient();
|
||||
Assert.NotEmpty(await clientBasic.GetRateSources());
|
||||
var config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.False(config.IsCustomScript);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
Assert.Equal("coingecko", config.PreferredSource);
|
||||
|
||||
Assert.Equal(0.9m,
|
||||
Assert.Single(await clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() {IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1;", Spread = 10m,},
|
||||
new[] {"BTC_XYZ"})).Rate);
|
||||
|
||||
Assert.True((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m,}))
|
||||
.IsCustomScript);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.NotNull(config.EffectiveScript);
|
||||
Assert.Equal("BTC_XYZ = 1;", config.EffectiveScript);
|
||||
Assert.Equal(10m, config.Spread);
|
||||
Assert.Null(config.PreferredSource);
|
||||
|
||||
Assert.NotNull((await clientBasic.GetStoreRateConfiguration(user.StoreId)).EffectiveScript);
|
||||
Assert.NotNull((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko"}))
|
||||
.PreferredSource);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript", "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" }));
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ rg8w*# 1;" }));
|
||||
await AssertValidationError(new[] { "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "", PreferredSource = "coingecko" }));
|
||||
|
||||
await AssertValidationError(new[] { "PreferredSource", "Spread" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
|
||||
|
||||
await AssertValidationError(new[] { "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodianAccountControllerTests()
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RateSource = BTCPayServer.Client.Models.RateSource;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates/configuration")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenfieldStoreRateConfigurationController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public GreenfieldStoreRateConfigurationController(
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_rateProviderFactory = rateProviderFactory;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public IActionResult GetStoreRateConfiguration()
|
||||
{
|
||||
var data = HttpContext.GetStoreData();
|
||||
var blob = data.GetStoreBlob();
|
||||
|
||||
return Ok(new StoreRateConfiguration()
|
||||
{
|
||||
EffectiveScript = blob.GetRateRules(_btcPayNetworkProvider, out var preferredExchange).ToString(),
|
||||
Spread = blob.Spread * 100.0m,
|
||||
IsCustomScript = blob.RateScripting,
|
||||
PreferredSource = preferredExchange ? blob.PreferredExchange : null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/misc/rate-sources")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
|
||||
public ActionResult<List<RateSource>> GetRateSources()
|
||||
{
|
||||
return Ok(_rateProviderFactory.RateProviderFactory.GetSupportedExchanges().Select(provider =>
|
||||
new RateSource() {Id = provider.Id, Name = provider.DisplayName}));
|
||||
}
|
||||
|
||||
[HttpPut("")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdateStoreRateConfiguration(
|
||||
StoreRateConfiguration configuration)
|
||||
{
|
||||
var storeData = HttpContext.GetStoreData();
|
||||
var blob = storeData.GetStoreBlob();
|
||||
ValidateAndSanitizeConfiguration(configuration, blob);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
PopulateBlob(configuration, blob);
|
||||
|
||||
storeData.SetStoreBlob(blob);
|
||||
|
||||
await _storeRepository.UpdateStore(storeData);
|
||||
|
||||
|
||||
return GetStoreRateConfiguration();
|
||||
}
|
||||
|
||||
[HttpPost("preview")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> PreviewUpdateStoreRateConfiguration(
|
||||
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.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);
|
||||
}
|
||||
ValidateAndSanitizeConfiguration(configuration, blob);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
PopulateBlob(configuration, blob);
|
||||
|
||||
var rules = blob.GetRateRules(_btcPayNetworkProvider);
|
||||
|
||||
|
||||
var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None);
|
||||
await Task.WhenAll(rateTasks.Values);
|
||||
var result = new List<StoreRatePreviewResult>();
|
||||
foreach (var rateTask in rateTasks)
|
||||
{
|
||||
var rateTaskResult = rateTask.Value.Result;
|
||||
|
||||
result.Add(new StoreRatePreviewResult()
|
||||
{
|
||||
CurrencyPair = rateTask.Key.ToString(),
|
||||
Errors = rateTaskResult.Errors.Select(errors => errors.ToString()).ToList(),
|
||||
Rate = rateTaskResult.Errors.Any() ? (decimal?)null : rateTaskResult.BidAsk.Bid
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private void ValidateAndSanitizeConfiguration(StoreRateConfiguration? configuration, StoreBlob storeBlob)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
ModelState.AddModelError("", "Body required");
|
||||
return;
|
||||
}
|
||||
if (configuration.Spread < 0 || configuration.Spread > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.Spread),
|
||||
$"Spread value must be in %, between 0 and 100");
|
||||
}
|
||||
|
||||
if (configuration.IsCustomScript)
|
||||
{
|
||||
if (string.IsNullOrEmpty(configuration.EffectiveScript))
|
||||
{
|
||||
configuration.EffectiveScript = storeBlob.GetDefaultRateRules(_btcPayNetworkProvider).ToString();
|
||||
}
|
||||
|
||||
if (!RateRules.TryParse(configuration.EffectiveScript, out var r))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.EffectiveScript),
|
||||
$"Script syntax is invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuration.EffectiveScript = r.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"You can't set the preferredSource if you are using custom scripts");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration.EffectiveScript))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.EffectiveScript),
|
||||
$"You can't set the effectiveScript if you aren't using custom scripts");
|
||||
}
|
||||
if (string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"The preferredSource is required if you aren't using custom scripts");
|
||||
}
|
||||
|
||||
configuration.PreferredSource = _rateProviderFactory
|
||||
.RateProviderFactory
|
||||
.GetSupportedExchanges()
|
||||
.FirstOrDefault(s =>
|
||||
s.Id.Equals(configuration.PreferredSource,
|
||||
StringComparison.InvariantCultureIgnoreCase))?.Id;
|
||||
|
||||
if (string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"Unsupported source, please check /misc/rate-sources to see valid values ({configuration.PreferredSource})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateBlob(StoreRateConfiguration configuration, StoreBlob storeBlob)
|
||||
{
|
||||
storeBlob.PreferredExchange = configuration.PreferredSource;
|
||||
storeBlob.Spread = configuration.Spread / 100.0m;
|
||||
storeBlob.RateScripting = configuration.IsCustomScript;
|
||||
storeBlob.RateScript = configuration.EffectiveScript;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1121,5 +1121,31 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
{
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||
}
|
||||
|
||||
public override Task<List<RateSource>> GetRateSources(CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(GetFromActionResult(GetController<GreenfieldStoreRateConfigurationController>().GetRateSources()));
|
||||
}
|
||||
|
||||
public override Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(GetFromActionResult<StoreRateConfiguration>(GetController<GreenfieldStoreRateConfigurationController>().GetStoreRateConfiguration()));
|
||||
}
|
||||
|
||||
public override async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
string[] currencyPair,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<List<StoreRatePreviewResult>>(
|
||||
await GetController<GreenfieldStoreRateConfigurationController>().PreviewUpdateStoreRateConfiguration(request,
|
||||
currencyPair));
|
||||
}
|
||||
|
||||
public override async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId, StoreRateConfiguration request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,15 +131,21 @@ namespace BTCPayServer.Data
|
|||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
return GetRateRules(networkProvider, out _);
|
||||
}
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider, out bool preferredSource)
|
||||
{
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
preferredSource = true;
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredSource = false;
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
@ -9,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace BTCPayServer.Security.Greenfield
|
||||
{
|
||||
|
@ -31,7 +34,25 @@ namespace BTCPayServer.Security.Greenfield
|
|||
_identityOptions = identityOptions;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
// This one deserve some explanation...
|
||||
// Some routes have this authorization.
|
||||
// [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
|
||||
// This is meant for API routes that we wish to access by greenfield but also via the browser for documentation purpose (say /misc/rate-sources)
|
||||
// Now, if we aren't logged nor authenticated via greenfield, the AuthenticationHandlers get challenged.
|
||||
// The last handler to be challenged is the CookieAuthenticationHandler, which instruct to handle the challenge as a redirection to
|
||||
// the login page.
|
||||
// But this isn't what we want when we call the API programmatically, instead we want an error 401 with a json error message.
|
||||
// This hack modify a request's header to trick the CookieAuthenticationHandler to not do a redirection.
|
||||
if (!Request.Headers.Accept.Any(s => s.StartsWith("text/html", StringComparison.OrdinalIgnoreCase)))
|
||||
Request.Headers.XRequestedWith = new Microsoft.Extensions.Primitives.StringValues("XMLHttpRequest");
|
||||
return base.HandleChallengeAsync(properties);
|
||||
}
|
||||
private bool IsJson(string contentType)
|
||||
{
|
||||
return contentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true;
|
||||
}
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey))
|
||||
|
|
|
@ -1,5 +1,42 @@
|
|||
{
|
||||
"paths": {
|
||||
"/misc/rate-sources": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Miscelleneous"
|
||||
],
|
||||
"summary": "Get available rate sources",
|
||||
"description": "View available rate providers that you can use in stores",
|
||||
"operationId": "GetRateSources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "rate providers array",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The id of the rate provider"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the rate provider"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": []
|
||||
}
|
||||
},
|
||||
"/misc/permissions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
{
|
||||
"paths": {
|
||||
"/api/v1/stores/{storeId}/rates/configuration": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stores (Rates Config)"
|
||||
],
|
||||
"summary": "Get store rate settings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View rate settings of the specified store",
|
||||
"operationId": "Stores_GetStoreRateConfiguration",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified store rate settings",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StoreRateConfiguration"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Stores (Rates Config)"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "Update store rate settings",
|
||||
"description": "Update a store's rate settings",
|
||||
"operationId": "Stores_UpdateStoreRateConfiguration",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StoreRateConfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-position": 1
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The settings were updated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StoreRateConfiguration"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when updating 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.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/rates/configuration/preview": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Stores (Rates Config)"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "currencyPair",
|
||||
"description": "The currency pairs to preview",
|
||||
"in": "query",
|
||||
"style": "form",
|
||||
"explode": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-position": 1
|
||||
}
|
||||
],
|
||||
"summary": "Preview rate configuration results",
|
||||
"description": "Preview rate configuration results before you set it on the store",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StoreRateConfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-position": 1
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The settings were executed and a preview was returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/StoreRatePreviewResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"StoreRateConfiguration": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"spread": {
|
||||
"type": "string",
|
||||
"description": "A spread applies to the rate fetched in `%`. Must be `>= 0` or `<= 100`"
|
||||
},
|
||||
"preferredSource": {
|
||||
"type": "string",
|
||||
"description": "If `isCustomerScript` is `false` affect use this source in the default's `effectiveScript`, if `isCustomerScript` is `false`, this setting is set to `null`. (See /misc/rate-sources for the available sources)"
|
||||
},
|
||||
"isCustomScript": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to use `preferredSource` with default script or a custom `effectiveScript`."
|
||||
},
|
||||
"effectiveScript": {
|
||||
"type": "string",
|
||||
"description": "When `isCustomScript` is `true`, this represent the custom script used to calculate a currency pair's exchange rate. Else, it represent the script generated by the default rules and `preferredSource`."
|
||||
}
|
||||
}
|
||||
},
|
||||
"StoreRatePreviewResult": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currencyPair": {
|
||||
"type": "string",
|
||||
"description": "Currency pair in the format of BTC_USD"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Errors relating to this currency pair fetching based on your config"
|
||||
},
|
||||
"rate": {
|
||||
"type": "string",
|
||||
"description": "the rate fetched based on th currency pair"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Stores (Rates Config)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue