Compare commits

..

5 Commits

Author SHA1 Message Date
6729827645 Update greenfield-development.md 2020-06-15 12:45:05 +02:00
6c828a29ec Update greenfield-development.md 2020-06-12 14:10:43 +02:00
34239dc383 Update docs/greenfield-development.md
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2020-06-04 12:03:56 +02:00
6af3b4a51d Update greenfield-development.md 2020-06-03 11:12:26 +02:00
16afca8058 Create GreenField Api Development Docs 2020-06-03 10:28:37 +02:00
54 changed files with 693 additions and 910 deletions

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{

View File

@ -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()
{

View File

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

View File

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

View File

@ -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)
{ }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.