Compare commits
5 Commits
torproxy
...
gf-dev-doc
Author | SHA1 | Date | |
---|---|---|---|
6729827645 | |||
6c828a29ec | |||
34239dc383 | |||
6af3b4a51d | |||
16afca8058 |
@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="5.0.40" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
|
||||
<PackageReference Include="NBitcoin" Version="5.0.39" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.1.0.22" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Client
|
||||
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default)
|
||||
@ -33,7 +33,7 @@ namespace BTCPayServer.Client
|
||||
if (apikey == null)
|
||||
throw new ArgumentNullException(nameof(apikey));
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
|
||||
@ -61,9 +61,9 @@ namespace BTCPayServer.Client
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
|
||||
|
@ -4,7 +4,6 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
@ -27,7 +26,7 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
|
||||
@ -63,9 +62,9 @@ namespace BTCPayServer.Client
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
|
||||
|
@ -31,7 +31,7 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
||||
|
@ -26,7 +26,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}", method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreData> CreateStore(CreateStoreRequest request, CancellationToken token = default)
|
||||
|
@ -43,25 +43,14 @@ namespace BTCPayServer.Client
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
protected async Task HandleResponse(HttpResponseMessage message)
|
||||
protected void HandleResponse(HttpResponseMessage message)
|
||||
{
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); ;
|
||||
throw new GreenFieldValidationException(err);
|
||||
}
|
||||
else if (message.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
|
||||
throw new GreenFieldAPIException(err);
|
||||
}
|
||||
|
||||
message.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
|
||||
{
|
||||
await HandleResponse(message);
|
||||
HandleResponse(message);
|
||||
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public class GreenFieldAPIException : Exception
|
||||
{
|
||||
public GreenFieldAPIException(Models.GreenfieldAPIError error):base(error.Message)
|
||||
{
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
APIError = error;
|
||||
}
|
||||
public Models.GreenfieldAPIError APIError { get; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public class GreenFieldValidationException : Exception
|
||||
{
|
||||
public GreenFieldValidationException(Models.GreenfieldValidationError[] errors) : base(BuildMessage(errors))
|
||||
{
|
||||
ValidationErrors = errors;
|
||||
}
|
||||
|
||||
private static string BuildMessage(GreenfieldValidationError[] errors)
|
||||
{
|
||||
if (errors == null)
|
||||
throw new ArgumentNullException(nameof(errors));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
builder.AppendLine($"{error.Path}: {error.Message}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public Models.GreenfieldValidationError[] ValidationErrors { get; }
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class NodeUriJsonConverter : JsonConverter<NodeInfo>
|
||||
{
|
||||
public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new JsonObjectException(reader.Path, "Unexpected token type for NodeUri");
|
||||
if (NodeInfo.TryParse((string)reader.Value, out var info))
|
||||
return info;
|
||||
throw new JsonObjectException(reader.Path, "Invalid NodeUri");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is NodeInfo)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ConnectToNodeRequest
|
||||
{
|
||||
public ConnectToNodeRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public ConnectToNodeRequest(NodeInfo nodeInfo)
|
||||
{
|
||||
NodeURI = nodeInfo;
|
||||
}
|
||||
[JsonConverter(typeof(NodeUriJsonConverter))]
|
||||
[JsonProperty("nodeURI")]
|
||||
public NodeInfo NodeURI { get; set; }
|
||||
public string NodeInfo { get; set; }
|
||||
public string NodeId { get; set; }
|
||||
public string NodeHost { get; set; }
|
||||
public int NodePort { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
@ -8,20 +7,9 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateLightningInvoiceRequest
|
||||
{
|
||||
public CreateLightningInvoiceRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
|
||||
{
|
||||
Amount = amount;
|
||||
Description = description;
|
||||
Expiry = expiry;
|
||||
}
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class GreenfieldAPIError
|
||||
{
|
||||
public GreenfieldAPIError()
|
||||
{
|
||||
|
||||
}
|
||||
public GreenfieldAPIError(string code, string message)
|
||||
{
|
||||
if (code == null)
|
||||
throw new ArgumentNullException(nameof(code));
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class GreenfieldValidationError
|
||||
{
|
||||
public GreenfieldValidationError()
|
||||
{
|
||||
|
||||
}
|
||||
public GreenfieldValidationError(string path, string message)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
Path = path;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -10,21 +10,19 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
|
||||
public LightningInvoiceStatus Status { get; set; }
|
||||
|
||||
[JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney AmountReceived { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LightningNodeInformationData
|
||||
{
|
||||
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
|
||||
public NodeInfo[] NodeURIs { get; set; }
|
||||
public IEnumerable<string> NodeInfoList { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
|
||||
@ -22,10 +21,10 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Capacity { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney LocalBalance { get; set; }
|
||||
|
||||
public string ChannelPoint { get; set; }
|
||||
|
@ -1,5 +1,3 @@
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
@ -9,13 +7,11 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class OpenLightningChannelRequest
|
||||
{
|
||||
[JsonConverter(typeof(NodeUriJsonConverter))]
|
||||
[JsonProperty("nodeURI")]
|
||||
public NodeInfo NodeURI { get; set; }
|
||||
[JsonConverter(typeof(MoneyJsonConverter))]
|
||||
public ConnectToNodeRequest Node { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))]
|
||||
public Money ChannelAmount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FeeRateJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PayLightningInvoiceRequest
|
||||
{
|
||||
[Newtonsoft.Json.JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
public string Invoice { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -57,14 +57,14 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
public class Permission
|
||||
{
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
public static Permission Create(string policy, string storeId = null)
|
||||
{
|
||||
if (TryCreatePermission(policy, scope, out var r))
|
||||
if (TryCreatePermission(policy, storeId, out var r))
|
||||
return r;
|
||||
throw new ArgumentException("Invalid Permission");
|
||||
}
|
||||
|
||||
public static bool TryCreatePermission(string policy, string scope, out Permission permission)
|
||||
public static bool TryCreatePermission(string policy, string storeId, out Permission permission)
|
||||
{
|
||||
permission = null;
|
||||
if (policy == null)
|
||||
@ -72,9 +72,9 @@ namespace BTCPayServer.Client
|
||||
policy = policy.Trim().ToLowerInvariant();
|
||||
if (!Policies.IsValidPolicy(policy))
|
||||
return false;
|
||||
if (scope != null && !Policies.IsStorePolicy(policy))
|
||||
if (storeId != null && !Policies.IsStorePolicy(policy))
|
||||
return false;
|
||||
permission = new Permission(policy, scope);
|
||||
permission = new Permission(policy, storeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -108,10 +108,10 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
}
|
||||
|
||||
internal Permission(string policy, string scope)
|
||||
internal Permission(string policy, string storeId)
|
||||
{
|
||||
Policy = policy;
|
||||
Scope = scope;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
public bool Contains(Permission subpermission)
|
||||
@ -125,7 +125,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
if (!Policies.IsStorePolicy(subpermission.Policy))
|
||||
return true;
|
||||
return Scope == null || subpermission.Scope == this.Scope;
|
||||
return StoreId == null || subpermission.StoreId == this.StoreId;
|
||||
}
|
||||
|
||||
public static IEnumerable<Permission> ToPermissions(string[] permissions)
|
||||
@ -161,14 +161,14 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
}
|
||||
|
||||
public string Scope { get; }
|
||||
public string StoreId { get; }
|
||||
public string Policy { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Scope != null)
|
||||
if (StoreId != null)
|
||||
{
|
||||
return $"{Policy}:{Scope}";
|
||||
return $"{Policy}:{StoreId}";
|
||||
}
|
||||
return Policy;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
@ -192,7 +192,7 @@ namespace BTCPayServer.Tests
|
||||
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
|
||||
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
|
||||
var unrestricted = Permission.Create(Policies.Unrestricted, null);
|
||||
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
|
||||
var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Policies.CanModifyStoreSettings);
|
||||
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
|
||||
{
|
||||
var resultStores =
|
||||
@ -202,11 +202,11 @@ namespace BTCPayServer.Tests
|
||||
foreach (var selectiveStorePermission in selectiveStorePermissions)
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/{selectiveStorePermission.Scope}/can-edit",
|
||||
$"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
Assert.Contains(resultStores,
|
||||
data => data.Id.Equals(selectiveStorePermission.Scope, StringComparison.InvariantCultureIgnoreCase));
|
||||
data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
bool shouldBeAuthorized = false;
|
||||
|
@ -147,10 +147,6 @@ namespace BTCPayServer.Tests
|
||||
config.AppendLine($"socksendpoint={SocksEndpoint}");
|
||||
config.AppendLine($"debuglog=debug.log");
|
||||
|
||||
if (SocksHTTPProxy is string v)
|
||||
{
|
||||
config.AppendLine($"sockshttpproxy={v}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
|
||||
config.AppendLine($"sshpassword={SSHPassword}");
|
||||
@ -295,8 +291,6 @@ namespace BTCPayServer.Tests
|
||||
public string SSHPassword { get; internal set; }
|
||||
public string SSHKeyFile { get; internal set; }
|
||||
public string SSHConnection { get; set; }
|
||||
public string SocksHTTPProxy { get; set; }
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -17,7 +16,6 @@ using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Org.BouncyCastle.Utilities.Collections;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
@ -107,13 +105,13 @@ namespace BTCPayServer.Tests
|
||||
tester.PayTester.DisableRegistration = true;
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertValidationError(new[] { "Email", "Password" },
|
||||
await AssertHttpError(400,
|
||||
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
|
||||
await AssertValidationError(new[] { "Password" },
|
||||
await AssertHttpError(400,
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test@gmail.com"}));
|
||||
// Pass too simple
|
||||
await AssertValidationError(new[] { "Password" },
|
||||
await AssertHttpError(400,
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"}));
|
||||
|
||||
@ -125,7 +123,7 @@ namespace BTCPayServer.Tests
|
||||
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"});
|
||||
|
||||
// Duplicate email
|
||||
await AssertValidationError(new[] { "Email" },
|
||||
await AssertHttpError(400,
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}));
|
||||
|
||||
@ -254,18 +252,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AssertValidationError(string[] fields, Func<Task> act)
|
||||
{
|
||||
var remainingFields = fields.ToHashSet();
|
||||
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray());
|
||||
remainingFields.Remove(field);
|
||||
}
|
||||
Assert.Empty(remainingFields);
|
||||
}
|
||||
|
||||
private async Task AssertHttpError(int code, Func<Task> act)
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
|
||||
@ -317,17 +303,17 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
Assert.NotNull(newUser2);
|
||||
|
||||
await AssertValidationError(new[] { "Email" }, async () =>
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
|
||||
await AssertValidationError(new[] { "Password" }, async () =>
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await clientServer.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",}));
|
||||
|
||||
await AssertValidationError(new[] { "Email" }, async () =>
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await clientServer.CreateUser(
|
||||
new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()}));
|
||||
}
|
||||
@ -389,16 +375,16 @@ namespace BTCPayServer.Tests
|
||||
//create payment request
|
||||
|
||||
//validation errors
|
||||
await AssertValidationError(new[] { "Amount", "Currency" }, async () =>
|
||||
await AssertHttpError(400, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"});
|
||||
});
|
||||
await AssertValidationError(new[] { "Amount" }, async () =>
|
||||
await AssertHttpError(400, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0});
|
||||
});
|
||||
await AssertValidationError(new[] { "Currency" }, async () =>
|
||||
await AssertHttpError(400, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
|
||||
|
@ -56,7 +56,6 @@ namespace BTCPayServer.Tests
|
||||
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
|
||||
// since in dev we already can have some users / stores registered, while on CI database is being initalized
|
||||
// for the first time and first registered user gets admin status by default
|
||||
SocksHTTPProxy = GetEnvironment("TESTS_SOCKSHTTP", "http://127.0.0.1:8118/"),
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
|
||||
};
|
||||
@ -97,7 +96,7 @@ namespace BTCPayServer.Tests
|
||||
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
PayTester.UseLightning = true;
|
||||
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
|
||||
}
|
||||
|
@ -77,8 +77,8 @@ namespace BTCPayServer.Tests
|
||||
return p;
|
||||
}).GroupBy(permission => permission.Policy).Select(p =>
|
||||
{
|
||||
var stores = p.Where(permission => !string.IsNullOrEmpty(permission.Scope))
|
||||
.Select(permission => permission.Scope).ToList();
|
||||
var stores = p.Where(permission => !string.IsNullOrEmpty(permission.StoreId))
|
||||
.Select(permission => permission.StoreId).ToList();
|
||||
return new ManageController.AddApiKeyViewModel.PermissionValueItem()
|
||||
{
|
||||
Permission = p.Key,
|
||||
@ -252,39 +252,23 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public bool IsAdmin { get; internal set; }
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
|
||||
}
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
|
@ -64,7 +64,6 @@ using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using TwentyTwenty.Storage;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -743,91 +742,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUseLightningAPI()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.ActivateLightning();
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
|
||||
|
||||
var merchant = tester.NewAccount();
|
||||
merchant.GrantAccess(true);
|
||||
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
|
||||
var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}");
|
||||
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60)));
|
||||
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var client = await user.CreateClient("btcpay.server.canuseinternallightningnode");
|
||||
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||
Assert.Contains("503", err.Message);
|
||||
// Not permission for the store!
|
||||
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
Assert.Contains("403", err.Message);
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
Expiry = TimeSpan.FromSeconds(400),
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||
Assert.Contains("403", err.Message);
|
||||
|
||||
var data = await client.GetLightningNodeChannels(user.StoreId, "BTC");
|
||||
Assert.Equal(2, data.Count());
|
||||
BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest);
|
||||
|
||||
invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
Expiry = TimeSpan.FromSeconds(400),
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
|
||||
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
|
||||
|
||||
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = merchantInvoice.BOLT11
|
||||
});
|
||||
await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = "lol"
|
||||
}));
|
||||
|
||||
var validationErr = await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = -1,
|
||||
Expiry = TimeSpan.FromSeconds(-1),
|
||||
Description = null
|
||||
}));
|
||||
Assert.Equal(2, validationErr.ValidationErrors.Length);
|
||||
|
||||
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
|
||||
Assert.NotNull(invoice.PaidAt);
|
||||
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
|
||||
// Amount received might be bigger because of internal implementation shit from lightning
|
||||
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
|
||||
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
}
|
||||
}
|
||||
|
||||
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
|
||||
{
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
@ -989,16 +903,12 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Theory(Timeout = TestTimeout)]
|
||||
[Xunit.InlineData(true)]
|
||||
[Xunit.InlineData(false)]
|
||||
public async Task CanUseTorClient(bool useInternalTorSocksProxy)
|
||||
public async Task CanUseTorClient()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
if (useInternalTorSocksProxy)
|
||||
tester.PayTester.SocksHTTPProxy = null;
|
||||
await tester.StartAsync();
|
||||
var proxy = tester.PayTester.GetService<Socks5HttpProxyServer>();
|
||||
void AssertConnectionDropped()
|
||||
@ -3697,34 +3607,6 @@ normal:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CheckOnionlocationForNonOnionHtmlRequests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var url = tester.PayTester.ServerUri.AbsoluteUri;
|
||||
|
||||
// check onion location is present for HTML page request
|
||||
using var htmlRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||
htmlRequest.Headers.TryAddWithoutValidation("Accept", "text/html,*/*");
|
||||
|
||||
var htmlResponse = await tester.PayTester.HttpClient.SendAsync(htmlRequest);
|
||||
htmlResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(htmlResponse.Headers.TryGetValues("Onion-Location", out var onionLocation));
|
||||
Assert.StartsWith("http://wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", onionLocation.FirstOrDefault() ?? "no-onion-location-header");
|
||||
|
||||
// no onion location for other mime types
|
||||
using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||
otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*");
|
||||
|
||||
var otherResponse = await tester.PayTester.HttpClient.SendAsync(otherRequest);
|
||||
otherResponse.EnsureSuccessStatusCode();
|
||||
Assert.False(otherResponse.Headers.Contains("Onion-Location"));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
|
@ -12,7 +12,6 @@ services:
|
||||
environment:
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_SOCKSHTTP: http://tor:8118/
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
@ -263,7 +262,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.10.1-beta
|
||||
image: btcpayserver/lnd:v0.9.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -283,7 +282,7 @@ services:
|
||||
debuglevel=debug
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "35531:8080"
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
@ -293,7 +292,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.10.1-beta
|
||||
image: btcpayserver/lnd:v0.9.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -313,7 +312,7 @@ services:
|
||||
debuglevel=debug
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "35532:8080"
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
@ -329,13 +328,9 @@ services:
|
||||
container_name: tor
|
||||
environment:
|
||||
TOR_PASSWORD: btcpayserver
|
||||
TOR_EXTRA_ARGS: |
|
||||
CookieAuthentication 1
|
||||
HTTPTunnelPort 0.0.0.0:8118
|
||||
ports:
|
||||
- "9050:9050" # SOCKS
|
||||
- "9051:9051" # Tor Control
|
||||
- "8118:8118" # HTTP Proxy
|
||||
volumes:
|
||||
- "tor_datadir:/home/tor/.tor"
|
||||
- "torrcdir:/usr/local/etc/tor"
|
||||
|
@ -30,7 +30,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.0" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.15" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Configuration
|
||||
private set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
public Uri SocksHttpProxy { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
@ -171,11 +171,7 @@ namespace BTCPayServer.Configuration
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
var socksuri = conf.GetOrDefault<Uri>("sockshttpproxy", null);
|
||||
if (socksuri != null)
|
||||
{
|
||||
SocksHttpProxy = socksuri;
|
||||
}
|
||||
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
|
@ -44,7 +44,6 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sockshttpproxy", "Socks v5 HTTP proxy, this is currently used only for sending payjoin to tor endpoints. You can get a socks http proxy with the HTTPTunnelPort setting on Tor 0.3.2.x. (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public async Task<ActionResult<ApiKeyData>> CreateKey(CreateApiKeyRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
return NotFound();
|
||||
return BadRequest();
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
|
||||
@ -74,7 +74,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public async Task<IActionResult> RevokeKey(string apikey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(apikey))
|
||||
return NotFound();
|
||||
return BadRequest();
|
||||
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
|
||||
return Ok();
|
||||
else
|
||||
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
public static class GreenFieldUtils
|
||||
{
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
|
||||
foreach (var error in modelState)
|
||||
{
|
||||
foreach (var errorMessage in error.Value.Errors)
|
||||
{
|
||||
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
|
||||
}
|
||||
}
|
||||
return controller.UnprocessableEntity(errors.ToArray());
|
||||
}
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[LightningUnavailableExceptionFilter]
|
||||
public class InternalLightningNodeApiController : LightningNodeApiController
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/address")]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/address")]
|
||||
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
{
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
|
@ -17,7 +17,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[LightningUnavailableExceptionFilter]
|
||||
public class StoreLightningNodeApiController : LightningNodeApiController
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
@ -66,7 +65,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
|
||||
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
{
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
@ -82,7 +81,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/{id}")]
|
||||
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
{
|
||||
return base.GetInvoice(cryptoCode, id);
|
||||
|
@ -5,30 +5,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
public class LightningUnavailableExceptionFilter : Attribute, IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is NBitcoin.JsonConverters.JsonObjectException jsonObject)
|
||||
{
|
||||
context.Result = new ObjectResult(new GreenfieldValidationError(jsonObject.Path, jsonObject.Message));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Result = new StatusCodeResult(503);
|
||||
}
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
public abstract class LightningNodeApiController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
@ -50,12 +32,21 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var info = await lightningClient.GetInfo();
|
||||
return Ok(new LightningNodeInformationData()
|
||||
|
||||
try
|
||||
{
|
||||
BlockHeight = info.BlockHeight,
|
||||
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
|
||||
});
|
||||
var info = await lightningClient.GetInfo();
|
||||
return Ok(new LightningNodeInformationData()
|
||||
{
|
||||
BlockHeight = info.BlockHeight,
|
||||
NodeInfoList = info.NodeInfoList.Select(nodeInfo => nodeInfo.ToString())
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
|
||||
@ -66,23 +57,24 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request?.NodeURI is null)
|
||||
if (TryGetNodeInfo(request, out var nodeInfo))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to connect to");
|
||||
ModelState.AddModelError(nameof(request.NodeId), "A valid node info was not provided to connect to");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
if (CheckValidation(out var errorActionResult))
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return errorActionResult;
|
||||
}
|
||||
|
||||
var result = await lightningClient.ConnectTo(request.NodeURI);
|
||||
switch (result)
|
||||
try
|
||||
{
|
||||
case ConnectionResult.Ok:
|
||||
return Ok();
|
||||
case ConnectionResult.CouldNotConnect:
|
||||
return this.CreateAPIError("could-not-connect", "Could not connect to the remote node");
|
||||
await lightningClient.ConnectTo(nodeInfo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
@ -96,19 +88,27 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var channels = await lightningClient.ListChannels();
|
||||
return Ok(channels.Select(channel => new LightningChannelData()
|
||||
try
|
||||
{
|
||||
Capacity = channel.Capacity,
|
||||
ChannelPoint = channel.ChannelPoint.ToString(),
|
||||
IsActive = channel.IsActive,
|
||||
IsPublic = channel.IsPublic,
|
||||
LocalBalance = channel.LocalBalance,
|
||||
RemoteNode = channel.RemoteNode.ToString()
|
||||
}));
|
||||
var channels = await lightningClient.ListChannels();
|
||||
return Ok(channels.Select(channel => new LightningChannelData()
|
||||
{
|
||||
Capacity = channel.Capacity,
|
||||
ChannelPoint = channel.ChannelPoint.ToString(),
|
||||
IsActive = channel.IsActive,
|
||||
IsPublic = channel.IsPublic,
|
||||
LocalBalance = channel.LocalBalance,
|
||||
RemoteNode = channel.RemoteNode.ToString()
|
||||
}));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
@ -117,9 +117,9 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request?.NodeURI is null)
|
||||
if (TryGetNodeInfo(request.Node, out var nodeInfo))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.NodeURI),
|
||||
ModelState.AddModelError(nameof(request.Node),
|
||||
"A valid node info was not provided to open a channel with");
|
||||
}
|
||||
|
||||
@ -141,43 +141,30 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
if (CheckValidation(out var errorActionResult))
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return errorActionResult;
|
||||
}
|
||||
|
||||
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
|
||||
try
|
||||
{
|
||||
ChannelAmount = request.ChannelAmount,
|
||||
FeeRate = request.FeeRate,
|
||||
NodeInfo = request.NodeURI
|
||||
});
|
||||
|
||||
string errorCode, errorMessage;
|
||||
switch (response.Result)
|
||||
{
|
||||
case OpenChannelResult.Ok:
|
||||
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
|
||||
{
|
||||
ChannelAmount = request.ChannelAmount, FeeRate = request.FeeRate, NodeInfo = nodeInfo
|
||||
});
|
||||
if (response.Result == OpenChannelResult.Ok)
|
||||
{
|
||||
return Ok();
|
||||
case OpenChannelResult.AlreadyExists:
|
||||
errorCode = "channel-already-exists";
|
||||
errorMessage = "The channel already exists";
|
||||
break;
|
||||
case OpenChannelResult.CannotAffordFunding:
|
||||
errorCode = "cannot-afford-funding";
|
||||
errorMessage = "Not enough money to open a channel";
|
||||
break;
|
||||
case OpenChannelResult.NeedMoreConf:
|
||||
errorCode = "need-more-confirmations";
|
||||
errorMessage = "Need to wait for more confirmations";
|
||||
break;
|
||||
case OpenChannelResult.PeerNotConnected:
|
||||
errorCode = "peer-not-connected";
|
||||
errorMessage = "Not connected to peer";
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("Unknown OpenChannelResult");
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, response.Result.ToString());
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
return this.CreateAPIError(errorCode, errorMessage);
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
@ -188,7 +175,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
|
||||
return Ok((await lightningClient.GetDepositAddress()).ToString());
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
|
||||
@ -200,78 +187,91 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (lightningInvoice?.BOLT11 is null ||
|
||||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
|
||||
try
|
||||
{
|
||||
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
|
||||
BOLT11PaymentRequest.TryParse(lightningInvoice.Invoice, out var bolt11PaymentRequest, network.NBitcoinNetwork);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(lightningInvoice), "The BOLT11 invoice was invalid.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
if (CheckValidation(out var errorActionResult))
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return errorActionResult;
|
||||
}
|
||||
|
||||
var result = await lightningClient.Pay(lightningInvoice.BOLT11);
|
||||
var result = await lightningClient.Pay(lightningInvoice.Invoice);
|
||||
switch (result.Result)
|
||||
{
|
||||
case PayResult.CouldNotFindRoute:
|
||||
return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer");
|
||||
case PayResult.Error:
|
||||
return this.CreateAPIError("generic-error", result.ErrorDetail);
|
||||
case PayResult.Ok:
|
||||
return Ok();
|
||||
default:
|
||||
throw new NotSupportedException("Unsupported Payresult");
|
||||
case PayResult.CouldNotFindRoute:
|
||||
ModelState.AddModelError(nameof(lightningInvoice.Invoice), "Could not find route");
|
||||
break;
|
||||
case PayResult.Error:
|
||||
ModelState.AddModelError(nameof(lightningInvoice.Invoice), result.ErrorDetail);
|
||||
break;
|
||||
}
|
||||
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
|
||||
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var inv = await lightningClient.GetInvoice(id);
|
||||
if (inv == null)
|
||||
try
|
||||
{
|
||||
return NotFound();
|
||||
var inv = await lightningClient.GetInvoice(id);
|
||||
if (inv == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(ToModel(inv));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
return Ok(ToModel(inv));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
|
||||
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.Amount < LightMoney.Zero)
|
||||
if (CheckValidation(out var errorActionResult))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0");
|
||||
return errorActionResult;
|
||||
}
|
||||
|
||||
if (request.Expiry <= TimeSpan.Zero)
|
||||
try
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0");
|
||||
}
|
||||
var invoice = await lightningClient.CreateInvoice(
|
||||
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
var invoice = await lightningClient.CreateInvoice(
|
||||
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints
|
||||
},
|
||||
CancellationToken.None);
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
|
||||
private LightningInvoiceData ToModel(LightningInvoice invoice)
|
||||
@ -288,12 +288,40 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
};
|
||||
}
|
||||
|
||||
private bool CheckValidation(out IActionResult result)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
result = BadRequest(new ValidationProblemDetails(ModelState));
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool CanUseInternalLightning(bool doingAdminThings)
|
||||
{
|
||||
return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) ||
|
||||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
|
||||
}
|
||||
|
||||
|
||||
private bool TryGetNodeInfo(ConnectToNodeRequest request, out NodeInfo nodeInfo)
|
||||
{
|
||||
nodeInfo = null;
|
||||
if (!string.IsNullOrEmpty(request.NodeInfo)) return NodeInfo.TryParse(request.NodeInfo, out nodeInfo);
|
||||
try
|
||||
{
|
||||
nodeInfo = new NodeInfo(new PubKey(request.NodeId), request.NodeHost, request.NodePort);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
|
||||
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
|
||||
|
||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) :null;
|
||||
return !ModelState.IsValid ? BadRequest(new ValidationProblemDetails(ModelState)) : null;
|
||||
}
|
||||
|
||||
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}")]
|
||||
public async Task<IActionResult> RemoveStore(string storeId)
|
||||
public async Task<ActionResult> RemoveStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -57,8 +57,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
if (!_storeRepository.CanDeleteStores())
|
||||
{
|
||||
return this.CreateAPIError("unsupported",
|
||||
"BTCPay Server is using a database server that does not allow you to remove stores.");
|
||||
ModelState.AddModelError(string.Empty, "BTCPay Server is using a database server that does not allow you to remove stores.");
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
|
||||
return Ok();
|
||||
@ -194,8 +194,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
ModelState.AddModelError(nameof(request.MonitoringExpiration), "InvoiceExpiration can only be between 10 and 34560 mins");
|
||||
if(request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
|
||||
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
|
||||
|
||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
||||
|
||||
return !ModelState.IsValid ? BadRequest(new ValidationProblemDetails(ModelState)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NicolasDorier.RateLimits;
|
||||
using BTCPayServer.Client;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
@ -63,21 +62,16 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("~/api/v1/users")]
|
||||
public async Task<IActionResult> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
|
||||
public async Task<ActionResult<ApplicationUserData>> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request?.Email is null)
|
||||
ModelState.AddModelError(nameof(request.Email), "Email is missing");
|
||||
if (!string.IsNullOrEmpty(request?.Email) && !Validation.EmailValidator.IsEmail(request.Email))
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing"));
|
||||
if (!Validation.EmailValidator.IsEmail(request.Email))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Email), "Invalid email"));
|
||||
}
|
||||
if (request?.Password is null)
|
||||
ModelState.AddModelError(nameof(request.Password), "Password is missing");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing"));
|
||||
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
var isAuth = User.Identity.AuthenticationType == GreenFieldConstants.AuthenticationType;
|
||||
@ -119,7 +113,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Password), error.Description);
|
||||
}
|
||||
return this.CreateValidationError(ModelState);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
if (!isAdmin)
|
||||
{
|
||||
@ -131,12 +125,9 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
foreach (var error in identityResult.Errors)
|
||||
{
|
||||
if (error.Code == "DuplicateUserName")
|
||||
ModelState.AddModelError(nameof(request.Email), error.Description);
|
||||
else
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return this.CreateValidationError(ModelState);
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
}
|
||||
|
||||
if (request.IsAdministrator is true)
|
||||
@ -161,6 +152,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return CreatedAtAction(string.Empty, user);
|
||||
}
|
||||
|
||||
private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage)
|
||||
{
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError(propertyName, errorMessage);
|
||||
return new ValidationProblemDetails(modelState);
|
||||
}
|
||||
|
||||
private static ApplicationUserData FromModel(ApplicationUser data)
|
||||
{
|
||||
return new ApplicationUserData()
|
||||
|
@ -128,10 +128,10 @@ namespace BTCPayServer.Controllers
|
||||
else if (wanted?.Any()??false)
|
||||
{
|
||||
if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) &&
|
||||
wanted.Any(permission => !string.IsNullOrEmpty(permission.Scope)))
|
||||
wanted.Any(permission => !string.IsNullOrEmpty(permission.StoreId)))
|
||||
{
|
||||
permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.Specific;
|
||||
permissionValue.SpecificStores = wanted.Select(permission => permission.Scope).ToList();
|
||||
permissionValue.SpecificStores = wanted.Select(permission => permission.StoreId).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -475,14 +475,11 @@ namespace BTCPayServer.Controllers
|
||||
if (await CanUseHotWallet())
|
||||
{
|
||||
var derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationScheme.IsHotWallet)
|
||||
{
|
||||
var extKey = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
return SignWithSeed(walletId,
|
||||
new SignWithSeedViewModel() { SeedOrKey = extKey, SigningContext = signingContext });
|
||||
}
|
||||
var extKey = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
return SignWithSeed(walletId,
|
||||
new SignWithSeedViewModel() {SeedOrKey = extKey, SigningContext = signingContext });
|
||||
}
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ namespace BTCPayServer
|
||||
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -151,8 +151,6 @@ namespace BTCPayServer
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Source { get; set; }
|
||||
[JsonIgnore]
|
||||
public bool IsHotWallet => Source == "NBXplorer";
|
||||
|
||||
[Obsolete("Use GetAccountKeySettings().AccountKeyPath instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
@ -62,32 +62,23 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (_opts.SocksEndpoint is null || _ServerContext != null)
|
||||
return Task.CompletedTask;
|
||||
if (_opts.SocksHttpProxy is Uri uri)
|
||||
_Cts = new CancellationTokenSource();
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
Port = ((IPEndPoint)(socket.LocalEndPoint)).Port;
|
||||
Uri = new Uri($"http://127.0.0.1:{Port}");
|
||||
socket.Listen(5);
|
||||
_ServerContext = new ServerContext()
|
||||
{
|
||||
Logs.PayServer.LogInformation($"sockshttpproxy is set, we will be using a socks http proxy at {uri.AbsoluteUri}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.PayServer.LogInformation($"sockshttpproxy is not set, we will be using an internal socks http proxy");
|
||||
_Cts = new CancellationTokenSource();
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
Port = ((IPEndPoint)(socket.LocalEndPoint)).Port;
|
||||
Uri = new Uri($"http://127.0.0.1:{Port}");
|
||||
socket.Listen(5);
|
||||
_ServerContext = new ServerContext()
|
||||
{
|
||||
SocksEndpoint = _opts.SocksEndpoint,
|
||||
ServerSocket = socket,
|
||||
CancellationToken = _Cts.Token,
|
||||
ConnectionCount = 0
|
||||
};
|
||||
socket.BeginAccept(Accept, _ServerContext);
|
||||
Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy listening at {Uri}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
SocksEndpoint = _opts.SocksEndpoint,
|
||||
ServerSocket = socket,
|
||||
CancellationToken = _Cts.Token,
|
||||
ConnectionCount = 0
|
||||
};
|
||||
socket.BeginAccept(Accept, _ServerContext);
|
||||
Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy listening at {Uri}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public int Port { get; private set; }
|
||||
|
@ -12,7 +12,6 @@ using Newtonsoft.Json;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
@ -21,13 +20,10 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
RequestDelegate _Next;
|
||||
BTCPayServerOptions _Options;
|
||||
BTCPayServerEnvironment _Env;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
BTCPayServerOptions options,
|
||||
BTCPayServerEnvironment env)
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_Env = env ?? throw new ArgumentNullException(nameof(env));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
@ -60,12 +56,6 @@ namespace BTCPayServer.Hosting
|
||||
await _Next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!httpContext.Request.IsOnion() && (httpContext.Request.Headers["Accept"].ToString().StartsWith("text/html", StringComparison.InvariantCulture)))
|
||||
{
|
||||
var onionLocation = _Env.OnionUrl + httpContext.Request.Path;
|
||||
httpContext.Response.SetHeader("Onion-Location", onionLocation);
|
||||
}
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{ }
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -169,8 +169,9 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var prefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
|
||||
var nodeSupport = _dashboard?.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities
|
||||
?.CanSupportTransactionCheck is true;
|
||||
onchainMethod.PayjoinEnabled &= supportedPaymentMethod.IsHotWallet && nodeSupport;
|
||||
if (!supportedPaymentMethod.IsHotWallet)
|
||||
bool isHotwallet = supportedPaymentMethod.Source == "NBXplorer";
|
||||
onchainMethod.PayjoinEnabled &= isHotwallet && nodeSupport;
|
||||
if (!isHotwallet)
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet");
|
||||
if (!nodeSupport)
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.");
|
||||
|
@ -1,16 +1,14 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Socks5HttpClientHandler : HttpClientHandler
|
||||
{
|
||||
public Socks5HttpClientHandler(BTCPayServerOptions opts, Socks5HttpProxyServer sock5)
|
||||
public Socks5HttpClientHandler(Socks5HttpProxyServer sock5)
|
||||
{
|
||||
this.Proxy = new WebProxy(sock5.Uri ?? opts.SocksHttpProxy);
|
||||
this.Proxy = new WebProxy(sock5.Uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>BTCPay Server Greenfield API</title>
|
||||
<title>ReDoc</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
@ -14,38 +14,57 @@
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationProblemDetails": {
|
||||
"type": "array",
|
||||
"description": "An array of validation errors of the request",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A specific validation error on a json property",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "The json path of the property which failed validation"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "User friendly error message about the validation"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"errors": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProblemDetails": {
|
||||
"type": "object",
|
||||
"description": "Description of an error happening during processing of the request",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "An error code describing the error"
|
||||
"nullable": true
|
||||
},
|
||||
"message": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "User friendly error message about the error"
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"instance": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,67 +2,98 @@
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ConnectToNodeRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeURI": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Node URI in the form `pubkey@endpoint[:port]`"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeInfo": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"nodeHost": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"nodePort": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"CreateLightningInvoiceRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"description": "Amount wrapped in a string, represented in a millistatoshi string. (1000 millisatoshi = 1 satoshi)",
|
||||
"nullable": false
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/LightMoney"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Description of the invoice in the BOLT11"
|
||||
"nullable": true
|
||||
},
|
||||
"expiry": {
|
||||
"type": "integer",
|
||||
"description": "Expiration time in seconds"
|
||||
"type": "string",
|
||||
"format": "time-span"
|
||||
},
|
||||
"privateRouteHints": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": false,
|
||||
"description": "True if the invoice should include private route hints"
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"LightMoney": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "a number amount wrapped in a string, represented in millistatoshi (00000000001BTC = 1 mSAT)",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"LightningChannelData": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"remoteNode": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "The public key of the node (Node ID)"
|
||||
"nullable": true
|
||||
},
|
||||
"isPublic": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the node is public"
|
||||
"type": "boolean"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the node is online"
|
||||
"type": "boolean"
|
||||
},
|
||||
"capacity": {
|
||||
"type": "string",
|
||||
"description": "The capacity of the channel in millisatoshi",
|
||||
"nullable": false
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/LightMoney"
|
||||
}
|
||||
]
|
||||
},
|
||||
"localBalance": {
|
||||
"type": "string",
|
||||
"description": "The local balance of the channel in millisatoshi",
|
||||
"nullable": false
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/LightMoney"
|
||||
}
|
||||
]
|
||||
},
|
||||
"channelPoint": {
|
||||
"type": "string",
|
||||
@ -76,32 +107,39 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The invoice's ID"
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/LightningInvoiceStatus"
|
||||
},
|
||||
"BOLT11": {
|
||||
"bolT11": {
|
||||
"type": "string",
|
||||
"description": "The BOLT11 representation of the invoice",
|
||||
"nullable": false
|
||||
"nullable": true
|
||||
},
|
||||
"paidAt": {
|
||||
"type": "integer",
|
||||
"description": "The unix timestamp when the invoice got paid",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "integer",
|
||||
"description": "The unix timestamp when the invoice expires"
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"description": "The amount of the invoice in millisatoshi"
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/LightMoney"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amountReceived": {
|
||||
"type": "string",
|
||||
"description": "The amount received in millisatoshi"
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/LightMoney"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -121,26 +159,28 @@
|
||||
},
|
||||
"LightningNodeInformationData": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeURIs": {
|
||||
"nodeInfoList": {
|
||||
"type": "array",
|
||||
"description": "Node URIs to connect to this node in the form `pubkey@endpoint[:port]`",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"blockHeight": {
|
||||
"type": "integer",
|
||||
"description": "The block height of the lightning node"
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayLightningInvoiceRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"BOLT11": {
|
||||
"invoice": {
|
||||
"type": "string",
|
||||
"description": "The BOLT11 of the invoice to pay"
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -148,19 +188,48 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeURI": {
|
||||
"type": "string",
|
||||
"description": "Node URI in the form `pubkey@endpoint[:port]`"
|
||||
"node": {
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ConnectToNodeRequest"
|
||||
}
|
||||
]
|
||||
},
|
||||
"channelAmount": {
|
||||
"type": "string",
|
||||
"description": "The amount to fund (in satoshi)"
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Money"
|
||||
}
|
||||
]
|
||||
},
|
||||
"feeRate": {
|
||||
"type": "number",
|
||||
"description": "The amount to fund (in satoshi per byte)"
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/FeeRate"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Money": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "a number amount wrapped in a string, represented in satoshi (00000001BTC = 1 sat)"
|
||||
},
|
||||
"FeeRate": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,11 +30,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -70,8 +77,8 @@
|
||||
"200": {
|
||||
"description": "Successfully connected"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to connect to the lightning node",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -80,21 +87,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `could-not-connect`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -150,8 +144,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -185,8 +189,8 @@
|
||||
"200": {
|
||||
"description": "Successfully opened"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to connect to the lightning node",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -195,18 +199,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -230,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
"/api/v1/server/lightning/{cryptoCode}/address": {
|
||||
"post": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Lightning (Internal Node)"
|
||||
],
|
||||
@ -254,17 +248,23 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "A bitcoin address belonging to the lightning node"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -316,11 +316,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration or the specified invoice was not found "
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -356,8 +363,8 @@
|
||||
"200": {
|
||||
"description": "Successfully paid"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to pay the lightning invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -366,21 +373,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `could-not-find-route`, `generic-error`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -433,11 +427,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to create the lightning invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -445,7 +446,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateLightningInvoiceRequest"
|
||||
"$ref": "#/components/schemas/PayLightningInvoiceRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,19 @@
|
||||
"summary": "Get node information",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -39,17 +39,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -64,19 +71,19 @@
|
||||
"summary": "Connect to lightning node",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -88,8 +95,8 @@
|
||||
"200": {
|
||||
"description": "Successfully connected"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to connect to the lightning node",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -98,21 +105,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `could-not-connect`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -128,7 +122,7 @@
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -143,19 +137,19 @@
|
||||
"summary": "Get channels",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -177,14 +171,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -197,19 +201,19 @@
|
||||
"summary": "Open channel",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -221,8 +225,8 @@
|
||||
"200": {
|
||||
"description": "Successfully opened"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to connect to the lightning node",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -231,21 +235,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -261,7 +252,7 @@
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -269,27 +260,26 @@
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/address": {
|
||||
"post": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Lightning (Store)"
|
||||
],
|
||||
"summary": "Get deposit address",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "A bitcoin address belonging to the lightning node"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -308,24 +298,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -334,19 +330,19 @@
|
||||
"summary": "Get invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -374,17 +370,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration or the specified invoice was not found "
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -399,19 +402,19 @@
|
||||
"summary": "Pay Lightning Invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -423,8 +426,8 @@
|
||||
"200": {
|
||||
"description": "Successfully paid"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to pay the lightning invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -433,21 +436,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `could-not-find-route`, `generic-error`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -463,7 +453,7 @@
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
@ -478,19 +468,19 @@
|
||||
"summary": "Create lightning invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"description": "The store id with the lightning node configuration you wish to use",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -509,11 +499,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when attempting to create the lightning invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -529,7 +526,7 @@
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.server.cancreatelightninginvoiceinternalnode"
|
||||
"btcpay.store.cancreatelightninginvoice"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.202 AS builder
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
WORKDIR /source
|
||||
COPY nuget.config nuget.config
|
||||
COPY Build/Common.csproj Build/Common.csproj
|
||||
@ -28,7 +27,6 @@ ENV LANG en_US.UTF-8
|
||||
WORKDIR /datadir
|
||||
WORKDIR /app
|
||||
ENV BTCPAY_DATADIR=/datadir
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
|
@ -1,6 +1,5 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.202 AS builder
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
RUN apt-get update \
|
||||
&& apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support
|
||||
|
||||
@ -33,7 +32,6 @@ ENV LANG en_US.UTF-8
|
||||
WORKDIR /datadir
|
||||
WORKDIR /app
|
||||
ENV BTCPAY_DATADIR=/datadir
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
|
@ -1,6 +1,5 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.202 AS builder
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
RUN apt-get update \
|
||||
&& apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support
|
||||
|
||||
@ -33,7 +32,6 @@ ENV LANG en_US.UTF-8
|
||||
WORKDIR /datadir
|
||||
WORKDIR /app
|
||||
ENV BTCPAY_DATADIR=/datadir
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
|
72
docs/greenfield-development.md
Normal file
72
docs/greenfield-development.md
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
# GreenField API Development Documentation
|
||||
## Adding new API endpoints
|
||||
|
||||
* Always document all endpoints and model schemas in swagger. OpenAPI 3.0 is used as a specification, in JSON formatting, and is written manually. The specification is split to a file per controller and then merged by the server through a controller action at `/swagger/v1/swagger.json`.
|
||||
* All `JsonConverter` usage should be registered through attributes within the model itself.
|
||||
* `decimal` and `long` and other similar types, if there is a need for decimal precision or has the possibility of an overflow issue, should be serialized to a string and able to deserialize from the original type and a string.
|
||||
* Ensure that the correct security permissions are set on the endpoint. Create a new permission if none of the existing ones are suitable.
|
||||
* Use HTTP methods according to REST principles when possible. This means:
|
||||
* `POST` - Create or custom action
|
||||
* `PUT` - Update full model
|
||||
* `PATCH` - Update partially
|
||||
* `DELETE` - Delete or Archive
|
||||
* When returning an error response, we should differentiate from 2 possible scenarios:
|
||||
* Model validation - an error or errors on the request was found - [Status Code 422](https://httpstatuses.com/422) with the model:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "prop-name",
|
||||
"message": "human readable message"
|
||||
}
|
||||
]
|
||||
```
|
||||
* Generic request error - an error resulting from the business logic unable to handle the specified request - [Status Code 400](https://httpstatuses.com/400) with the model:
|
||||
```json
|
||||
{
|
||||
"code": "unique-error-code",
|
||||
"message":"a human readable message"
|
||||
}
|
||||
```
|
||||
|
||||
## Updating existing API endpoints
|
||||
|
||||
### Scenario 1: Changing a property type on the model
|
||||
Changing a property on a model is a breaking change unless the server starts handling both versions.
|
||||
|
||||
#### Solutions
|
||||
* Bump the version of the endpoint.
|
||||
|
||||
#### Alternatives considered
|
||||
* Create a `JsonConverter` that allows conversion between the original type and the new type. However, if this option is used, you will need to ensure that the response model returns the same format. In the case of the `GET` endpoint, you will break clients expecting the original type.
|
||||
|
||||
### Scenario 2: Removing a property on the model
|
||||
Removing a property on a model is a breaking change.
|
||||
|
||||
#### Solutions
|
||||
* Bump the version of the endpoint.
|
||||
|
||||
#### Alternatives considered
|
||||
* Create a default value (one that is not useful) to be sent back in the model. Ignore the property being sent on the model to the server.
|
||||
|
||||
### Scenario 3: Adding a property on the model
|
||||
Adding a property on a model can potentially be a breaking change. It is a breaking change if:
|
||||
* the property is required.
|
||||
* the property has no default value.
|
||||
|
||||
#### Solutions
|
||||
* Check if the payload has the property present. If not, either set to the default value (in the case of a`POST`) or set to the model's current value. See [Detecting missing properties in a JSON model](#missing-properties-detect) for how to achieve this.
|
||||
|
||||
#### Alternatives considered
|
||||
* Bump the version of the endpoint.
|
||||
* Assume the property is always sent and let the value be set to the default if not ( in the case of nullable types, this may be problematic when calling update endpoints).
|
||||
* Use [`[JsonExtensionData]AdditionalData`](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonExtensionDataAttribute.htm) so that clients receive the full payload even after updating only the server. This is problematic as it only fixes clients which implement this opinionated flow (this is not a standard or common way of doing API calls) .
|
||||
|
||||
|
||||
|
||||
## Technical specifics
|
||||
|
||||
### <a name="missing-properties-detect"></a>Detecting missing properties in a JSON model.
|
||||
Possible solutions:
|
||||
* Read the raw JSON object in the controller action and and search for the lack of a specific property.
|
||||
* Use [`JSON.NET Serialization Callabacks`](https://www.newtonsoft.com/json/help/html/SerializationCallbacks.htm) to set a `List<string> MissingProperties;` variable.
|
Reference in New Issue
Block a user