Compare commits
104 Commits
v1.0.2.99
...
v1.0.2.114
Author | SHA1 | Date | |
---|---|---|---|
4c963d6edf | |||
396bc7f7b4 | |||
2896a9b26f | |||
9267a45449 | |||
c430d470c4 | |||
3921a3ca22 | |||
1ff0a98d30 | |||
f0efd52cb7 | |||
bb8fa88688 | |||
4b976c13c1 | |||
f68d4efcdd | |||
fea247b218 | |||
f419c56a3c | |||
a5fca7a1c4 | |||
e18d0b5d51 | |||
9952cdca7f | |||
6278145374 | |||
84018a5caa | |||
d7785fe2d2 | |||
e1751c4d91 | |||
913da79ff4 | |||
a4fbb2de7e | |||
b5601ed5e6 | |||
42c4f15f22 | |||
6fbd9b2628 | |||
d04bfb58a2 | |||
cded2548f5 | |||
dcc859a86a | |||
9bec38559f | |||
2856454d41 | |||
b3c4fc4003 | |||
c2bbc04c4c | |||
db40c7bc32 | |||
60707fdbd1 | |||
f05614f4da | |||
a10c382bd4 | |||
da2fb876cb | |||
3c58bff803 | |||
a28814bc0e | |||
3cff8261ae | |||
b16e8f7b76 | |||
57ab001c0c | |||
3d85dace38 | |||
7a04c2974f | |||
d459839bf7 | |||
657cfe1b23 | |||
f4eaa0f01f | |||
1e2ffcadf0 | |||
dcc05af02e | |||
4b7f78f38b | |||
f94ff4cc74 | |||
b750663a1f | |||
4c4b76e995 | |||
da19d2c1a7 | |||
fb15c5b354 | |||
6ffe1cfcab | |||
87678c58ac | |||
feab4cc48a | |||
712946f512 | |||
a7bfceae05 | |||
8a26cd549a | |||
1cf3ce0617 | |||
73c65fada2 | |||
92ea923c03 | |||
e7db453717 | |||
10ee09f052 | |||
be7d91a138 | |||
3278c80d3f | |||
65e1edb0b8 | |||
e05c88370f | |||
15c29f8419 | |||
fc722731d3 | |||
1c9c564e90 | |||
872b60f8ea | |||
0d3364b3da | |||
fed53661b3 | |||
e86b4d89ca | |||
c5cb32f6dd | |||
deb56e16ec | |||
b5626ef01c | |||
e39d9067f2 | |||
43d34d5d35 | |||
7341be76bb | |||
0abd62dfe8 | |||
735012e3d7 | |||
b5efb8d2e6 | |||
1dbeabb716 | |||
671f9e56e2 | |||
dc6c189948 | |||
4501824f3f | |||
4568d2a98e | |||
f5d81334f8 | |||
f3ed90399b | |||
fada01cec9 | |||
1b4b9fb4cc | |||
6eeef8a866 | |||
24979a0af2 | |||
be1a44f018 | |||
9fcc2903fc | |||
06df63b283 | |||
0f1efc16f5 | |||
da8a06952c | |||
38d810cef7 | |||
393a3a2b8f |
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csChangellyTests.csChargeTester.csLightningDTester.cs
Lnd
RateRulesTest.csServerTester.csTestAccount.csUnitTest1.csUnitTestPeusa.csdocker-compose.ymlBTCPayServer
BTCPayNetworkProvider.Groestlcoin.csBTCPayServer.csprojPaymentMethodExtensions.csProgram.csWalletId.cs
DockerfileREADME.mdglobal.jsonConfiguration
Controllers
AccountController.csAppsController.PointOfSale.csAppsController.csAppsPublicController.csChangellyController.csInvoiceController.API.csInvoiceController.PaymentProtocol.csInvoiceController.UI.csInvoiceController.csPaymentRequestActionResult.csPublicController.csRateController.csServerController.csStoresController.BTCLike.csStoresController.Changelly.csStoresController.LightningLike.csStoresController.csWalletsController.cs
Data
Filters
HostedServices
Hosting
JsonConverters
Models
InvoicingModels
StoreViewModels
CheckoutExperienceViewModel.csPayButtonViewModel.csStoreViewModel.csUpdateChangellySettingsViewModel.cs
WalletViewModels
Payments
Changelly
Lightning
CLightning
Charge
ILightningInvoiceClient.csLightMoney.csLightningClientFactory.csLightningConnectionString.csLightningLikePaymentData.csLightningLikePaymentHandler.csLightningListener.csLightningSupportedPaymentMethod.csLnd
Properties
Rating
Security
Services
Views
Account
Apps
AppsPublic
Home
Invoice
Manage
Public
Shared
Stores
CheckoutExperience.cshtmlPayButton.cshtmlPayButtonEnable.cshtmlPayButtonTest.cshtmlUpdateChangellySettings.cshtmlUpdateStore.cshtml
Wallets
wwwroot
@ -9,13 +9,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -27,4 +26,7 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -198,15 +198,19 @@ namespace BTCPayServer.Tests
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
context.Request.Host = new HostString("127.0.0.1", Port);
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
if (additionalClaims != null)
|
||||
claims.AddRange(additionalClaims);
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
|
||||
}
|
||||
if (storeId != null)
|
||||
{
|
||||
|
318
BTCPayServer.Tests/ChangellyTests.cs
Normal file
318
BTCPayServer.Tests/ChangellyTests.cs
Normal file
@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChangellyTests
|
||||
{
|
||||
public ChangellyTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.Null(storeBlob.ChangellySettings);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
|
||||
updateModel.ApiSecret);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
|
||||
updateModel.ChangellyMerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async void CanToggleChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var changellyController =
|
||||
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
|
||||
|
||||
//test non existing payment method
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel
|
||||
{
|
||||
Enabled = false
|
||||
};
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
updateModel.Enabled = true;
|
||||
//test with enabled method
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
|
||||
Assert.IsNotType<BitpayErrorModel>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async void CanGetCurrencyListFromChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
//save changelly settings
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa"
|
||||
};
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
//confirm saved
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var mockChangelly = new MockChangelly(new MockHttpClientFactory(), updateModel.ApiKey, updateModel.ApiSecret, updateModel.ApiUrl);
|
||||
var mock = new MockChangellyClientProvider(mockChangelly, tester.PayTester.StoreRepository);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
var changellyController = new ChangellyController(mock, tester.NetworkProvider, fetcher);
|
||||
|
||||
|
||||
mockChangelly.GetCurrenciesFullResult = new List<CurrencyFull>()
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Name = "a",
|
||||
Enable = true,
|
||||
PayInConfirmations = 10,
|
||||
FullName = "aa",
|
||||
ImageLink = ""
|
||||
}
|
||||
};
|
||||
var result = Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value as IEnumerable<CurrencyFull>;
|
||||
Assert.Equal(1, mockChangelly.GetCurrenciesFullCallCount);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async void CanCalculateToAmountForChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa"
|
||||
};
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var mockChangelly = new MockChangelly(new MockHttpClientFactory(), updateModel.ApiKey, updateModel.ApiSecret, updateModel.ApiUrl);
|
||||
var mock = new MockChangellyClientProvider(mockChangelly, tester.PayTester.StoreRepository);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
var changellyController = new ChangellyController(mock,tester.NetworkProvider,fetcher);
|
||||
|
||||
mockChangelly.GetExchangeAmountResult = (from, to, amount) =>
|
||||
{
|
||||
Assert.Equal("A", from);
|
||||
Assert.Equal("B", to);
|
||||
|
||||
switch (mockChangelly.GetExchangeAmountCallCount)
|
||||
{
|
||||
case 1:
|
||||
return 0.5m;
|
||||
default:
|
||||
return 1.01m;
|
||||
}
|
||||
};
|
||||
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "A", "B", 1.0m)).Value);
|
||||
Assert.True(mockChangelly.GetExchangeAmountCallCount > 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MockHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
public class MockChangelly : Changelly
|
||||
{
|
||||
public IEnumerable<CurrencyFull> GetCurrenciesFullResult { get; set; }
|
||||
|
||||
public delegate decimal ParamsFunc<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
|
||||
|
||||
public ParamsFunc<string, string, decimal, (decimal amount, bool Success, string Error)> GetExchangeAmountResult
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int GetCurrenciesFullCallCount { get; set; } = 0;
|
||||
public int GetExchangeAmountCallCount { get; set; } = 0;
|
||||
|
||||
public MockChangelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl) : base(httpClientFactory, apiKey, apiSecret, apiUrl)
|
||||
{
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
public override async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
GetCurrenciesFullCallCount++;
|
||||
return GetCurrenciesFullResult;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
public override async Task<decimal> GetExchangeAmount(string fromCurrency,
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
string toCurrency, decimal amount)
|
||||
{
|
||||
GetExchangeAmountCallCount++;
|
||||
return GetExchangeAmountResult.Invoke(fromCurrency, toCurrency, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public class MockChangellyClientProvider : ChangellyClientProvider
|
||||
{
|
||||
public MockChangelly MockChangelly;
|
||||
|
||||
public MockChangellyClientProvider(
|
||||
MockChangelly mockChangelly,
|
||||
StoreRepository storeRepository) : base(storeRepository, new MockHttpClientFactory())
|
||||
{
|
||||
MockChangelly = mockChangelly;
|
||||
}
|
||||
|
||||
public override bool TryGetChangellyClient(string storeId, out string error, out Changelly changelly)
|
||||
{
|
||||
error = null;
|
||||
changelly = MockChangelly;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningRPCClient RPC { get; }
|
||||
public CLightningClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
Client = new LndClient(Swagger, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public LndClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
||||
|
@ -1,246 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using System.Globalization;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
// this depends for now on `docker-compose up devlnd`
|
||||
public class UnitTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public UnitTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
initializeEnvironment();
|
||||
|
||||
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
||||
|
||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
||||
}
|
||||
|
||||
private LndSwaggerClient MerchantLnd { get; set; }
|
||||
private LndInvoiceClient InvoiceClient { get; set; }
|
||||
|
||||
private LndSwaggerClient CustomerLnd { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task GetInfo()
|
||||
{
|
||||
var res = await InvoiceClient.GetInfo();
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice()
|
||||
{
|
||||
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice()
|
||||
{
|
||||
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
|
||||
|
||||
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play()
|
||||
{
|
||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
||||
var seq2 = seq.Slice(3);
|
||||
var pos = seq2.GetPosition(0);
|
||||
}
|
||||
|
||||
// integration tests
|
||||
[Fact]
|
||||
public async Task TestWaitListenInvoice()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
var waitToken = default(CancellationToken);
|
||||
var listener = await InvoiceClient.Listen(waitToken);
|
||||
var waitTask = listener.WaitInvoice(waitToken);
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await waitTask;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
||||
|
||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice2.BOLT11
|
||||
});
|
||||
|
||||
invoice = await waitTask2;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
||||
await Task.Delay(100);
|
||||
listener.Dispose();
|
||||
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLndInvoiceAndPay()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
|
||||
await EventuallyAsync(async () =>
|
||||
{
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var merchantNodeAddress = new LnrpcLightningAddress
|
||||
{
|
||||
Pubkey = merchantInfo.NodeId,
|
||||
Host = "merchant_lnd:9735"
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if channel is pending generate blocks until confirmed
|
||||
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
|
||||
if (pendingResponse.Pending_open_channels?
|
||||
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
|
||||
{
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if channel is established
|
||||
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
|
||||
LnrpcChannel channelToMerchant = null;
|
||||
if (chanResponse != null && chanResponse.Channels != null)
|
||||
{
|
||||
channelToMerchant = chanResponse.Channels
|
||||
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (channelToMerchant == null)
|
||||
{
|
||||
// create new channel
|
||||
var isConnected = await CustomerLnd.ListPeersAsync();
|
||||
if (isConnected.Peers == null ||
|
||||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
|
||||
{
|
||||
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
|
||||
{
|
||||
Addr = merchantNodeAddress
|
||||
});
|
||||
}
|
||||
|
||||
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
|
||||
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
|
||||
var channelReq = new LnrpcOpenChannelRequest
|
||||
{
|
||||
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
|
||||
Node_pubkey_string = merchantInfo.NodeId
|
||||
};
|
||||
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// channel exists, return it
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
return channelToMerchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await InvoiceClient.GetInfo();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
private void initializeEnvironment()
|
||||
{
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,19 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
public void SecondDuplicatedRuleIsIgnored()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = 1.1");
|
||||
builder.AppendLine("DOGE_X = 1.2");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
|
||||
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
|
||||
rule.Reevaluate();
|
||||
Assert.True(!rule.HasError);
|
||||
Assert.Equal(1.1m, rule.BidAsk.Ask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
|
@ -18,10 +18,10 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -49,8 +49,8 @@ namespace BTCPayServer.Tests
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
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);
|
||||
|
||||
@ -78,88 +78,24 @@ namespace BTCPayServer.Tests
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
||||
if (lndBackend == LightningConnectionType.LndREST)
|
||||
client = MerchantLnd.Client;
|
||||
|
||||
PrepareLightningAsync(client).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] SKIPPED_STATES =
|
||||
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
public Task EnsureConnectedToDestinations()
|
||||
{
|
||||
bool awaitingLocking = false;
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
|
||||
var peers = await CustomerLightningD.ListPeersAsync();
|
||||
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
|
||||
var channel = filteringToTargetedPeers
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
case null:
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
|
||||
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
|
||||
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
|
||||
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
awaitingLocking = true;
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
default:
|
||||
throw new NotSupportedException(channel?.State ?? "");
|
||||
}
|
||||
}
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
|
||||
await Task.WhenAll(synching);
|
||||
if (synching.All(c => c.Result != null))
|
||||
return synching[0].Result;
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
yield return CustomerLightningD;
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
yield return MerchantLightningD;
|
||||
yield return MerchantLnd.Client;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
@ -171,12 +107,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerLightningD.SendAsync(bolt11);
|
||||
await CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
public ILightningClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ILightningClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
@ -218,7 +154,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach(var store in Stores)
|
||||
foreach (var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -71,11 +73,11 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
@ -90,6 +92,8 @@ namespace BTCPayServer.Tests
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
@ -133,7 +137,7 @@ namespace BTCPayServer.Tests
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
||||
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
|
@ -40,6 +40,10 @@ using BTCPayServer.Rating;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -331,7 +335,7 @@ namespace BTCPayServer.Tests
|
||||
(0.1m, "$0.10 (USD)"),
|
||||
})
|
||||
{
|
||||
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
|
||||
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
}
|
||||
@ -387,18 +391,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUseLightMoney()
|
||||
{
|
||||
var light = LightMoney.MilliSatoshis(1);
|
||||
Assert.Equal("0.00000000001", light.ToString());
|
||||
|
||||
light = LightMoney.MilliSatoshis(200000);
|
||||
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Flaky", "Flaky")]
|
||||
public void CanSetLightningServer()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -435,139 +427,24 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseLightningURL()
|
||||
public async Task CanSendLightningPaymentCLightning()
|
||||
{
|
||||
LightningConnectionString conn = null;
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal(i == 0, conn.IsLegacy);
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("tcp://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=tcp://test/a", conn.ToString());
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://aaa:bbb@test/a", conn.ToString());
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
|
||||
Assert.Equal("aaa", conn.Username);
|
||||
Assert.Equal("bbb", conn.Password);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
|
||||
}
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
|
||||
|
||||
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
|
||||
|
||||
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
|
||||
|
||||
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
|
||||
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
|
||||
|
||||
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
|
||||
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
|
||||
|
||||
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
|
||||
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
|
||||
Assert.Equal(conn2.ToString(), conn.ToString());
|
||||
Assert.Equal(lndUri, conn.ToString());
|
||||
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
|
||||
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
|
||||
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
|
||||
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
|
||||
|
||||
// AllowInsecure can be set to allow http
|
||||
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Flaky", "Flaky")]
|
||||
public void CanSendLightningPaymentCLightning()
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Flaky", "Flaky")]
|
||||
public void CanSendLightningPaymentCharge()
|
||||
public async Task CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
await ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Flaky", "Flaky")]
|
||||
public void CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
void ProcessLightningPayment(LightningConnectionType type)
|
||||
async Task ProcessLightningPayment(LightningConnectionType type)
|
||||
{
|
||||
// For easier debugging and testing
|
||||
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
||||
@ -580,11 +457,11 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
tester.PrepareLightning(type);
|
||||
await tester.EnsureConnectedToDestinations();
|
||||
|
||||
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
|
||||
await CanSendLightningPaymentCore(tester, user);
|
||||
|
||||
Task.WaitAll(Enumerable.Range(0, 5)
|
||||
await Task.WhenAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||
.ToArray());
|
||||
}
|
||||
@ -695,6 +572,72 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRescanWallet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
var btcDerivationScheme = acc.DerivationScheme;
|
||||
acc.RegisterDerivationScheme("LTC");
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(acc.UserId);
|
||||
WalletId walletId = new WalletId(acc.StoreId, "LTC");
|
||||
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
Assert.False(rescan.Ok);
|
||||
Assert.True(rescan.IsFullySync);
|
||||
Assert.False(rescan.IsSupportedByCurrency);
|
||||
Assert.False(rescan.IsServerAdmin);
|
||||
|
||||
walletId = new WalletId(acc.StoreId, "BTC");
|
||||
var serverAdminClaim = new[] { new Claim(Policies.CanModifyServerSettings.Key, "true") };
|
||||
walletController = tester.PayTester.GetController<WalletsController>(acc.UserId, additionalClaims: serverAdminClaim);
|
||||
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
Assert.True(rescan.Ok);
|
||||
Assert.True(rescan.IsFullySync);
|
||||
Assert.True(rescan.IsSupportedByCurrency);
|
||||
Assert.True(rescan.IsServerAdmin);
|
||||
|
||||
rescan.GapLimit = 100;
|
||||
|
||||
// Sending a coin
|
||||
var txId = tester.ExplorerNode.SendToAddress(btcDerivationScheme.Derive(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
Assert.Empty(transactions.Transactions);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||
|
||||
while(true)
|
||||
{
|
||||
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
if(rescan.Progress == null && rescan.LastSuccess != null)
|
||||
{
|
||||
if (rescan.LastSuccess.Found == 0)
|
||||
continue;
|
||||
// Scan over
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Null(rescan.TimeOfScan);
|
||||
Assert.NotNull(rescan.RemainingTime);
|
||||
Assert.NotNull(rescan.Progress);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
Assert.Null(rescan.PreviousError);
|
||||
Assert.NotNull(rescan.TimeOfScan);
|
||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanListInvoices()
|
||||
{
|
||||
@ -767,6 +710,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(GetCurrencyPairRateResult);
|
||||
Assert.NotNull(GetCurrencyPairRateResult.Data);
|
||||
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
|
||||
|
||||
// Should be OK because the request is signed, so we can know the store
|
||||
var rates = acc.BitPay.GetRates();
|
||||
HttpClient client = new HttpClient();
|
||||
// Unauthentified requests should also be ok
|
||||
var response = client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}").GetAwaiter().GetResult();
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1444,14 +1394,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
|
||||
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
|
||||
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
|
||||
Assert.Equal("hello", vmview.Title);
|
||||
Assert.Equal(2, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
var invoice = user.BitPay.GetInvoices().First();
|
||||
Assert.Equal(10.00m, invoice.Price);
|
||||
Assert.Equal("CAD", invoice.Currency);
|
||||
@ -1483,6 +1435,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
@ -1713,7 +1666,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private static RateProviderFactory CreateBTCPayRateFactory()
|
||||
public static RateProviderFactory CreateBTCPayRateFactory()
|
||||
{
|
||||
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -34,10 +34,11 @@ services:
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
@ -49,10 +50,11 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
@ -63,7 +65,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.31
|
||||
image: nicolasdorier/nbxplorer:1.1.0.3
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -87,31 +89,31 @@ services:
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawtx=tcp://0.0.0.0:28332
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtxlock=tcp://0.0.0.0:28332
|
||||
zmqpubhashblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "28332" # ZMQ
|
||||
- "28333" # ZMQ
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -122,6 +124,7 @@ services:
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
@ -134,7 +137,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
image: shesek/lightning-charge:0.4.3
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -153,7 +156,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -200,7 +203,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
@ -208,11 +211,13 @@ services:
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
@ -224,7 +229,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
@ -232,11 +237,13 @@ services:
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
|
@ -26,6 +26,7 @@ namespace BTCPayServer
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.99</Version>
|
||||
<Version>1.0.2.114</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -33,34 +33,37 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.45" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.66" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.18" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.3.3" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.17" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -120,16 +123,20 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Apps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButtonEnable.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButton.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButtonHandle.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButtonTest.cshtml">
|
||||
<Content Update="Views\Public\PayButtonHandle.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LNDGRPCServices.cshtml">
|
||||
@ -144,6 +151,9 @@
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletRescan.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletTransactions.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
|
@ -39,6 +39,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
|
@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
|
@ -1,25 +1,11 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Newtonsoft.Json;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using System.IO;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -87,7 +73,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = Parse(settings.Template, settings.Currency);
|
||||
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
@ -109,11 +95,11 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
Parse(vm.Template, vm.Currency);
|
||||
_AppsHelper.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -138,131 +124,6 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await GetStore(app);
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
|
@ -5,8 +5,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -15,32 +16,32 @@ using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class AppsController : Controller
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
CurrencyNameTable _Currencies;
|
||||
InvoiceController _InvoiceController;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
CurrencyNameTable currencies,
|
||||
InvoiceController invoiceController,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
@ -104,7 +105,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var appData = new AppData() { Id = id };
|
||||
@ -115,7 +116,7 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
CreatedAppId = id;
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -136,21 +137,9 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
}
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
@ -201,13 +190,5 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
private async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
202
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
202
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = _AppsHelper.Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await _AppsHelper.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
return _Currencies.GetCurrencyData(currency, useFallback);
|
||||
}
|
||||
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
|
||||
{
|
||||
if (userId == null || appId == null)
|
||||
return null;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
BTCPayServer/Controllers/ChangellyController.cs
Normal file
125
BTCPayServer/Controllers/ChangellyController.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("[controller]/{storeId}")]
|
||||
public class ChangellyController : Controller
|
||||
{
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly RateFetcher _RateProviderFactory;
|
||||
|
||||
public ChangellyController(ChangellyClientProvider changellyClientProvider,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
RateFetcher rateProviderFactory)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("currencies")]
|
||||
public async Task<IActionResult> GetCurrencyList(string storeId)
|
||||
{
|
||||
if (!TryGetChangellyClient(storeId, out var actionResult, out var client))
|
||||
{
|
||||
return actionResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await client.GetCurrenciesFull());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
{
|
||||
if (!TryGetChangellyClient(storeId, out var actionResult, out var client))
|
||||
{
|
||||
return actionResult;
|
||||
}
|
||||
|
||||
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var callCounter = 0;
|
||||
var response1 = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = response1;
|
||||
while (true)
|
||||
{
|
||||
if (callCounter > 10)
|
||||
{
|
||||
BadRequest();
|
||||
}
|
||||
|
||||
var response2 = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
callCounter++;
|
||||
if (response2 < toCurrencyAmount)
|
||||
{
|
||||
var newCurrentAmount = ((toCurrencyAmount / response2) * 1m) * currentAmount;
|
||||
|
||||
currentAmount = newCurrentAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(currentAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetChangellyClient(string storeId, out IActionResult actionResult,
|
||||
out Changelly changelly)
|
||||
{
|
||||
changelly = null;
|
||||
actionResult = null;
|
||||
storeId = storeId ?? HttpContext.GetStoreData()?.Id;
|
||||
|
||||
if (_changellyClientProvider.TryGetChangellyClient(storeId, out var error, out changelly))
|
||||
return true;
|
||||
actionResult = BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = error
|
||||
});
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,20 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
|
@ -1,16 +1,14 @@
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -84,4 +82,36 @@ namespace BTCPayServer.Controllers
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,26 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -60,7 +58,8 @@ namespace BTCPayServer.Controllers
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -215,7 +214,6 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
@ -231,7 +229,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c=> paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
.FirstOrDefault();
|
||||
if (paymentMethodTemp == null)
|
||||
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
@ -247,6 +245,18 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
(1m + (changelly.AmountMarkupPercentage / 100m)))
|
||||
: (decimal?)null;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
@ -286,7 +296,10 @@ namespace BTCPayServer.Controllers
|
||||
Status = invoice.Status,
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
ChangellyEnabled = changelly != null,
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
@ -310,7 +323,7 @@ namespace BTCPayServer.Controllers
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " (via Lightning)";
|
||||
network.DisplayName : network.DisplayName + " (Lightning)";
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
@ -326,39 +339,12 @@ namespace BTCPayServer.Controllers
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||
}
|
||||
|
||||
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||
{
|
||||
var provider = currencies.GetNumberFormatInfo(currency, true);
|
||||
var currencyData = currencies.GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
||||
{
|
||||
price = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return price.ToString("C", provider);
|
||||
else
|
||||
return price.ToString("C", provider) + $" ({currency})";
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -435,23 +421,17 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
var filterString = new SearchString(searchTerm);
|
||||
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
}))
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
};
|
||||
|
||||
var list = await ListInvoicesProcess(searchTerm, skip, count);
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
@ -463,12 +443,29 @@ namespace BTCPayServer.Controllers
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
model.Skip = skip;
|
||||
model.Count = count;
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
@ -502,7 +499,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
StatusMessage = null;
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||
return View(model);
|
||||
@ -531,6 +528,7 @@ namespace BTCPayServer.Controllers
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationEmail = model.NotificationEmail,
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
|
@ -1,47 +1,25 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin.Payment;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -84,6 +62,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
@ -103,6 +83,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
|
@ -1,40 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
62
BTCPayServer/Controllers/PublicController.cs
Normal file
62
BTCPayServer/Controllers/PublicController.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PublicController : Controller
|
||||
{
|
||||
public PublicController(InvoiceController invoiceController,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
private InvoiceController _InvoiceController;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
ModelState.AddModelError("Store", "Invalid store");
|
||||
else
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
||||
}
|
||||
|
||||
if (model == null || model.Price <= 0)
|
||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
NotificationEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,23 +10,32 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[AllowAnonymous]
|
||||
public class RateController : Controller
|
||||
{
|
||||
RateFetcher _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
|
||||
public TokenRepository TokenRepository { get; }
|
||||
|
||||
public RateController(
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
TokenRepository = tokenRepository;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
@ -36,7 +45,7 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
storeId = await GetStoreId(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
@ -66,7 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
@ -79,7 +88,7 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
@ -87,11 +96,29 @@ namespace BTCPayServer.Controllers
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
private async Task<string> GetStoreId(string storeId)
|
||||
{
|
||||
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
|
||||
return storeId;
|
||||
if(storeId == null)
|
||||
{
|
||||
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
|
||||
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
|
||||
}
|
||||
if (storeId == null)
|
||||
return null;
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return null;
|
||||
this.HttpContext.SetStoreData(store);
|
||||
return storeId;
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
|
||||
|
@ -24,6 +24,7 @@ using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -99,9 +99,17 @@ namespace BTCPayServer.Controllers
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
|
||||
if (!showAddress)
|
||||
{
|
||||
@ -110,9 +118,7 @@ namespace BTCPayServer.Controllers
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
|
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/changelly")]
|
||||
public IActionResult UpdateChangellySettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().ChangellySettings;
|
||||
if (existing == null) return;
|
||||
vm.ApiKey = existing.ApiKey;
|
||||
vm.ApiSecret = existing.ApiSecret;
|
||||
vm.ApiUrl = existing.ApiUrl;
|
||||
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
vm.ShowFiat = existing.ShowFiat;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/changelly")]
|
||||
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var changellySettings = new ChangellySettings()
|
||||
{
|
||||
ApiKey = vm.ApiKey,
|
||||
ApiSecret = vm.ApiSecret,
|
||||
ApiUrl = vm.ApiUrl,
|
||||
ChangellyMerchantId = vm.ChangellyMerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage,
|
||||
ShowFiat = vm.ShowFiat
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.ChangellySettings = changellySettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Changelly settings modified";
|
||||
_changellyClientProvider.InvalidateClient(storeId);
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,12 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -9,6 +10,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -48,24 +50,25 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
InvoiceController invoiceController)
|
||||
ChangellyClientProvider changellyClientProvider,
|
||||
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
_InvoiceController = invoiceController;
|
||||
}
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
@ -79,8 +82,9 @@ namespace BTCPayServer.Controllers
|
||||
TokenRepository _TokenRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private LanguageService _LangService;
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
IHostingEnvironment _Env;
|
||||
InvoiceController _InvoiceController;
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
@ -321,7 +325,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
@ -365,7 +368,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
@ -402,6 +404,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
@ -449,6 +452,15 @@ namespace BTCPayServer.Controllers
|
||||
Enabled = !excludeFilters.Match(paymentId)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = changellyEnabled,
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -473,6 +485,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
@ -779,6 +792,12 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var store = StoreData;
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
{
|
||||
return View("PayButtonEnable", null);
|
||||
}
|
||||
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||
var model = new PayButtonViewModel
|
||||
{
|
||||
@ -793,39 +812,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/pay")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle(string storeId, [FromForm]PayButtonViewModel model)
|
||||
[Route("{storeId}/paybutton")]
|
||||
public async Task<IActionResult> PayButton(bool enableStore)
|
||||
{
|
||||
var store = StoreData;
|
||||
|
||||
// TODO: extract validation to model
|
||||
if (model.Price <= 0)
|
||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = enableStore;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
BuyerEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(PayButton), new
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/paybuttontest")]
|
||||
public IActionResult PayButtonTest(string storeId)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
@ -32,17 +33,19 @@ namespace BTCPayServer.Controllers
|
||||
[Route("wallets")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class WalletsController : Controller
|
||||
public partial class WalletsController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
public StoreRepository Repository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public ExplorerClientProvider ExplorerClientProvider { get; }
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||
private readonly NBXplorerDashboard _dashboard;
|
||||
private readonly ExplorerClientProvider _explorerProvider;
|
||||
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
RateFetcher _RateProvider;
|
||||
public RateFetcher RateFetcher { get; }
|
||||
CurrencyNameTable _currencyTable;
|
||||
public WalletsController(StoreRepository repo,
|
||||
CurrencyNameTable currencyTable,
|
||||
@ -56,13 +59,13 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
_Repo = repo;
|
||||
_RateProvider = rateProvider;
|
||||
_NetworkProvider = networkProvider;
|
||||
Repository = repo;
|
||||
RateFetcher = rateProvider;
|
||||
NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
_mvcJsonOptions = mvcJsonOptions;
|
||||
_dashboard = dashboard;
|
||||
_explorerProvider = explorerProvider;
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
@ -70,10 +73,10 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var stores = await Repository.GetStoresByUserId(GetUserId());
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
@ -111,7 +114,7 @@ namespace BTCPayServer.Controllers
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
@ -120,7 +123,7 @@ namespace BTCPayServer.Controllers
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
{
|
||||
var vm = new ListTransactionsViewModel.TransactionViewModel();
|
||||
model.Transactions.Add(vm);
|
||||
@ -139,29 +142,33 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var storeData = store.GetStoreBlob();
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
|
||||
rateRules.Spread = 0.0m;
|
||||
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
|
||||
model.CryptoCurrency = walletId.CryptoCode;
|
||||
WalletModel model = new WalletModel()
|
||||
{
|
||||
DefaultAddress = defaultDestination,
|
||||
DefaultAmount = defaultAmount,
|
||||
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
|
||||
CryptoCurrency = walletId.CryptoCode
|
||||
};
|
||||
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
@ -173,11 +180,78 @@ namespace BTCPayServer.Controllers
|
||||
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
|
||||
}
|
||||
}
|
||||
catch(Exception ex) { model.RateError = ex.Message; }
|
||||
catch (Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new RescanWalletModel();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched();
|
||||
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
|
||||
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
|
||||
if(scanProgress != null)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
|
||||
{
|
||||
if (scanProgress.Progress == null)
|
||||
{
|
||||
vm.Progress = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Progress = scanProgress.Progress.OverallProgress;
|
||||
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
|
||||
}
|
||||
}
|
||||
if (scanProgress.Status == ScanUTXOStatus.Complete)
|
||||
{
|
||||
vm.LastSuccess = scanProgress.Progress;
|
||||
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
|
||||
}
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, RescanWalletModel vm)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
|
||||
}
|
||||
return RedirectToAction();
|
||||
}
|
||||
|
||||
private string GetCurrencyCode(string defaultLang)
|
||||
{
|
||||
if (defaultLang == null)
|
||||
@ -187,7 +261,7 @@ namespace BTCPayServer.Controllers
|
||||
var ri = new RegionInfo(defaultLang);
|
||||
return ri.ISOCurrencySymbol;
|
||||
}
|
||||
catch(ArgumentException) { }
|
||||
catch (ArgumentException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -197,7 +271,7 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
@ -257,7 +331,7 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
network = NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
@ -361,9 +435,8 @@ namespace BTCPayServer.Controllers
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
@ -386,7 +459,6 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
@ -403,7 +475,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = _explorerProvider.GetExplorerClient(network);
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -166,24 +167,25 @@ namespace BTCPayServer.Data
|
||||
public Claim[] GetClaims()
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.AddRange(AdditionalClaims);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
var role = Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
if (role == StoreRoles.Owner)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
}
|
||||
if (role == StoreRoles.Guest)
|
||||
|
||||
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
|
||||
}
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
public bool HasClaim(string claim)
|
||||
{
|
||||
return GetClaims().Any(c => c.Type == claim);
|
||||
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
|
||||
}
|
||||
|
||||
public byte[] StoreBlob
|
||||
@ -196,6 +198,9 @@ namespace BTCPayServer.Data
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
|
||||
{
|
||||
@ -257,11 +262,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
@ -302,6 +302,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
public string LightningDescriptionTemplate
|
||||
|
@ -28,6 +28,28 @@ namespace BTCPayServer.Filters
|
||||
}
|
||||
}
|
||||
|
||||
public class MediaTypeAcceptConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public MediaTypeAcceptConstraintAttribute(string mediaType)
|
||||
{
|
||||
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey("Accept"))
|
||||
return false;
|
||||
return context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().StartsWith(MediaType, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
|
||||
|
@ -23,7 +23,10 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
if (context.IsEffectivePolicy<XFrameOptionsAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -52,24 +53,44 @@ namespace BTCPayServer.HostedServices
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
IEmailSender _EmailSender;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_EmailSender = emailSender;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
|
||||
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
|
||||
{
|
||||
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
|
||||
var ipn = new
|
||||
{
|
||||
invoice.Id,
|
||||
invoice.Status,
|
||||
invoice.StoreId
|
||||
};
|
||||
// TODO: Consider adding info on ItemDesc and payment info (amount)
|
||||
|
||||
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
|
||||
await _EmailSender.SendEmailAsync(
|
||||
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
@ -203,7 +224,7 @@ namespace BTCPayServer.HostedServices
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates,
|
||||
|
||||
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
@ -264,15 +285,15 @@ namespace BTCPayServer.HostedServices
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
if (t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
@ -289,7 +310,7 @@ namespace BTCPayServer.HostedServices
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
if (executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
|
@ -47,7 +47,11 @@ namespace BTCPayServer.HostedServices
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
||||
public NBXplorerSummary Get(string cryptoCode)
|
||||
{
|
||||
_Summaries.TryGetValue(cryptoCode, out var summary);
|
||||
return summary;
|
||||
}
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
|
@ -38,6 +38,7 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -88,7 +89,6 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
return dbContext;
|
||||
});
|
||||
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
|
||||
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
@ -96,6 +96,8 @@ namespace BTCPayServer.Hosting
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<AppsHelper>();
|
||||
|
||||
services.TryAddSingleton<LightningConfigurationProvider>();
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
@ -124,6 +126,8 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
@ -157,7 +161,7 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
var bundle = new BundleOptions();
|
||||
bundle.UseMinifiedFiles = opts.BundleJsCss;
|
||||
bundle.UseBundles = opts.BundleJsCss;
|
||||
bundle.AppendVersion = true;
|
||||
return bundle;
|
||||
});
|
||||
|
@ -83,14 +83,14 @@ namespace BTCPayServer.Hosting
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
@ -105,8 +105,8 @@ namespace BTCPayServer.Hosting
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@ -140,13 +140,9 @@ namespace BTCPayServer.Hosting
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
|
||||
}
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
|
@ -100,6 +100,9 @@ namespace BTCPayServer.Hosting
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
});
|
||||
|
||||
services.AddHangfire((o) =>
|
||||
|
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class LightMoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
Type longType = typeof(long).GetTypeInfo();
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
reader.TokenType == JsonToken.Integer ?
|
||||
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
|
||||
: new LightMoney(long.MaxValue) :
|
||||
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||
: null;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
||||
}
|
||||
}
|
||||
}
|
@ -53,6 +53,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
public string NotificationEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Uri]
|
||||
public string NotificationUrl
|
||||
{
|
||||
|
@ -142,5 +142,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public AddressModel[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
public string NotificationEmail { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,10 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PaymentMethodName { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
|
||||
public bool AllowCoinConversion { get; set; }
|
||||
public bool ChangellyEnabled { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string PeerInfo { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal? ChangellyAmountDue { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public class PayButtonViewModel
|
||||
{
|
||||
public decimal Price { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
[Required]
|
||||
public string Currency { get; set; }
|
||||
public string CheckoutDesc { get; set; }
|
||||
|
@ -21,7 +21,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public WalletId WalletId { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class ThirdPartyPaymentMethod
|
||||
{
|
||||
public string Provider { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Action { get; set; }
|
||||
}
|
||||
public StoreViewModel()
|
||||
{
|
||||
|
||||
@ -47,8 +53,14 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Allow anyone to create invoice")]
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public List<ThirdPartyPaymentMethod> ThirdPartyPaymentMethods { get; set; } =
|
||||
new List<ThirdPartyPaymentMethod>();
|
||||
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
|
@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class UpdateChangellySettingsViewModel
|
||||
{
|
||||
[Required] public string ApiKey { get; set; } = "6ed02cdf1b614d89a8c0ceb170eebb61";
|
||||
|
||||
[Required] public string ApiSecret { get; set; } = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483";
|
||||
|
||||
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
|
||||
|
||||
[Display(Name = "Optional, Changelly Merchant Id")]
|
||||
public string ChangellyMerchantId { get; set; } = "804298eb5753";
|
||||
|
||||
[Display(Name = "Show Fiat Currencies as option in conversion")]
|
||||
public bool ShowFiat { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Range(0, 100)]
|
||||
[Display(Name =
|
||||
"Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")]
|
||||
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
31
BTCPayServer/Models/WalletViewModels/RescanWalletModel.cs
Normal file
31
BTCPayServer/Models/WalletViewModels/RescanWalletModel.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class RescanWalletModel
|
||||
{
|
||||
public bool IsServerAdmin { get; set; }
|
||||
public bool IsSupportedByCurrency { get; set; }
|
||||
public bool IsFullySync { get; set; }
|
||||
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync;
|
||||
|
||||
[Range(1000, 10_000)]
|
||||
public int BatchSize { get; set; } = 3000;
|
||||
[Range(0, 10_000_000)]
|
||||
public int StartingIndex { get; set; } = 0;
|
||||
|
||||
[Range(100, 100000)]
|
||||
public int GapLimit { get; set; } = 10000;
|
||||
|
||||
public int? Progress { get; set; }
|
||||
public string PreviousError { get; set; }
|
||||
public ScanUTXOProgress LastSuccess { get; internal set; }
|
||||
public string TimeOfScan { get; internal set; }
|
||||
public string RemainingTime { get; internal set; }
|
||||
}
|
||||
}
|
@ -15,6 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DefaultAddress { get; set; }
|
||||
public string DefaultAmount { get; set; }
|
||||
|
||||
public decimal? Rate { get; set; }
|
||||
public int Divisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
|
109
BTCPayServer/Payments/Changelly/Changelly.cs
Normal file
109
BTCPayServer/Payments/Changelly/Changelly.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SshNet.Security.Cryptography;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class Changelly
|
||||
{
|
||||
private readonly string _apisecret;
|
||||
private readonly bool _showFiat;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_showFiat = showFiat;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
}
|
||||
|
||||
|
||||
private static string ToHexString(byte[] array)
|
||||
{
|
||||
var hex = new StringBuilder(array.Length * 2);
|
||||
foreach (var b in array)
|
||||
{
|
||||
hex.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", b);
|
||||
}
|
||||
|
||||
return hex.ToString();
|
||||
}
|
||||
|
||||
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
|
||||
{
|
||||
var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
|
||||
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
|
||||
var sign = ToHexString(hashMessage);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Headers.Add("sign", sign);
|
||||
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
|
||||
|
||||
var result = await _httpClient.SendAsync(request);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new ChangellyException(result.ReasonPhrase);
|
||||
var content =
|
||||
await result.Content.ReadAsStringAsync();
|
||||
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
|
||||
{
|
||||
const string message = @"{
|
||||
""jsonrpc"": ""2.0"",
|
||||
""id"": 1,
|
||||
""method"": ""getCurrenciesFull"",
|
||||
""params"": []
|
||||
}";
|
||||
|
||||
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
|
||||
var appendedResult = _showFiat
|
||||
? result.Result.Concat(new[]
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "EUR",
|
||||
FullName = "Euro",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/eur.png"
|
||||
},
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "USD",
|
||||
FullName = "US Dollar",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/usd.png"
|
||||
}
|
||||
})
|
||||
: result.Result;
|
||||
return appendedResult;
|
||||
}
|
||||
|
||||
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
|
||||
string toCurrency,
|
||||
decimal amount)
|
||||
{
|
||||
var message =
|
||||
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
|
||||
|
||||
var result = await PostToApi<string>(message);
|
||||
|
||||
return Convert.ToDecimal(result.Result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
75
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
75
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyClientProvider
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
|
||||
new ConcurrentDictionary<string, Changelly>();
|
||||
|
||||
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public void InvalidateClient(string storeId)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
_clientCache.Remove(storeId, out var value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual bool TryGetChangellyClient(string storeId, out string error,
|
||||
out Changelly changelly)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
changelly = _clientCache[storeId];
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
changelly = null;
|
||||
|
||||
|
||||
var store = _storeRepository.FindStore(storeId).Result;
|
||||
if (store == null)
|
||||
{
|
||||
error = "Store not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var changellySettings = blob.ChangellySettings;
|
||||
|
||||
|
||||
if (changellySettings == null || !changellySettings.IsConfigured())
|
||||
{
|
||||
error = "Changelly not configured for this store";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!changellySettings.Enabled)
|
||||
{
|
||||
error = "Changelly not enabled for this store";
|
||||
return false;
|
||||
}
|
||||
|
||||
changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl, changellySettings.ShowFiat);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
11
BTCPayServer/Payments/Changelly/ChangellyException.cs
Normal file
11
BTCPayServer/Payments/Changelly/ChangellyException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyException : Exception
|
||||
{
|
||||
public ChangellyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
21
BTCPayServer/Payments/Changelly/ChangellySettings.cs
Normal file
21
BTCPayServer/Payments/Changelly/ChangellySettings.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellySettings
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string ApiSecret { get; set; }
|
||||
public string ApiUrl { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal AmountMarkupPercentage { get; set; }
|
||||
public bool ShowFiat { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return
|
||||
!string.IsNullOrEmpty(ApiKey) ||
|
||||
!string.IsNullOrEmpty(ApiSecret) ||
|
||||
!string.IsNullOrEmpty(ApiUrl);
|
||||
}
|
||||
}
|
||||
}
|
17
BTCPayServer/Payments/Changelly/Models/ChangellyResponse.cs
Normal file
17
BTCPayServer/Payments/Changelly/Models/ChangellyResponse.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
|
||||
public class ChangellyResponse<T>
|
||||
{
|
||||
[JsonProperty("jsonrpc")]
|
||||
public string JsonRPC { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public object Id { get; set; }
|
||||
[JsonProperty("result")]
|
||||
public T Result { get; set; }
|
||||
[JsonProperty("error")]
|
||||
public Error Error { get; set; }
|
||||
}
|
||||
}
|
18
BTCPayServer/Payments/Changelly/Models/CurrencyFull.cs
Normal file
18
BTCPayServer/Payments/Changelly/Models/CurrencyFull.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
public class CurrencyFull
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("fullName")]
|
||||
public string FullName { get; set; }
|
||||
[JsonProperty("enabled")]
|
||||
public bool Enable { get; set; }
|
||||
[JsonProperty("payinConfirmations")]
|
||||
public int PayInConfirmations { get; set; }
|
||||
[JsonProperty("image")]
|
||||
public string ImageLink { get; set; }
|
||||
}
|
||||
}
|
13
BTCPayServer/Payments/Changelly/Models/Error.cs
Normal file
13
BTCPayServer/Payments/Changelly/Models/Error.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
public class Error
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class CLightningInvoice
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
[JsonProperty("payment_hash")]
|
||||
public uint256 PaymentHash { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney MilliSatoshi { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
[JsonProperty("expiry_time")]
|
||||
public DateTimeOffset ExpiryTime { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
[JsonProperty("expires_at")]
|
||||
public DateTimeOffset ExpiryAt { get; set; }
|
||||
[JsonProperty("bolt11")]
|
||||
public string BOLT11 { get; set; }
|
||||
[JsonProperty("pay_index")]
|
||||
public int? PayIndex { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Status { get; set; }
|
||||
[JsonProperty("paid_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
}
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Mono.Unix;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class LightningRPCException : Exception
|
||||
{
|
||||
public LightningRPCException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
|
||||
{
|
||||
public Network Network { get; private set; }
|
||||
public Uri Address { get; private set; }
|
||||
|
||||
public CLightningRPCClient(Uri address, Network network)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if(address.Scheme == "file")
|
||||
{
|
||||
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
|
||||
}
|
||||
Address = address;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
|
||||
}
|
||||
|
||||
public Task SendAsync(string bolt11)
|
||||
{
|
||||
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
|
||||
}
|
||||
|
||||
public async Task<PeerInfo[]> ListPeersAsync()
|
||||
{
|
||||
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
|
||||
foreach (var peer in peers)
|
||||
{
|
||||
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
|
||||
}
|
||||
return peers;
|
||||
}
|
||||
|
||||
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
|
||||
{
|
||||
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
|
||||
}
|
||||
|
||||
public Task ConnectAsync(NodeInfo nodeInfo)
|
||||
{
|
||||
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
|
||||
}
|
||||
|
||||
static Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
parameters = parameters ?? Array.Empty<string>();
|
||||
using (Socket socket = await Connect())
|
||||
{
|
||||
using (var networkStream = new NetworkStream(socket))
|
||||
{
|
||||
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
|
||||
{
|
||||
using (var jsonWriter = new JsonTextWriter(textWriter))
|
||||
{
|
||||
var req = new JObject();
|
||||
req.Add("id", 0);
|
||||
req.Add("method", command);
|
||||
req.Add("params", new JArray(parameters));
|
||||
await req.WriteToAsync(jsonWriter, cancellation);
|
||||
await jsonWriter.FlushAsync(cancellation);
|
||||
}
|
||||
await textWriter.FlushAsync();
|
||||
}
|
||||
await networkStream.FlushAsync(cancellation);
|
||||
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
|
||||
{
|
||||
using (var jsonReader = new JsonTextReader(textReader))
|
||||
{
|
||||
var resultAsync = JObject.LoadAsync(jsonReader, cancellation);
|
||||
|
||||
// without this hack resultAsync is blocking even if cancellation happen
|
||||
using (cancellation.Register(() => { socket.Dispose(); }))
|
||||
{
|
||||
var result = await resultAsync;
|
||||
var error = result.Property("error");
|
||||
if (error != null)
|
||||
{
|
||||
throw new LightningRPCException(error.Value["message"].Value<string>());
|
||||
}
|
||||
if (noReturn)
|
||||
return default(T);
|
||||
if (isArray)
|
||||
{
|
||||
return result["result"].Children().First().Children().First().ToObject<T>();
|
||||
}
|
||||
return result["result"].ToObject<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Socket> Connect()
|
||||
{
|
||||
Socket socket = null;
|
||||
EndPoint endpoint = null;
|
||||
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
|
||||
{
|
||||
var domain = Address.DnsSafeHost;
|
||||
if (!IPAddress.TryParse(domain, out IPAddress address))
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
|
||||
if (address == null)
|
||||
throw new Exception("Host not found");
|
||||
}
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
endpoint = new IPEndPoint(address, Address.Port);
|
||||
}
|
||||
else if (Address.Scheme == "unix")
|
||||
{
|
||||
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
|
||||
if (!path.StartsWith('/'))
|
||||
path = "/" + path;
|
||||
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
|
||||
{
|
||||
path = path.Remove(0, 1);
|
||||
}
|
||||
if (path.Length < 2)
|
||||
throw new FormatException("Invalid unix url");
|
||||
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
|
||||
endpoint = new UnixEndPoint(path);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
|
||||
|
||||
await socket.ConnectAsync(endpoint);
|
||||
return socket;
|
||||
}
|
||||
|
||||
public async Task<BitcoinAddress> NewAddressAsync()
|
||||
{
|
||||
var obj = await SendCommandAsync<JObject>("newaddr");
|
||||
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
|
||||
if (invoices.Length == 0)
|
||||
return null;
|
||||
return ToLightningInvoice(invoices[0]);
|
||||
}
|
||||
|
||||
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
|
||||
invoice.Label = id;
|
||||
invoice.MilliSatoshi = amount;
|
||||
invoice.Status = "unpaid";
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoice.Label,
|
||||
Amount = invoice.MilliSatoshi,
|
||||
BOLT11 = invoice.BOLT11,
|
||||
Status = invoice.Status,
|
||||
PaidAt = invoice.PaidAt
|
||||
};
|
||||
}
|
||||
|
||||
Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
|
||||
{
|
||||
return Task.FromResult<ILightningListenInvoiceSession>(this);
|
||||
}
|
||||
long lastInvoiceIndex = 99999999999;
|
||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
|
||||
lastInvoiceIndex = invoice.PayIndex.Value;
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
||||
{
|
||||
var info = await GetInfoAsync(cancellation);
|
||||
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
};
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class NodeInfo
|
||||
{
|
||||
public NodeInfo(string nodeId, string host, int port)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
if (nodeId == null)
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
Port = port;
|
||||
Host = host;
|
||||
NodeId = nodeId;
|
||||
}
|
||||
public string NodeId { get; private set; }
|
||||
public string Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}@{Host}:{Port}";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class ChannelInfo
|
||||
{
|
||||
public string State { get; set; }
|
||||
public string Owner { get; set; }
|
||||
|
||||
[JsonProperty("funding_txid")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 FundingTxId { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi_to_us")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney ToUs { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi_total")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney Total { get; set; }
|
||||
|
||||
[JsonProperty("dust_limit_satoshis")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
|
||||
public Money DustLimit { get; set; }
|
||||
|
||||
[JsonProperty("max_htlc_value_in_flight_msat")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney MaxHTLCValueInFlight { get; set; }
|
||||
|
||||
[JsonProperty("channel_reserve_satoshis")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
|
||||
public Money ChannelReserve { get; set; }
|
||||
|
||||
[JsonProperty("htlc_minimum_msat")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney HTLCMinimum { get; set; }
|
||||
|
||||
[JsonProperty("to_self_delay")]
|
||||
public int ToSelfDelay { get; set; }
|
||||
[JsonProperty("max_accepted_htlcs")]
|
||||
public int MaxAcceptedHTLCS { get; set; }
|
||||
public string[] Status { get; set; }
|
||||
}
|
||||
public class PeerInfo
|
||||
{
|
||||
public string State { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty("netaddr")]
|
||||
public string[] NetworkAddresses { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
public string Owner { get; set; }
|
||||
public ChannelInfo[] Channels { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
//
|
||||
// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets.
|
||||
//
|
||||
// Authors:
|
||||
// Gonzalo Paniagua Javier (gonzalo@ximian.com)
|
||||
//
|
||||
// (C) 2003 Ximian, Inc (http://www.ximian.com)
|
||||
//
|
||||
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Mono.Unix
|
||||
{
|
||||
[Serializable]
|
||||
public class UnixEndPoint : EndPoint
|
||||
{
|
||||
string filename;
|
||||
|
||||
public UnixEndPoint(string filename)
|
||||
{
|
||||
if (filename == null)
|
||||
throw new ArgumentNullException("filename");
|
||||
|
||||
if (filename.Length == 0)
|
||||
throw new ArgumentException("Cannot be empty.", "filename");
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public string Filename
|
||||
{
|
||||
get
|
||||
{
|
||||
return (filename);
|
||||
}
|
||||
set
|
||||
{
|
||||
filename = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override AddressFamily AddressFamily
|
||||
{
|
||||
get { return AddressFamily.Unix; }
|
||||
}
|
||||
|
||||
public override EndPoint Create(SocketAddress socketAddress)
|
||||
{
|
||||
/*
|
||||
* Should also check this
|
||||
*
|
||||
int addr = (int) AddressFamily.Unix;
|
||||
if (socketAddress [0] != (addr & 0xFF))
|
||||
throw new ArgumentException ("socketAddress is not a unix socket address.");
|
||||
|
||||
if (socketAddress [1] != ((addr & 0xFF00) >> 8))
|
||||
throw new ArgumentException ("socketAddress is not a unix socket address.");
|
||||
*/
|
||||
|
||||
if (socketAddress.Size == 2)
|
||||
{
|
||||
// Empty filename.
|
||||
// Probably from RemoteEndPoint which on linux does not return the file name.
|
||||
UnixEndPoint uep = new UnixEndPoint("a");
|
||||
uep.filename = "";
|
||||
return uep;
|
||||
}
|
||||
int size = socketAddress.Size - 2;
|
||||
byte[] bytes = new byte[size];
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = socketAddress[i + 2];
|
||||
// There may be junk after the null terminator, so ignore it all.
|
||||
if (bytes[i] == 0)
|
||||
{
|
||||
size = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string name = Encoding.Default.GetString(bytes, 0, size);
|
||||
return new UnixEndPoint(name);
|
||||
}
|
||||
|
||||
public override SocketAddress Serialize()
|
||||
{
|
||||
byte[] bytes = Encoding.Default.GetBytes(filename);
|
||||
SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1);
|
||||
// sa [0] -> family low byte, sa [1] -> family high byte
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
sa[2 + i] = bytes[i];
|
||||
|
||||
//NULL suffix for non-abstract path
|
||||
sa[2 + bytes.Length] = 0;
|
||||
|
||||
return sa;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (filename);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return filename.GetHashCode(StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override bool Equals(object o)
|
||||
{
|
||||
UnixEndPoint other = o as UnixEndPoint;
|
||||
if (other == null)
|
||||
return false;
|
||||
|
||||
return (other.filename == filename);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class ChargeClient : ILightningInvoiceClient
|
||||
{
|
||||
private Uri _Uri;
|
||||
public Uri Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Uri;
|
||||
}
|
||||
}
|
||||
private Network _Network;
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public ChargeClient(Uri uri, Network network)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
this._Uri = uri;
|
||||
this._Network = network;
|
||||
if (uri.UserInfo == null)
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
||||
var userInfo = uri.UserInfo.Split(':');
|
||||
if (userInfo.Length != 2)
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
||||
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
|
||||
}
|
||||
|
||||
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var message = CreateMessage(HttpMethod.Post, "invoice");
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
|
||||
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||
if(request.Description != null)
|
||||
parameters.Add("description", request.Description);
|
||||
message.Content = new FormUrlEncodedContent(parameters);
|
||||
var result = await _Client.SendAsync(message, cancellation);
|
||||
result.EnsureSuccessStatusCode();
|
||||
var content = await result.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
|
||||
}
|
||||
|
||||
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var socket = new ClientWebSocket();
|
||||
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
|
||||
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
|
||||
if (!uri.EndsWith('/'))
|
||||
uri += "/";
|
||||
uri += "ws";
|
||||
uri = ToWebsocketUri(uri);
|
||||
await socket.ConnectAsync(new Uri(uri), cancellation);
|
||||
return new ChargeSession(socket);
|
||||
}
|
||||
|
||||
private static string ToWebsocketUri(string uri)
|
||||
{
|
||||
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
|
||||
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
|
||||
return uri;
|
||||
}
|
||||
|
||||
public NetworkCredential Credentials { get; set; }
|
||||
|
||||
public GetInfoResponse GetInfo()
|
||||
{
|
||||
return GetInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<ChargeInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}");
|
||||
var message = await _Client.SendAsync(request, cancellation);
|
||||
if (message.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
message.EnsureSuccessStatusCode();
|
||||
var content = await message.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<ChargeInvoice>(content);
|
||||
}
|
||||
|
||||
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var request = CreateMessage(HttpMethod.Get, "info");
|
||||
var message = await _Client.SendAsync(request, cancellation);
|
||||
message.EnsureSuccessStatusCode();
|
||||
var content = await message.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
|
||||
{
|
||||
var uri = GetFullUri(path);
|
||||
var request = new HttpRequestMessage(method, uri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
|
||||
return request;
|
||||
}
|
||||
|
||||
private string GetBase64Creds()
|
||||
{
|
||||
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
|
||||
}
|
||||
|
||||
private Uri GetFullUri(string partialUrl)
|
||||
{
|
||||
var uri = _Uri.AbsoluteUri;
|
||||
if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
uri += "/";
|
||||
return new Uri(uri + partialUrl);
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await GetInvoice(invoiceId, cancellation);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
return ChargeClient.ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
async Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
|
||||
{
|
||||
return await Listen(cancellation);
|
||||
}
|
||||
|
||||
internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoice.Id ?? invoice.Label,
|
||||
Amount = invoice.MilliSatoshi,
|
||||
BOLT11 = invoice.PaymentRequest,
|
||||
PaidAt = invoice.PaidAt,
|
||||
Status = invoice.Status
|
||||
};
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
|
||||
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
||||
}
|
||||
|
||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
||||
{
|
||||
var info = await GetInfoAsync(cancellation);
|
||||
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class ChargeInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney MilliSatoshi { get; set; }
|
||||
[JsonProperty("paid_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
[JsonProperty("expires_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("payreq")]
|
||||
public string PaymentRequest { get; set; }
|
||||
public string Label { get; set; }
|
||||
}
|
||||
public class ChargeSession : ILightningListenInvoiceSession
|
||||
{
|
||||
private ClientWebSocket socket;
|
||||
|
||||
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
|
||||
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
|
||||
public ChargeSession(ClientWebSocket socket)
|
||||
{
|
||||
this.socket = socket;
|
||||
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
|
||||
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
ArraySegment<byte> _Buffer;
|
||||
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var buffer = _Buffer;
|
||||
var array = _Buffer.Array;
|
||||
var originalSize = _Buffer.Array.Length;
|
||||
var newSize = _Buffer.Array.Length;
|
||||
while (true)
|
||||
{
|
||||
var message = await socket.ReceiveAsync(buffer, cancellation);
|
||||
if (message.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
|
||||
break;
|
||||
}
|
||||
if (message.MessageType != WebSocketMessageType.Text)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
|
||||
break;
|
||||
}
|
||||
if (message.EndOfMessage)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
|
||||
try
|
||||
{
|
||||
var o = ParseMessage(buffer);
|
||||
if (newSize != originalSize)
|
||||
{
|
||||
Array.Resize(ref array, originalSize);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.Count - message.Count <= 0)
|
||||
{
|
||||
newSize *= 2;
|
||||
if (newSize > MAX_BUFFER_SIZE)
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
|
||||
Array.Resize(ref array, newSize);
|
||||
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
|
||||
}
|
||||
|
||||
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Should never happen");
|
||||
}
|
||||
|
||||
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
|
||||
private ChargeInvoice ParseMessage(ArraySegment<byte> buffer)
|
||||
{
|
||||
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
|
||||
return JsonConvert.DeserializeObject<ChargeInvoice>(str, new JsonSerializerSettings());
|
||||
}
|
||||
|
||||
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
|
||||
{
|
||||
var array = _Buffer.Array;
|
||||
if (array.Length != ORIGINAL_BUFFER_SIZE)
|
||||
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
|
||||
await socket.CloseSocket(status, description, cancellation);
|
||||
throw new WebSocketException($"The socket has been closed ({status}: {description})");
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
await this.socket.CloseSocket();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await this.socket.CloseSocket();
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
|
||||
{
|
||||
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
public LightMoney Amount { get; set; }
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class CreateInvoiceResponse
|
||||
{
|
||||
public string PayReq { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
|
||||
public class GetInfoResponse
|
||||
{
|
||||
public class GetInfoAddress
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Port { get; set; }
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public int Port { get; set; }
|
||||
public GetInfoAddress[] Address { get; set; }
|
||||
public string Version { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string Network { get; set; }
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string BOLT11 { get; set; }
|
||||
public DateTimeOffset? PaidAt
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public LightMoney Amount { get; set; }
|
||||
}
|
||||
|
||||
public class LightningNodeInformation
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public int P2PPort { get; internal set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
public interface ILightningInvoiceClient
|
||||
{
|
||||
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
|
||||
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
|
||||
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
|
||||
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
|
||||
}
|
||||
|
||||
public interface ILightningListenInvoiceSession : IDisposable
|
||||
{
|
||||
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
|
||||
}
|
||||
}
|
@ -1,573 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public enum LightMoneyUnit : ulong
|
||||
{
|
||||
BTC = 100000000000,
|
||||
MilliBTC = 100000000,
|
||||
Bit = 100000,
|
||||
Satoshi = 1000,
|
||||
MilliSatoshi = 1
|
||||
}
|
||||
|
||||
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
|
||||
{
|
||||
|
||||
|
||||
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
|
||||
private const NumberStyles BitcoinStyle =
|
||||
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
|
||||
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <param name="nRet"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryParse(string bitcoin, out LightMoney nRet)
|
||||
{
|
||||
nRet = null;
|
||||
|
||||
decimal value;
|
||||
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
nRet = new LightMoney(value, LightMoneyUnit.BTC);
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <returns></returns>
|
||||
public static LightMoney Parse(string bitcoin)
|
||||
{
|
||||
LightMoney result;
|
||||
if (TryParse(bitcoin, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
throw new FormatException("Impossible to parse the string in a bitcoin amount");
|
||||
}
|
||||
|
||||
long _MilliSatoshis;
|
||||
public long MilliSatoshi
|
||||
{
|
||||
get
|
||||
{
|
||||
return _MilliSatoshis;
|
||||
}
|
||||
// used as a central point where long.MinValue checking can be enforced
|
||||
private set
|
||||
{
|
||||
CheckLongMinValue(value);
|
||||
_MilliSatoshis = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get absolute value of the instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public LightMoney Abs()
|
||||
{
|
||||
var a = this;
|
||||
if (a < LightMoney.Zero)
|
||||
a = -a;
|
||||
return a;
|
||||
}
|
||||
|
||||
public LightMoney(int msatoshis)
|
||||
{
|
||||
MilliSatoshi = msatoshis;
|
||||
}
|
||||
|
||||
public LightMoney(uint msatoshis)
|
||||
{
|
||||
MilliSatoshi = msatoshis;
|
||||
}
|
||||
public LightMoney(Money money)
|
||||
{
|
||||
MilliSatoshi = checked(money.Satoshi * 1000);
|
||||
}
|
||||
public LightMoney(long msatoshis)
|
||||
{
|
||||
MilliSatoshi = msatoshis;
|
||||
}
|
||||
|
||||
public LightMoney(ulong msatoshis)
|
||||
{
|
||||
// overflow check.
|
||||
// ulong.MaxValue is greater than long.MaxValue
|
||||
checked
|
||||
{
|
||||
MilliSatoshi = (long)msatoshis;
|
||||
}
|
||||
}
|
||||
|
||||
public LightMoney(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
// sanity check. Only valid units are allowed
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
checked
|
||||
{
|
||||
var satoshi = amount * (long)unit;
|
||||
MilliSatoshi = (long)satoshi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Split the Money in parts without loss
|
||||
/// </summary>
|
||||
/// <param name="parts">The number of parts (must be more than 0)</param>
|
||||
/// <returns>The splitted money</returns>
|
||||
public IEnumerable<LightMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
|
||||
long remain;
|
||||
long result = DivRem(_MilliSatoshis, parts, out remain);
|
||||
|
||||
for (int i = 0; i < parts; i++)
|
||||
{
|
||||
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
private static long DivRem(long a, long b, out long result)
|
||||
{
|
||||
result = a % b;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
return new LightMoney(amount, unit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToDecimal)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToUnit(LightMoneyUnit unit)
|
||||
{
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
// overflow safe because (long / int) always fit in decimal
|
||||
// decimal operations are checked by default
|
||||
return (decimal)MilliSatoshi / (ulong)unit;
|
||||
}
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToUnit)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToDecimal(LightMoneyUnit unit)
|
||||
{
|
||||
return ToUnit(unit);
|
||||
}
|
||||
|
||||
public static LightMoney Coins(decimal coins)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Bits(decimal bits)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Cents(decimal cents)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(decimal sats)
|
||||
{
|
||||
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(ulong sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(long sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(long msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(ulong msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
#region IEquatable<Money> Members
|
||||
|
||||
public bool Equals(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
public int CompareTo(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return 1;
|
||||
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
LightMoney m = obj as LightMoney;
|
||||
if (m != null)
|
||||
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
|
||||
#if !(PORTABLE || NETCORE)
|
||||
return _MilliSatoshis.CompareTo(obj);
|
||||
#else
|
||||
return _Satoshis.CompareTo((long)obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static LightMoney operator -(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator -(LightMoney left)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(-left._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator +(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(int left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator *(LightMoney right, int left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
|
||||
}
|
||||
public static LightMoney operator *(long left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(LightMoney right, long left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator /(LightMoney left, long right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(left._MilliSatoshis / right));
|
||||
}
|
||||
|
||||
public static bool operator <(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis < right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis > right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator <=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis <= right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis >= right._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(long value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
public static implicit operator LightMoney(int value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(uint value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(ulong value)
|
||||
{
|
||||
return new LightMoney(checked((long)value));
|
||||
}
|
||||
|
||||
public static implicit operator long(LightMoney value)
|
||||
{
|
||||
return value.MilliSatoshi;
|
||||
}
|
||||
|
||||
public static implicit operator ulong(LightMoney value)
|
||||
{
|
||||
return checked((ulong)value.MilliSatoshi);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(string value)
|
||||
{
|
||||
return LightMoney.Parse(value);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
LightMoney item = obj as LightMoney;
|
||||
if (item == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(item._MilliSatoshis);
|
||||
}
|
||||
public static bool operator ==(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a._MilliSatoshis == b._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static bool operator !=(LightMoney a, LightMoney b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _MilliSatoshis.GetHashCode();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <param name="fplus">True if show + for a positive amount</param>
|
||||
/// <param name="trimExcessZero">True if trim excess zeroes</param>
|
||||
/// <returns></returns>
|
||||
public string ToString(bool fplus, bool trimExcessZero = true)
|
||||
{
|
||||
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
|
||||
(fplus ? "+" : null),
|
||||
(trimExcessZero ? "2" : "11"));
|
||||
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
|
||||
}
|
||||
|
||||
|
||||
static LightMoney _Zero = new LightMoney(0);
|
||||
public static LightMoney Zero
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Zero;
|
||||
}
|
||||
}
|
||||
|
||||
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
|
||||
{
|
||||
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
|
||||
|
||||
public object GetFormat(Type formatType)
|
||||
{
|
||||
return formatType == typeof(ICustomFormatter) ? this : null;
|
||||
}
|
||||
|
||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
||||
{
|
||||
if (!this.Equals(formatProvider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var i = 0;
|
||||
var plus = format[i] == '+';
|
||||
if (plus)
|
||||
i++;
|
||||
int decPos = 0;
|
||||
if (int.TryParse(format.Substring(i, 1), out decPos))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
var unit = format[i];
|
||||
var unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
switch (unit)
|
||||
{
|
||||
case 'B':
|
||||
unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
break;
|
||||
}
|
||||
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
|
||||
var zeros = new string('0', decPos);
|
||||
var rest = new string('#', 11 - decPos);
|
||||
var fmt = plus && val > 0 ? "+" : string.Empty;
|
||||
|
||||
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
|
||||
return string.Format(CultureInfo.InvariantCulture, fmt, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="dust">more or less amount</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, LightMoney dust)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (dust == null)
|
||||
throw new ArgumentNullException("dust");
|
||||
return (amount - this).Abs() <= dust;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="margin">error margin (between 0 and 1)</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, decimal margin)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (margin < 0.0m || margin > 1.0m)
|
||||
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
|
||||
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
|
||||
return Almost(amount, dust);
|
||||
}
|
||||
|
||||
public static LightMoney Min(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a <= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
public static LightMoney Max(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a >= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
private static void CheckLongMinValue(long value)
|
||||
{
|
||||
if (value == long.MinValue)
|
||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
||||
}
|
||||
|
||||
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
|
||||
{
|
||||
var typeOfMoneyUnit = typeof(LightMoneyUnit);
|
||||
if (!Enum.IsDefined(typeOfMoneyUnit, value))
|
||||
{
|
||||
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
int IComparable.CompareTo(object obj)
|
||||
{
|
||||
return this.CompareTo(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningClientFactory
|
||||
{
|
||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var uri = supportedPaymentMethod.GetLightningUrl();
|
||||
return CreateClient(uri, network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
|
||||
{
|
||||
if (connString.ConnectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
return new ChargeClient(connString.ToUri(true), network);
|
||||
}
|
||||
else if (connString.ConnectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
return new CLightningRPCClient(connString.ToUri(false), network);
|
||||
}
|
||||
else if (connString.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri)
|
||||
{
|
||||
Macaroon = connString.Macaroon,
|
||||
MacaroonFilePath = connString.MacaroonFilePath,
|
||||
CertificateThumbprint = connString.CertificateThumbprint,
|
||||
AllowInsecure = connString.AllowInsecure,
|
||||
}));
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)
|
||||
{
|
||||
if (!Payments.Lightning.LightningConnectionString.TryParse(connectionString, false, out var conn, out string error))
|
||||
throw new FormatException($"Invalid format ({error})");
|
||||
return Payments.Lightning.LightningClientFactory.CreateClient(conn, network);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,438 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public enum LightningConnectionType
|
||||
{
|
||||
Charge,
|
||||
CLightning,
|
||||
LndREST,
|
||||
LndGRPC
|
||||
}
|
||||
public class LightningConnectionString
|
||||
{
|
||||
static Dictionary<string, LightningConnectionType> typeMapping;
|
||||
static Dictionary<LightningConnectionType, string> typeMappingReverse;
|
||||
static LightningConnectionString()
|
||||
{
|
||||
typeMapping = new Dictionary<string, LightningConnectionType>();
|
||||
typeMapping.Add("clightning", LightningConnectionType.CLightning);
|
||||
typeMapping.Add("charge", LightningConnectionType.Charge);
|
||||
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
|
||||
typeMapping.Add("lnd-grpc", LightningConnectionType.LndGRPC);
|
||||
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
|
||||
foreach (var kv in typeMapping)
|
||||
{
|
||||
typeMappingReverse.Add(kv.Value, kv.Key);
|
||||
}
|
||||
}
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
|
||||
{
|
||||
return TryParse(str, supportLegacy, out connectionString, out var error);
|
||||
}
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
if (supportLegacy)
|
||||
{
|
||||
var parsed = TryParseLegacy(str, out connectionString, out error);
|
||||
if (!parsed)
|
||||
{
|
||||
parsed = TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
connectionString = null;
|
||||
error = null;
|
||||
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Dictionary<string, string> keyValues = new Dictionary<string, string>();
|
||||
foreach (var part in parts.Select(p => p.Trim()))
|
||||
{
|
||||
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
|
||||
if (idx == -1)
|
||||
{
|
||||
error = "The format of the connectionString should a list of key=value delimited by semicolon";
|
||||
return false;
|
||||
}
|
||||
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
|
||||
var value = part.Substring(idx + 1).Trim();
|
||||
if (keyValues.ContainsKey(key))
|
||||
{
|
||||
error = $"Duplicate key {key}";
|
||||
return false;
|
||||
}
|
||||
keyValues.Add(key, value);
|
||||
}
|
||||
|
||||
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
|
||||
|
||||
LightningConnectionString result = new LightningConnectionString();
|
||||
var type = Take(keyValues, "type");
|
||||
if (type == null)
|
||||
{
|
||||
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
|
||||
{
|
||||
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
result.ConnectionType = connectionType;
|
||||
|
||||
switch (connectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
var apiToken = Take(keyValues, "api-token");
|
||||
if (apiToken == null)
|
||||
{
|
||||
error = "The key 'api-token' is not found";
|
||||
return false;
|
||||
}
|
||||
result.Username = "api-token";
|
||||
result.Password = apiToken;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:" + str;
|
||||
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:/" + str;
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
case LightningConnectionType.LndGRPC:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for lnd connection strings";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
|
||||
var macaroon = Take(keyValues, "macaroon");
|
||||
if (macaroon != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Macaroon = Encoder.DecodeData(macaroon);
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'macaroon' format should be in hex";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
|
||||
if (macaroonFilePath != null)
|
||||
{
|
||||
if (macaroon != null)
|
||||
{
|
||||
error = $"The key 'macaroon' is already specified";
|
||||
return false;
|
||||
}
|
||||
if (!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
|
||||
return false;
|
||||
}
|
||||
result.MacaroonFilePath = macaroonFilePath;
|
||||
}
|
||||
|
||||
string securitySet = null;
|
||||
var certthumbprint = Take(keyValues, "certthumbprint");
|
||||
if (certthumbprint != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
if (bytes.Length != 32)
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
|
||||
return false;
|
||||
}
|
||||
result.CertificateThumbprint = bytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
|
||||
return false;
|
||||
}
|
||||
securitySet = "certthumbprint";
|
||||
}
|
||||
|
||||
var allowinsecureStr = Take(keyValues, "allowinsecure");
|
||||
|
||||
if (allowinsecureStr != null)
|
||||
{
|
||||
var allowedValues = new[] { "true", "false" };
|
||||
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = $"The key 'allowinsecure' should be true or false";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
if (securitySet != null && allowInsecure)
|
||||
{
|
||||
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
|
||||
return false;
|
||||
}
|
||||
result.AllowInsecure = allowInsecure;
|
||||
}
|
||||
|
||||
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
|
||||
{
|
||||
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
}
|
||||
|
||||
if (keyValues.Count != 0)
|
||||
{
|
||||
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
public LightningConnectionString Clone()
|
||||
{
|
||||
LightningConnectionString.TryParse(this.ToString(), false, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string Take(Dictionary<string, string> keyValues, string key)
|
||||
{
|
||||
if (keyValues.TryGetValue(key, out var v))
|
||||
keyValues.Remove(key);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str.StartsWith('/'))
|
||||
str = "unix:" + str;
|
||||
var result = new LightningConnectionString();
|
||||
connectionString = null;
|
||||
error = null;
|
||||
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
{
|
||||
error = "Invalid URL";
|
||||
return false;
|
||||
}
|
||||
|
||||
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
|
||||
if (!supportedDomains.Contains(uri.Scheme))
|
||||
{
|
||||
var protocols = String.Join(",", supportedDomains);
|
||||
error = $"The url support the following protocols {protocols}";
|
||||
return false;
|
||||
}
|
||||
if (uri.Scheme == "unix")
|
||||
{
|
||||
str = uri.AbsoluteUri.Substring("unix:".Length);
|
||||
while (str.Length >= 1 && str[0] == '/')
|
||||
{
|
||||
str = str.Substring(1);
|
||||
}
|
||||
uri = new Uri("unix://" + str, UriKind.Absolute);
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "tcp")
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
|
||||
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||||
{
|
||||
var parts = uri.UserInfo.Split(':');
|
||||
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
|
||||
{
|
||||
error = "The url is missing user and password";
|
||||
return false;
|
||||
}
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
result.ConnectionType = LightningConnectionType.Charge;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
error = "The url should not have user information";
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
result.IsLegacy = true;
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
public LightningConnectionString()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public Uri BaseUri { get; set; }
|
||||
public bool IsLegacy { get; private set; }
|
||||
|
||||
public LightningConnectionType ConnectionType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
|
||||
public Uri ToUri(bool withCredentials)
|
||||
{
|
||||
if (withCredentials)
|
||||
{
|
||||
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
return BaseUri;
|
||||
}
|
||||
}
|
||||
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
|
||||
public override string ToString()
|
||||
{
|
||||
var type = typeMappingReverse[ConnectionType];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append($"type={type}");
|
||||
switch (ConnectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
if (Username == null || Username == "api-token")
|
||||
{
|
||||
builder.Append($";server={BaseUri};api-token={Password}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
builder.Append($";server={BaseUri}");
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
case LightningConnectionType.LndGRPC:
|
||||
if (Username == null)
|
||||
{
|
||||
builder.Append($";server={BaseUri}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
if (Macaroon != null)
|
||||
{
|
||||
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
|
||||
}
|
||||
if (MacaroonFilePath != null)
|
||||
{
|
||||
builder.Append($";macaroonfilepath={MacaroonFilePath}");
|
||||
}
|
||||
if (CertificateThumbprint != null)
|
||||
{
|
||||
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
|
||||
}
|
||||
if (AllowInsecure)
|
||||
{
|
||||
builder.Append($";allowinsecure=true");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(type);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.JsonConverters;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -7,8 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
@ -18,12 +17,9 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningLikePaymentHandler(
|
||||
LightningClientFactory lightningClientFactory,
|
||||
NBXplorerDashboard dashboard)
|
||||
{
|
||||
_LightningClientFactory = lightningClientFactory;
|
||||
_Dashboard = dashboard;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
@ -32,7 +28,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var test = Test(supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
@ -74,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
LightningNodeInformation info = null;
|
||||
try
|
||||
{
|
||||
@ -89,7 +85,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if (info.Address == null)
|
||||
if (info.NodeInfo == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
@ -100,7 +96,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
return info.NodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -27,16 +28,13 @@ namespace BTCPayServer.Payments.Lightning
|
||||
EventAggregator _Aggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningListener(EventAggregator aggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
LightningClientFactory lightningClientFactory,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_Aggregator = aggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_LightningClientFactory = lightningClientFactory;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -100,22 +98,22 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
if (poll)
|
||||
{
|
||||
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
|
||||
var charge = lightningSupportedMethod.CreateClient(network);
|
||||
LightningInvoice chargeInvoice = null;
|
||||
try
|
||||
{
|
||||
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
|
||||
continue;
|
||||
}
|
||||
if (chargeInvoice == null)
|
||||
continue;
|
||||
if (chargeInvoice.Status == "paid")
|
||||
if (chargeInvoice.Status == LightningInvoiceStatus.Paid)
|
||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
||||
if (chargeInvoice.Status == LightningInvoiceStatus.Paid || chargeInvoice.Status == LightningInvoiceStatus.Expired)
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -143,11 +141,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
ILightningListenInvoiceSession session = null;
|
||||
ILightningInvoiceListener session = null;
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
var lightningClient = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var lightningClient = supportedPaymentMethod.CreateClient(network);
|
||||
session = await lightningClient.Listen(_Cts.Token);
|
||||
while (true)
|
||||
{
|
||||
@ -158,13 +156,13 @@ namespace BTCPayServer.Payments.Lightning
|
||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
{
|
||||
if (notification.Status == "paid" && notification.PaidAt.HasValue)
|
||||
if (notification.Status == LightningInvoiceStatus.Paid && notification.PaidAt.HasValue)
|
||||
{
|
||||
await AddPayment(network, notification, listenedInvoice);
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
if (notification.Status == "expired")
|
||||
if (notification.Status == LightningInvoiceStatus.Expired)
|
||||
{
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
@ -197,7 +195,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
||||
if(invoice != null)
|
||||
if (invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -28,7 +30,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (!string.IsNullOrEmpty(LightningConnectionString))
|
||||
{
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
||||
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
@ -37,7 +39,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
else
|
||||
{
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
||||
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
@ -58,5 +60,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
LightningChargeUrl = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public ILightningClient CreateClient(BTCPayNetwork network)
|
||||
{
|
||||
return LightningClientFactory.CreateClient(this.GetLightningUrl(), network.NBitcoinNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public abstract class LndAuthentication
|
||||
{
|
||||
public class FixedMacaroonAuthentication : LndAuthentication
|
||||
{
|
||||
public FixedMacaroonAuthentication(byte[] macaroon)
|
||||
{
|
||||
if (macaroon == null)
|
||||
throw new ArgumentNullException(nameof(macaroon));
|
||||
Macaroon = macaroon;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon));
|
||||
}
|
||||
}
|
||||
public class NullAuthentication : LndAuthentication
|
||||
{
|
||||
public static NullAuthentication Instance { get; } = new NullAuthentication();
|
||||
|
||||
private NullAuthentication()
|
||||
{
|
||||
|
||||
}
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class MacaroonFileAuthentication : LndAuthentication
|
||||
{
|
||||
public MacaroonFileAuthentication(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
// Because this dump the whole file, let's make sure it is indeed the macaroon
|
||||
if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath));
|
||||
FilePath = filePath;
|
||||
}
|
||||
public string FilePath { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(FilePath);
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void AddAuthentication(HttpRequestMessage httpRequest);
|
||||
}
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndInvoiceClient : ILightningInvoiceClient
|
||||
{
|
||||
class LndInvoiceClientSession : ILightningListenInvoiceSession
|
||||
{
|
||||
private LndSwaggerClient _Parent;
|
||||
Channel<LightningInvoice> _Invoices = Channel.CreateBounded<LightningInvoice>(50);
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
|
||||
|
||||
HttpClient _Client;
|
||||
HttpResponseMessage _Response;
|
||||
Stream _Body;
|
||||
StreamReader _Reader;
|
||||
Task _ListenLoop;
|
||||
|
||||
public LndInvoiceClientSession(LndSwaggerClient parent)
|
||||
{
|
||||
_Parent = parent;
|
||||
}
|
||||
|
||||
public Task StartListening()
|
||||
{
|
||||
try
|
||||
{
|
||||
_Client = _Parent.CreateHttpClient();
|
||||
_Client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, _Parent.BaseUrl.WithTrailingSlash() + "v1/invoices/subscribe");
|
||||
_Parent._Authentication.AddAuthentication(request);
|
||||
_ListenLoop = ListenLoop(request);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ListenLoop(HttpRequestMessage request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_Response = await _Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token);
|
||||
_Body = await _Response.Content.ReadAsStreamAsync();
|
||||
_Reader = new StreamReader(_Body);
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
string line = await _Reader.ReadLineAsync().WithCancellation(_Cts.Token);
|
||||
if (line != null)
|
||||
{
|
||||
if (line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var invoiceString = JObject.Parse(line)["result"].ToString();
|
||||
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
|
||||
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice), _Cts.Token);
|
||||
}
|
||||
else if (line.StartsWith("{\"error\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var errorString = JObject.Parse(line)["error"].ToString();
|
||||
var error = _Parent.Deserialize<LndError>(errorString);
|
||||
throw new LndException(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new LndException("Unknown result from LND: " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_Invoices.Writer.TryComplete(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _Invoices.Reader.ReadAsync(cancellation);
|
||||
}
|
||||
catch (ChannelClosedException ex) when (ex.InnerException == null)
|
||||
{
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
catch (ChannelClosedException ex)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
void Dispose(bool waitLoop)
|
||||
{
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
_Cts.Cancel();
|
||||
_Reader?.Dispose();
|
||||
_Reader = null;
|
||||
_Body?.Dispose();
|
||||
_Body = null;
|
||||
_Response?.Dispose();
|
||||
_Response = null;
|
||||
_Client?.Dispose();
|
||||
_Client = null;
|
||||
if (waitLoop)
|
||||
_ListenLoop?.Wait();
|
||||
_Invoices.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LndSwaggerClient _rpcClient;
|
||||
|
||||
public LndInvoiceClient(LndSwaggerClient swaggerClient)
|
||||
{
|
||||
if (swaggerClient == null)
|
||||
throw new ArgumentNullException(nameof(swaggerClient));
|
||||
_rpcClient = swaggerClient;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var strAmount = ConvertInv.ToString(amount.ToUnit(LightMoneyUnit.Satoshi));
|
||||
var strExpiry = ConvertInv.ToString(Math.Round(expiry.TotalSeconds, 0));
|
||||
// lnd requires numbers sent as strings. don't ask
|
||||
var resp = await _rpcClient.AddInvoiceAsync(new LnrpcInvoice
|
||||
{
|
||||
Value = strAmount,
|
||||
Memo = description,
|
||||
Expiry = strExpiry
|
||||
});
|
||||
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = amount,
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.GetInfoAsync(cancellation);
|
||||
|
||||
var nodeInfo = new LightningNodeInformation
|
||||
{
|
||||
BlockHeight = (int?)resp.Block_height ?? 0,
|
||||
NodeId = resp.Identity_pubkey
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
|
||||
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
|
||||
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
|
||||
|
||||
var firstNodeInfo = node.Node.Addresses.First();
|
||||
var externalHostPort = firstNodeInfo.Addr.Split(':');
|
||||
|
||||
nodeInfo.Address = externalHostPort[0];
|
||||
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
catch (SwaggerException ex) when (!string.IsNullOrEmpty(ex.Response))
|
||||
{
|
||||
throw new Exception("LND threw an error: " + ex.Response);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
|
||||
return ConvertLndInvoice(resp);
|
||||
}
|
||||
|
||||
public async Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var session = new LndInvoiceClientSession(this._rpcClient);
|
||||
await session.StartListening();
|
||||
return session;
|
||||
}
|
||||
|
||||
internal static LightningInvoice ConvertLndInvoice(LnrpcInvoice resp)
|
||||
{
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
// TODO: Verify id corresponds to R_hash
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = new LightMoney(ConvertInv.ToInt64(resp.Value), LightMoneyUnit.Satoshi),
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
|
||||
if (resp.Settle_date != null)
|
||||
{
|
||||
invoice.PaidAt = DateTimeOffset.FromUnixTimeSeconds(ConvertInv.ToInt64(resp.Settle_date));
|
||||
invoice.Status = "paid";
|
||||
}
|
||||
else
|
||||
{
|
||||
var invoiceExpiry = ConvertInv.ToInt64(resp.Creation_date) + ConvertInv.ToInt64(resp.Expiry);
|
||||
if (DateTimeOffset.FromUnixTimeSeconds(invoiceExpiry) > DateTimeOffset.UtcNow)
|
||||
{
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
|
||||
// utility static methods... maybe move to separate class
|
||||
private static string BitString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes)
|
||||
.Replace("-", "", StringComparison.InvariantCulture)
|
||||
.ToLower(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Invariant culture conversion
|
||||
public static class ConvertInv
|
||||
{
|
||||
public static int ToInt32(string str)
|
||||
{
|
||||
return Convert.ToInt32(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static long ToInt64(string str)
|
||||
{
|
||||
return Convert.ToInt64(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static string ToString(decimal d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string ToString(double d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndRestSettings
|
||||
{
|
||||
public LndRestSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public LndRestSettings(Uri uri)
|
||||
{
|
||||
Uri = uri;
|
||||
}
|
||||
public Uri Uri { get; set; }
|
||||
/// <summary>
|
||||
/// The SHA256 of the PEM certificate
|
||||
/// </summary>
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
|
||||
public LndAuthentication CreateLndAuthentication()
|
||||
{
|
||||
if (Macaroon != null)
|
||||
return new LndAuthentication.FixedMacaroonAuthentication(Macaroon);
|
||||
if (!string.IsNullOrEmpty(MacaroonFilePath))
|
||||
return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath);
|
||||
return LndAuthentication.NullAuthentication.Instance;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndException : Exception
|
||||
{
|
||||
public LndException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
public LndException(LndError error) : base(error.Message)
|
||||
{
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
_Error = error;
|
||||
}
|
||||
|
||||
|
||||
private readonly LndError _Error;
|
||||
public LndError Error
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
// {"grpc_code":2,"http_code":500,"message":"rpc error: code = Unknown desc = expected 1 macaroon, got 0","http_status":"Internal Server Error"}
|
||||
public class LndError
|
||||
{
|
||||
[JsonProperty("grpc_code")]
|
||||
public int GRPCCode { get; set; }
|
||||
[JsonProperty("http_code")]
|
||||
public int HttpCode { get; set; }
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
[JsonProperty("http_status")]
|
||||
public string HttpStatus { get; set; }
|
||||
}
|
||||
public partial class LndSwaggerClient
|
||||
{
|
||||
public LndSwaggerClient(LndRestSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
_LndSettings = settings;
|
||||
_Authentication = settings.CreateLndAuthentication();
|
||||
BaseUrl = settings.Uri.AbsoluteUri.TrimEnd('/');
|
||||
_httpClient = CreateHttpClient(settings);
|
||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
|
||||
{
|
||||
var json = new Newtonsoft.Json.JsonSerializerSettings();
|
||||
UpdateJsonSerializerSettings(json);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
LndRestSettings _LndSettings;
|
||||
internal LndAuthentication _Authentication;
|
||||
|
||||
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
|
||||
{
|
||||
_Authentication.AddAuthentication(request);
|
||||
}
|
||||
|
||||
internal static HttpClient CreateHttpClient(LndRestSettings settings)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
SslProtocols = SslProtocols.Tls12
|
||||
};
|
||||
|
||||
var expectedThumbprint = settings.CertificateThumbprint?.ToArray();
|
||||
if (expectedThumbprint != null)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
|
||||
{
|
||||
var actualCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
|
||||
var hash = actualCert.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
return hash.SequenceEqual(expectedThumbprint);
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.AllowInsecure)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (settings.Uri.Scheme == "http")
|
||||
throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https");
|
||||
}
|
||||
return new HttpClient(handler);
|
||||
}
|
||||
|
||||
internal HttpClient CreateHttpClient()
|
||||
{
|
||||
return LndSwaggerClient.CreateHttpClient(_LndSettings);
|
||||
}
|
||||
|
||||
internal T Deserialize<T>(string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(str, _settings.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
@ -15,11 +15,14 @@ using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Threading;
|
||||
using Serilog;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class Program
|
||||
{
|
||||
private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB.
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
ServicePointManager.DefaultConnectionLimit = 100;
|
||||
@ -31,7 +34,7 @@ namespace BTCPayServer
|
||||
var logger = loggerFactory.CreateLogger("Configuration");
|
||||
try
|
||||
{
|
||||
// This is the only way toat LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
|
||||
// This is the only way that LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
|
||||
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
|
||||
if (conf == null)
|
||||
return;
|
||||
@ -50,6 +53,20 @@ namespace BTCPayServer
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||
|
||||
// Use Serilog for debug log file.
|
||||
string debugLogFile = conf.GetOrDefault<string>("debuglog", null);
|
||||
if (String.IsNullOrEmpty(debugLogFile) == false)
|
||||
{
|
||||
Serilog.Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1)
|
||||
.CreateLogger();
|
||||
|
||||
l.AddSerilog(Serilog.Log.Logger);
|
||||
logger.LogDebug($"Debug log file configured for {debugLogFile}.");
|
||||
}
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
@ -73,6 +90,7 @@ namespace BTCPayServer
|
||||
Logs.Configuration.LogError("Configuration error");
|
||||
if (host != null)
|
||||
host.Dispose();
|
||||
Serilog.Log.CloseAndFlush();
|
||||
loggerProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,11 @@
|
||||
"profiles": {
|
||||
"Docker-Regtest": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "--debuglog debug.log",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_BUNDLEJSCSS": "false",
|
||||
"BTCPAY_BUNDLEJSCSS": "true",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
@ -17,4 +18,4 @@
|
||||
"applicationUrl": "http://127.0.0.1:14142/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ namespace BTCPayServer.Rating
|
||||
if (CurrencyPair.TryParse(id.Identifier.ValueText, out var currencyPair))
|
||||
{
|
||||
expression = expression.WithTriviaFrom(expression);
|
||||
ExpressionsByPair.Add(currencyPair, (expression, id));
|
||||
ExpressionsByPair.TryAdd(currencyPair, (expression, id));
|
||||
}
|
||||
}
|
||||
base.VisitAssignmentExpression(node);
|
||||
|
@ -88,8 +88,9 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
if (storeId != null)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, storeId));
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
store.AdditionalClaims.AddRange(claims);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
@ -209,14 +210,14 @@ namespace BTCPayServer.Security
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
|
@ -12,9 +12,9 @@ namespace BTCPayServer.Security
|
||||
public const string CookieAuthentication = "Identity.Application";
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
AddClaim(options, CanUseStore.Key);
|
||||
AddClaim(options, CanModifyStoreSettings.Key);
|
||||
AddClaim(options, CanModifyServerSettings.Key);
|
||||
AddClaim(options, CanCreateInvoice.Key);
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -27,13 +27,14 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifyserversettings";
|
||||
}
|
||||
public class CanUseStore
|
||||
{
|
||||
public const string Key = "btcpay.store.canusestore";
|
||||
}
|
||||
public class CanModifyStoreSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||
}
|
||||
|
||||
public class CanCreateInvoice
|
||||
{
|
||||
public const string Key = "btcpay.store.cancreateinvoice";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,6 @@ namespace BTCPayServer.Services.Fees
|
||||
ExplorerClient _ExplorerClient;
|
||||
public async Task<FeeRate> GetFeeRateAsync()
|
||||
{
|
||||
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
|
||||
return _Factory.Fallback;
|
||||
try
|
||||
{
|
||||
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
|
||||
|
@ -283,6 +283,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string NotificationEmail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string NotificationURL
|
||||
{
|
||||
get;
|
||||
|
@ -40,7 +40,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
private CustomThreadPool _IndexerThread;
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
|
||||
{
|
||||
_Engine = new DBreezeEngine(dbreezePath);
|
||||
int retryCount = 0;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
_Engine = new DBreezeEngine(dbreezePath);
|
||||
}
|
||||
catch when (retryCount++ < 5) { goto retry; }
|
||||
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
@ -148,7 +154,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
|
||||
foreach(var log in creationLogs.ToList())
|
||||
foreach (var log in creationLogs.ToList())
|
||||
{
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
@ -249,7 +255,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch(DbUpdateException) { } // Probably the invoice does not exists anymore
|
||||
catch (DbUpdateException) { } // Probably the invoice does not exists anymore
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,7 +447,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => statusSet.Contains(i.Status));
|
||||
}
|
||||
|
||||
if(queryObject.Unusual != null)
|
||||
if (queryObject.Unusual != null)
|
||||
{
|
||||
var unused = queryObject.Unusual.Value;
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
|
||||
@ -554,7 +560,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch(DbUpdateException) { return null; } // Already exists
|
||||
catch (DbUpdateException) { return null; } // Already exists
|
||||
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
|
||||
return entity;
|
||||
}
|
||||
|
@ -29,9 +29,16 @@ namespace BTCPayServer.Services
|
||||
new Language("pt-PT", "Portuguese"),
|
||||
new Language("pt-BR", "Portuguese (Brazil)"),
|
||||
new Language("nl-NL", "Dutch"),
|
||||
new Language("np-NP", "नेपाली"),
|
||||
new Language("cs-CZ", "Česky"),
|
||||
new Language("is-IS", "Íslenska"),
|
||||
new Language("hr-HR", "Croatian"),
|
||||
new Language("it-IT", "Italiano"),
|
||||
new Language("kk-KZ", "Қазақша"),
|
||||
new Language("ru-RU", "русский"),
|
||||
new Language("uk-UA", "Українська"),
|
||||
new Language("vi-VN", "Tiếng Việt"),
|
||||
new Language("zh-SP", "中文(简体)"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Services.Rates
|
||||
public DateTimeOffset NextRefresh;
|
||||
public DateTimeOffset Expiration;
|
||||
public Exception Exception;
|
||||
|
||||
public string ExchangeName;
|
||||
internal ExchangeRates GetResult()
|
||||
{
|
||||
if (Expiration <= DateTimeOffset.UtcNow)
|
||||
@ -29,7 +29,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("The rate has expired");
|
||||
throw new InvalidOperationException($"The rate has expired ({ExchangeName})");
|
||||
}
|
||||
}
|
||||
return Latest;
|
||||
@ -128,6 +128,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
var previous = _Latest;
|
||||
var fetch = new LatestFetch();
|
||||
fetch.ExchangeName = GetExchangeName();
|
||||
try
|
||||
{
|
||||
var rates = await _Inner.GetRatesAsync();
|
||||
|
@ -101,6 +101,40 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyProviders.TryAdd(code, number);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
|
||||
/// </summary>
|
||||
/// <param name="value">The value</param>
|
||||
/// <param name="currency">Currency code</param>
|
||||
/// <param name="threeLetterSuffix">Add three letter suffix (like USD)</param>
|
||||
/// <returns></returns>
|
||||
public string DisplayFormatCurrency(decimal value, string currency, bool threeLetterSuffix = true)
|
||||
{
|
||||
var provider = GetNumberFormatInfo(currency, true);
|
||||
var currencyData = GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return value.ToString("C", provider);
|
||||
else
|
||||
return value.ToString("C", provider) + $" ({currency})";
|
||||
}
|
||||
|
||||
Dictionary<string, CurrencyData> _Currencies;
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
@ -135,13 +169,16 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
{
|
||||
dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||
{
|
||||
Code = network.CryptoCode,
|
||||
Divisibility = 8,
|
||||
Name = network.CryptoCode,
|
||||
Crypto = true
|
||||
});
|
||||
}))
|
||||
{
|
||||
dico[network.CryptoCode].Crypto = true;
|
||||
}
|
||||
}
|
||||
|
||||
return dico.Values.ToArray();
|
||||
@ -150,9 +187,9 @@ namespace BTCPayServer.Services.Rates
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
CurrencyData result;
|
||||
if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
|
||||
if (!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
|
||||
{
|
||||
if(useFallback)
|
||||
if (useFallback)
|
||||
{
|
||||
var usd = GetCurrencyData("USD", false);
|
||||
result = new CurrencyData()
|
||||
|
@ -105,7 +105,6 @@ namespace BTCPayServer.Services.Rates
|
||||
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
|
||||
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
|
||||
|
@ -1,8 +1,19 @@
|
||||
@{
|
||||
ViewData["Title"] = "Locked out";
|
||||
}
|
||||
<section>
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h2 class="text-danger">@ViewData["Title"]</h2>
|
||||
<p class="text-danger">This account has been locked out, please try again later.</p>
|
||||
</header>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 lead">
|
||||
<p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -53,7 +53,7 @@
|
||||
{
|
||||
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
|
||||
}
|
||||
<a asp-action="@app.ViewAction" asp-controller="Apps" asp-route-appId="@app.Id">View</a><span> - </span>
|
||||
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a><span> - </span>
|
||||
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model ViewPointOfSaleViewModel
|
||||
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
Layout = null;
|
||||
@ -21,7 +21,7 @@
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<div class="row">
|
||||
@for(int i = 0; i < Model.Items.Length; i++)
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
|
||||
var item = Model.Items[i];
|
||||
@ -32,10 +32,11 @@
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
@if(Model.ShowCustomAmount)
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
|
||||
<div class="col-sm-3"> </div>
|
||||
<div class="col-sm-6">
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
|
||||
@ -44,6 +45,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-3"> </div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
@ -57,14 +57,14 @@
|
||||
<div class="container text-center">
|
||||
<h2>Video tutorials</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="col-md-2 text-center">
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank">
|
||||
<img src="~/img/youtube.png" height="225" width="400" />
|
||||
<img src="~/img/youtube.png" class="img-fluid" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<div class="col-md-2 text-center">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user