Compare commits
160 Commits
Author | SHA1 | Date | |
---|---|---|---|
65dcfd3549 | |||
6976fc54ca | |||
0e077ff5c4 | |||
c2f171a729 | |||
fea38758e4 | |||
444733565b | |||
96d28f00cc | |||
70cc79a77f | |||
8d10186fdf | |||
6f7e0205f8 | |||
7ef11817c1 | |||
c387c84861 | |||
ae7ad9f667 | |||
c55f1185e6 | |||
1619666bef | |||
bf784f6fd7 | |||
13e330fa65 | |||
827b133534 | |||
4067d4b00f | |||
359d8c5c6a | |||
265b7364e8 | |||
dc2b8c9e4c | |||
37869fd049 | |||
d7ada4d493 | |||
f093f85dbf | |||
1cf17872ab | |||
c79751829b | |||
7a21c03896 | |||
54f07139db | |||
d78990fbd5 | |||
9ed7dbc838 | |||
9b12c7bc57 | |||
8973c75bbc | |||
f425df7b6d | |||
a44b600c5e | |||
7f0a42c2d5 | |||
60cd864226 | |||
71cf02915e | |||
327d2298fb | |||
2ca11ed692 | |||
0224815a60 | |||
df824c36d2 | |||
66e7777b1a | |||
7ff85a86bf | |||
7b3700c2c6 | |||
04679aefd6 | |||
5190639b77 | |||
0bf73abb39 | |||
7e0211924d | |||
a8a857a7ce | |||
1d18965a26 | |||
e020b86a3f | |||
1b80b90609 | |||
efc3512994 | |||
acb8ca982f | |||
adc42cbba4 | |||
7edcb7ef5f | |||
656017c6df | |||
35db6d4a8b | |||
2741187546 | |||
c3a7ab647c | |||
92da0ec2d2 | |||
c767a49f2d | |||
ea8196b532 | |||
58f138e854 | |||
b4b6939498 | |||
bc97c07670 | |||
cf27fe5a53 | |||
449066449b | |||
eb5e32a07f | |||
708cdbe23f | |||
333de52c33 | |||
6b9932fa14 | |||
6c45689e6a | |||
1e3307c84c | |||
d0eed9857d | |||
4221763f48 | |||
184c797b0e | |||
4853e15d8a | |||
6b4b903669 | |||
05da63f2a5 | |||
5b4b073fc8 | |||
4723a83dbb | |||
78350db62d | |||
e8b71f36b2 | |||
6db9061dd1 | |||
320826a4b9 | |||
ad0edb5f4c | |||
2856c10bc3 | |||
24aa18e9ed | |||
767eca97cb | |||
73d5415ea9 | |||
e5a26cfca8 | |||
e40cd1fc0c | |||
978b7d930e | |||
0f2e3ef957 | |||
275b590e80 | |||
5d9da82d8e | |||
1a122726b7 | |||
0bd02a9272 | |||
3cce7b8b35 | |||
e3ab1f5228 | |||
4c875d9c7c | |||
e79334a6f6 | |||
a09c6d51e6 | |||
312c7b7193 | |||
ee733fee28 | |||
4d7e9d3f8a | |||
873c0a183a | |||
ea53ae8f20 | |||
686bc3380d | |||
67da20bcea | |||
561644f75b | |||
1abc89858f | |||
91c63a8ee6 | |||
563882d30b | |||
9a5eeee794 | |||
0578a692db | |||
f74f06338a | |||
1281f348bf | |||
5e76d4bfc1 | |||
2a302ea346 | |||
be90172840 | |||
b85ee895f5 | |||
93de408e07 | |||
d3662ae734 | |||
132d7795ea | |||
bf5a624209 | |||
abbdbda03a | |||
8a8593437a | |||
e203cada54 | |||
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
5c618233cb | |||
9e91259b9e | |||
014d08f38a | |||
7998ea142b | |||
a4051dac72 | |||
e3a8892d24 | |||
ea02d77e69 | |||
4f582a6712 | |||
4769b1d452 | |||
17b18d820f | |||
26f34e75c2 | |||
6f50ac50ec | |||
5261cfcdd3 | |||
675920697f | |||
24699bf2ba | |||
5ab92ed794 | |||
8e83f0faa1 | |||
30d5add2ea | |||
6e47babf45 | |||
9b95fa1f20 | |||
c67aa14a87 | |||
23f296ef34 | |||
c6ce676ad3 |
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -35,6 +35,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -120,7 +121,8 @@ namespace BTCPayServer.Tests
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
HttpClient = new HttpClient();
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
@ -209,6 +211,8 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
|
@ -1,12 +1,9 @@
|
||||
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
@ -35,27 +36,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public void Advance(TimeSpan time)
|
||||
public async Task Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
List<WaitObj> overdue = new List<WaitObj>();
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
wait.CTS.TrySetResult(true);
|
||||
overdue.Add(wait);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var o in overdue)
|
||||
o.CTS.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void AdvanceMilliseconds(long milli)
|
||||
{
|
||||
Advance(TimeSpan.FromMilliseconds(milli));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -85,9 +86,11 @@ namespace BTCPayServer.Tests
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task EnsureChannelsSetup()
|
||||
public async Task EnsureChannelsSetup()
|
||||
{
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
Logs.Tester.LogInformation("Connecting channels");
|
||||
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
|
||||
Logs.Tester.LogInformation("Channels connected");
|
||||
}
|
||||
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
|
@ -55,6 +55,7 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -146,6 +147,45 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseTorrc()
|
||||
{
|
||||
var nl = "\n";
|
||||
var input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
nl = Environment.NewLine;
|
||||
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out var torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
nl = "\r\n";
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80";
|
||||
nl = Environment.NewLine;
|
||||
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
@ -361,7 +401,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetLightningServer()
|
||||
{
|
||||
@ -399,21 +439,21 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCLightning()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentLnd()
|
||||
{
|
||||
@ -453,7 +493,9 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
|
||||
Logs.Tester.LogInformation($"Trying to send Lightning payment to {invoice.Id}");
|
||||
await tester.SendLightningPaymentAsync(invoice);
|
||||
Logs.Tester.LogInformation($"Lightning payment to {invoice.Id} is sent");
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
|
||||
@ -476,7 +518,6 @@ namespace BTCPayServer.Tests
|
||||
var controller = acc.GetController<StoresController>();
|
||||
var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "bla",
|
||||
PublicKey = null
|
||||
}).GetAwaiter().GetResult();
|
||||
@ -727,6 +768,18 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
// We don't have any default currencies, so this should be failing
|
||||
Assert.Null(GetRatesResult?.Data);
|
||||
|
||||
var store = acc.GetController<StoresController>();
|
||||
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates(acc.StoreId)).Model);
|
||||
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
|
||||
store.Rates(ratesVM).Wait();
|
||||
store = acc.GetController<StoresController>();
|
||||
rateController = acc.GetController<RateController>();
|
||||
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
// Now we should have a result
|
||||
Assert.NotNull(GetRatesResult);
|
||||
Assert.NotNull(GetRatesResult.Data);
|
||||
Assert.Equal(2, GetRatesResult.Data.Length);
|
||||
@ -869,7 +922,7 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
foreach(var req in new[]
|
||||
foreach (var req in new[]
|
||||
{
|
||||
"invoices/",
|
||||
"invoices",
|
||||
@ -918,7 +971,6 @@ namespace BTCPayServer.Tests
|
||||
var storeController = user.GetController<StoresController>();
|
||||
storeController.CreateToken(new CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "test2",
|
||||
StoreId = user.StoreId
|
||||
}).GetAwaiter().GetResult();
|
||||
@ -1006,7 +1058,7 @@ namespace BTCPayServer.Tests
|
||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
|
||||
{
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
|
||||
vm.PreferredExchange = exchange;
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -1021,6 +1073,49 @@ namespace BTCPayServer.Tests
|
||||
return invoice2.CryptoInfo[0].Rate;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseAnyoneCanCreateInvoice()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
Logs.Tester.LogInformation("StoreId without anyone can create invoice = 401");
|
||||
var response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
|
||||
{
|
||||
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
Assert.Equal(401, (int)response.StatusCode);
|
||||
|
||||
Logs.Tester.LogInformation("No store without anyone can create invoice = 404 because the bitpay API can't know the storeid");
|
||||
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices")
|
||||
{
|
||||
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
Assert.Equal(404, (int)response.StatusCode);
|
||||
|
||||
user.ModifyStore(s => s.AnyoneCanCreateInvoice = true);
|
||||
|
||||
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 401");
|
||||
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId=badid")
|
||||
{
|
||||
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
Assert.Equal(401, (int)response.StatusCode);
|
||||
|
||||
Logs.Tester.LogInformation("Good store with anyone can create invoice = 200");
|
||||
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
|
||||
{
|
||||
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanTweakRate()
|
||||
@ -1044,7 +1139,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
|
||||
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
|
||||
Assert.Equal(0.0, vm.Spread);
|
||||
vm.Spread = 40;
|
||||
storeController.Rates(vm).Wait();
|
||||
@ -1143,7 +1238,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var store = user.GetController<StoresController>();
|
||||
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.False(rateVm.ShowScripting);
|
||||
Assert.Equal("coinaverage", rateVm.PreferredExchange);
|
||||
Assert.Equal(0.0, rateVm.Spread);
|
||||
@ -1151,7 +1246,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
rateVm.PreferredExchange = "bitflyer";
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||
|
||||
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||
@ -1168,7 +1263,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
store = user.GetController<StoresController>();
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal(rateVm.StoreId, user.StoreId);
|
||||
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
rateVm.ScriptTest = "BTC_JPY";
|
||||
@ -1185,7 +1281,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
store = user.GetController<StoresController>();
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal(50, rateVm.Spread);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||
@ -1457,7 +1553,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
@ -1612,21 +1708,20 @@ donation:
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanScheduleBackgroundTasks()
|
||||
public async Task CanScheduleBackgroundTasks()
|
||||
{
|
||||
BackgroundJobClient client = new BackgroundJobClient();
|
||||
MockDelay mockDelay = new MockDelay();
|
||||
client.Delay = mockDelay;
|
||||
bool[] jobs = new bool[4];
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
|
||||
client.Schedule((_) => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(5.0));
|
||||
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
|
||||
client.Schedule((_) => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(2.0));
|
||||
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
|
||||
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
|
||||
client.Schedule((_) => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
|
||||
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
|
||||
client.Schedule((_) => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(7.0));
|
||||
|
||||
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
@ -1636,52 +1731,52 @@ donation:
|
||||
|
||||
var waitJobsFinish = client.WaitAllRunning(default);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(2.0));
|
||||
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(3.0));
|
||||
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
|
||||
Assert.Equal(1, client.GetExecutingCount());
|
||||
|
||||
Assert.False(waitJobsFinish.Wait(100));
|
||||
Assert.False(waitJobsFinish.Wait(1));
|
||||
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
|
||||
|
||||
Assert.True(waitJobsFinish.Wait(100));
|
||||
await waitJobsFinish;
|
||||
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
|
||||
Assert.True(!waitJobsFinish.IsFaulted);
|
||||
Assert.Equal(0, client.GetExecutingCount());
|
||||
|
||||
bool jobExecuted = false;
|
||||
Logs.Tester.LogInformation("This job will be cancelled");
|
||||
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
|
||||
client.Schedule((_) => { jobExecuted = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(1.0));
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(0.5));
|
||||
Assert.False(jobExecuted);
|
||||
Thread.Sleep(100);
|
||||
Assert.Equal(1, client.GetExecutingCount());
|
||||
TestUtils.Eventually(() => Assert.Equal(1, client.GetExecutingCount()));
|
||||
|
||||
|
||||
waitJobsFinish = client.WaitAllRunning(default);
|
||||
Assert.False(waitJobsFinish.Wait(100));
|
||||
cts.Cancel();
|
||||
Assert.True(waitJobsFinish.Wait(1000));
|
||||
await waitJobsFinish;
|
||||
Assert.True(waitJobsFinish.Wait(1));
|
||||
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
|
||||
Assert.True(!waitJobsFinish.IsFaulted);
|
||||
Assert.False(waitJobsFinish.IsFaulted);
|
||||
Assert.False(jobExecuted);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Thread.Sleep(100); // Make sure it get cancelled
|
||||
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
|
||||
Assert.False(jobExecuted);
|
||||
Assert.Equal(0, client.GetExecutingCount());
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await processing);
|
||||
Assert.True(processing.IsCanceled);
|
||||
Assert.True(client.WaitAllRunning(default).Wait(100));
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -2251,15 +2346,22 @@ donation:
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
if (result.ExpectedName == "bitbank")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
|
||||
}
|
||||
else
|
||||
{
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
|
||||
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
|
||||
);
|
||||
}
|
||||
}
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
@ -2352,7 +2454,7 @@ donation:
|
||||
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
|
||||
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
|
||||
|
||||
|
||||
// Error if directory not exists
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
|
||||
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.15
|
||||
image: nicolasdorier/nbxplorer:2.0.0.27
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -8,10 +8,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitTokenEntity
|
||||
{
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
@ -39,7 +35,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = Label,
|
||||
Facade = Facade,
|
||||
StoreId = StoreId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
|
@ -11,11 +11,6 @@ namespace BTCPayServer.Authentication
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -90,7 +90,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = data.Label,
|
||||
Facade = data.Facade,
|
||||
Value = data.Id,
|
||||
SIN = data.SIN,
|
||||
PairingTime = data.PairingTime,
|
||||
@ -129,7 +128,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
@ -178,7 +176,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
@ -213,7 +210,6 @@ namespace BTCPayServer.Authentication
|
||||
return null;
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
Id = data.Id,
|
||||
Label = data.Label,
|
||||
Expiration = data.Expiration,
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.82</Version>
|
||||
<Version>1.0.3.94</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -30,25 +30,27 @@
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.11" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.16" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.86" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.98" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.5" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.6" />
|
||||
<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" />
|
||||
@ -67,7 +69,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -48,11 +48,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -149,6 +145,16 @@ namespace BTCPayServer.Configuration
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||
if(!string.IsNullOrEmpty(socksEndpointString))
|
||||
{
|
||||
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -273,5 +279,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TorrcFile { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
|
@ -40,6 +40,8 @@ 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("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -42,15 +42,12 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required");
|
||||
if (string.IsNullOrEmpty(request.Facade))
|
||||
throw new BitpayHttpException(400, "'facade' property is required");
|
||||
|
||||
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
|
||||
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
|
||||
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = pairingCode,
|
||||
Facade = request.Facade,
|
||||
Label = request.Label
|
||||
});
|
||||
|
||||
@ -86,7 +83,7 @@ namespace BTCPayServer.Controllers
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
Facade = pairingEntity.Facade,
|
||||
Facade = "merchant",
|
||||
Token = pairingEntity.TokenValue,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
|
@ -136,6 +136,7 @@ namespace BTCPayServer.Controllers
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
|
@ -77,6 +77,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string NotificationEmail { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -101,7 +104,10 @@ namespace BTCPayServer.Controllers
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
@ -174,7 +180,11 @@ namespace BTCPayServer.Controllers
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
|
@ -7,8 +7,10 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@ -31,16 +33,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppService AppService,
|
||||
public AppsPublicController(AppService AppService,
|
||||
BTCPayServerOptions btcPayServerOptions,
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppService = AppService;
|
||||
_BtcPayServerOptions = btcPayServerOptions;
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppService _AppService;
|
||||
private readonly BTCPayServerOptions _BtcPayServerOptions;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
@ -85,34 +90,34 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if(!isAdmin)
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
|
||||
appInfo.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (settings.Enabled) return View(appInfo);
|
||||
if(!isAdmin)
|
||||
if (settings.Enabled)
|
||||
return View(appInfo);
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(appInfo);
|
||||
@ -134,7 +139,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled && !isAdmin) {
|
||||
if (!settings.Enabled && !isAdmin)
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
@ -176,22 +182,26 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) }, cancellationToken: cancellationToken);
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ??
|
||||
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> {AppService.GetAppInternalTag(appId)},
|
||||
cancellationToken: cancellationToken);
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||
new {invoiceId = invoice.Data.Id});
|
||||
new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -202,7 +212,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -211,7 +221,7 @@ namespace BTCPayServer.Controllers
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
@ -262,15 +272,21 @@ namespace BTCPayServer.Controllers
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
ExtendedNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken);
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
@ -8,19 +8,39 @@ using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly CssThemeManager _cachedServerSettings;
|
||||
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
public HomeController(IHttpClientFactory httpClientFactory)
|
||||
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_cachedServerSettings = cachedServerSettings;
|
||||
}
|
||||
public IActionResult Index()
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
|
||||
{
|
||||
var serviceProvider = HttpContext.RequestServices;
|
||||
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
|
||||
controller.Url = Url;
|
||||
controller.ControllerContext = ControllerContext;
|
||||
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
|
||||
if (res != null)
|
||||
{
|
||||
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
|
||||
return res; // return
|
||||
}
|
||||
}
|
||||
|
||||
return View("Home");
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
@ -188,7 +189,7 @@ namespace BTCPayServer.Controllers
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
@ -213,6 +214,23 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoice-noscript")]
|
||||
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
@ -284,6 +302,7 @@ namespace BTCPayServer.Controllers
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
@ -296,6 +315,7 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
@ -315,6 +335,7 @@ namespace BTCPayServer.Controllers
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
@ -437,7 +458,7 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
return Ok("{}");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -446,6 +467,10 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
if (searchTerm == null)
|
||||
{
|
||||
searchTerm = HttpContext.Session.GetString("InvoicesSearchTerm");
|
||||
}
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
SearchTerm = searchTerm,
|
||||
@ -607,6 +632,14 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
if (invoices.SearchTerm == null)
|
||||
{
|
||||
HttpContext.Session.Remove("InvoicesSearchTerm");
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Session.SetString("InvoicesSearchTerm", invoices.SearchTerm);
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
|
@ -84,6 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
|
||||
if (invoice.NotificationURL != null &&
|
||||
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
|
||||
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
|
||||
@ -105,7 +106,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
@ -125,6 +126,9 @@ namespace BTCPayServer.Controllers
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
@ -138,7 +142,7 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
}
|
||||
|
||||
@ -200,8 +204,22 @@ namespace BTCPayServer.Controllers
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
}
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await fetchingAll;
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
|
||||
}
|
||||
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
|
||||
});
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
@ -230,6 +248,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
@ -242,8 +261,13 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
|
||||
using (logs.Measure($"{logPrefix} Payment method details creation"))
|
||||
{
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
}
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
@ -271,7 +295,7 @@ namespace BTCPayServer.Controllers
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
logs.Write($"{logPrefix} {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
|
||||
}
|
||||
await _userManager.SetUserNameAsync(user, model.Username);
|
||||
}
|
||||
|
||||
var phoneNumber = user.PhoneNumber;
|
||||
|
@ -182,7 +182,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
|
||||
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -229,12 +229,12 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
|
@ -91,7 +91,6 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
@ -140,15 +139,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
|
||||
|
||||
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -39,16 +42,20 @@ namespace BTCPayServer.Controllers
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
private readonly TorServices _torServices;
|
||||
BTCPayServerOptions _Options;
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
TorServices torServices,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_Options = options;
|
||||
_UserManager = userManager;
|
||||
@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -246,6 +255,13 @@ namespace BTCPayServer.Controllers
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else if (command == "clean")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-clean.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The old docker images will be cleaned soon...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
@ -431,15 +447,46 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
|
||||
// load display app dropdown
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userId = _UserManager.GetUserId(base.User);
|
||||
var selectList = ctx.Users.Where(user => user.Id == userId)
|
||||
.SelectMany(s => s.UserStores)
|
||||
.Select(s => s.StoreData)
|
||||
.SelectMany(s => s.Apps)
|
||||
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
|
||||
selectList.Insert(0, new SelectListItem("(None)", null));
|
||||
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
|
||||
}
|
||||
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
|
||||
if (app != null)
|
||||
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
|
||||
else
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not preserved on client side, but clearing it just in case
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
@ -463,6 +510,25 @@ namespace BTCPayServer.Controllers
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
foreach (var torService in _torServices.Services)
|
||||
{
|
||||
if (torService.VirtualPort == 80)
|
||||
{
|
||||
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"http://{torService.OnionHost}"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
|
||||
});
|
||||
}
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
@ -651,7 +717,7 @@ namespace BTCPayServer.Controllers
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
|
||||
var server = IsLocalNetwork(settings.Server) ? this.Request.Host.Host: settings.Server;
|
||||
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
@ -661,13 +727,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static bool IsLocalNetwork(string server)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.Mode = existing.Mode;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
MerchantId = vm.MerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
Mode = vm.Mode
|
||||
Mode = vm.Mode,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
|
@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -189,24 +189,39 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
public IActionResult Rates(string storeId)
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = storeId;
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, CancellationToken cancellationToken = default)
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[] currencyPairs = null;
|
||||
try
|
||||
{
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
@ -333,13 +348,15 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
SetCryptoCurrencies(vm, StoreData);
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
return View(vm);
|
||||
}
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
@ -396,13 +413,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
@ -490,7 +509,7 @@ namespace BTCPayServer.Controllers
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
@ -594,7 +613,6 @@ namespace BTCPayServer.Controllers
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Facade = t.Facade,
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
@ -679,7 +697,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
{
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
|
||||
};
|
||||
@ -691,7 +708,6 @@ namespace BTCPayServer.Controllers
|
||||
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = tokenRequest.PairingCode,
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
});
|
||||
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
|
||||
@ -731,7 +747,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
@ -783,7 +798,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Facade = pairing.Facade,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
@ -874,7 +888,11 @@ namespace BTCPayServer.Controllers
|
||||
ButtonSize = 2,
|
||||
UrlRoot = appUrl,
|
||||
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
|
||||
StoreId = store.Id
|
||||
StoreId = store.Id,
|
||||
ButtonType = 0,
|
||||
Min = 1,
|
||||
Max = 20,
|
||||
Step = 1
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
@ -12,11 +12,6 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Unused")]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
|
@ -307,6 +307,25 @@ namespace BTCPayServer.Data
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
CurrencyPair[] _DefaultCurrencyPairs;
|
||||
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
|
||||
public CurrencyPair[] DefaultCurrencyPairs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
|
||||
}
|
||||
set
|
||||
{
|
||||
_DefaultCurrencyPairs = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDefaultCurrencyPairString()
|
||||
{
|
||||
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
|
||||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
@ -330,10 +349,11 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
@ -429,6 +449,7 @@ namespace BTCPayServer.Data
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public EmailSettings EmailSettings { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
|
@ -33,6 +33,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -165,6 +166,31 @@ namespace BTCPayServer
|
||||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
||||
}
|
||||
|
||||
public static bool IsLocalNetwork(string server)
|
||||
{
|
||||
if (server == null)
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
|
||||
}
|
||||
if(IPAddress.TryParse(server, out var ip))
|
||||
{
|
||||
return ip.IsLocal();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsOnion(this HttpRequest request)
|
||||
{
|
||||
if (request?.Host.Host == null)
|
||||
return false;
|
||||
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -303,30 +329,6 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -34,6 +35,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Stop == null)
|
||||
return;
|
||||
_Stop.Cancel();
|
||||
try
|
||||
{
|
||||
@ -43,7 +46,14 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
||||
}
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
try
|
||||
{
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +61,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
class BackgroundJob
|
||||
{
|
||||
public Func<Task> Action;
|
||||
public Func<CancellationToken, Task> Action;
|
||||
public TimeSpan Delay;
|
||||
public IDelay DelayImplementation;
|
||||
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
{
|
||||
this.Action = action;
|
||||
this.Delay = delay;
|
||||
@ -64,7 +74,7 @@ namespace BTCPayServer.HostedServices
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
await DelayImplementation.Wait(Delay, cancellationToken);
|
||||
await Action();
|
||||
await Action(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +89,9 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
|
||||
HashSet<Task> _Processing = new HashSet<Task>();
|
||||
public void Schedule(Func<Task> action, TimeSpan delay)
|
||||
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
|
||||
{
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
|
||||
}
|
||||
|
||||
public async Task WaitAllRunning(CancellationToken cancellationToken)
|
||||
@ -89,6 +99,8 @@ namespace BTCPayServer.HostedServices
|
||||
Task[] processing = null;
|
||||
lock (_Processing)
|
||||
{
|
||||
if (_Processing.Count == 0)
|
||||
return;
|
||||
processing = _Processing.ToArray();
|
||||
}
|
||||
|
||||
@ -96,9 +108,8 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll(processing).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,8 +124,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
_Processing.Add(processing);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
processing.ContinueWith(t =>
|
||||
_ = processing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
@ -125,7 +135,6 @@ namespace BTCPayServer.HostedServices
|
||||
_Processing.Remove(processing);
|
||||
}
|
||||
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -44,13 +45,20 @@ namespace BTCPayServer.HostedServices
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
|
||||
public AppType? RootAppType { get; set; }
|
||||
public string RootAppId { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
DiscourageSearchEngines = data.DiscourageSearchEngines;
|
||||
|
||||
RootAppType = data.RootAppType;
|
||||
RootAppId = data.RootAppId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceNotificationManager : IHostedService
|
||||
{
|
||||
public static HttpClient _Client = new HttpClient();
|
||||
HttpClient _Client;
|
||||
|
||||
public class ScheduledJob
|
||||
{
|
||||
@ -46,6 +46,7 @@ namespace BTCPayServer.HostedServices
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
@ -53,6 +54,7 @@ namespace BTCPayServer.HostedServices
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
EmailSenderFactory emailSenderFactory)
|
||||
{
|
||||
_Client = httpClientFactory.CreateClient();
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
@ -135,23 +137,29 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceStr, cancellation), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
bool reschedule = false;
|
||||
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
|
||||
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
|
||||
reschedule = !response.IsSuccessStatusCode;
|
||||
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// When the JobClient will be persistent, this will reschedule the job for after reboot
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
aggregatorEvent.Error = "Timeout";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
@ -172,14 +180,13 @@ namespace BTCPayServer.HostedServices
|
||||
aggregatorEvent.Error = $"Unexpected error: {message}";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
finally { cts?.Dispose(); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if (job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +210,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
@ -224,7 +231,14 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
var response = await Enqueue(notification.Data.Id, async () =>
|
||||
{
|
||||
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(1.0));
|
||||
return await _Client.SendAsync(request, cts.Token);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,6 @@ namespace BTCPayServer.HostedServices
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
@ -99,8 +98,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,19 +184,6 @@ namespace BTCPayServer.HostedServices
|
||||
return result;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
@ -231,25 +217,24 @@ namespace BTCPayServer.HostedServices
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
Task _Loop;
|
||||
Task _WaitingInvoices;
|
||||
CancellationTokenSource _Cts;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
_WaitingInvoices = WaitPendingInvoices();
|
||||
_ = WaitPendingInvoices();
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(b =>
|
||||
{
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
_ = Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
@ -264,79 +249,76 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
_WaitingInvoices = null;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
try
|
||||
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (loopCount < maxLoop)
|
||||
{
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
cancellation.ThrowIfCancellationRequested();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
||||
if (updateContext.Events.Count == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
_ = Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => Watch(invoiceId), TaskScheduler.Default);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return;
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
return Task.WhenAll(waitingPendingInvoices, _Loop);
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -68,7 +69,7 @@ namespace BTCPayServer.HostedServices
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
|
||||
{
|
||||
exchanges.Add(item);
|
||||
}
|
||||
|
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class TorServicesHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
private readonly TorServices _torServices;
|
||||
|
||||
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
|
||||
{
|
||||
_options = options;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
|
||||
if (string.IsNullOrEmpty(_options.TorrcFile))
|
||||
return Array.Empty<Task>();
|
||||
return new Task[] { CreateLoopTask(RefreshTorServices) };
|
||||
}
|
||||
|
||||
async Task RefreshTorServices()
|
||||
{
|
||||
await _torServices.Refresh();
|
||||
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,6 @@ using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,6 +47,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Npgsql;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -62,6 +62,9 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
services.TryAddSingleton<LightningClientFactoryService>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<InvoiceRepository>(o =>
|
||||
@ -188,6 +191,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
@ -215,7 +219,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
|
||||
services.AddBundles();
|
||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
|
@ -88,6 +88,9 @@ namespace BTCPayServer.Hosting
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
// In case of anyone can create invoice, the storeId can be set explicitely
|
||||
bitpayAuth |= httpContext.Request.Query.ContainsKey("storeid");
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
var method = httpContext.Request.Method;
|
||||
@ -95,7 +98,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
if (
|
||||
(isCors || bitpayAuth) &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(isCors || (method == "POST" && isJson)))
|
||||
return true;
|
||||
|
||||
|
50
BTCPayServer/Hosting/ResourceBundleProvider.cs
Normal file
50
BTCPayServer/Hosting/ResourceBundleProvider.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class ResourceBundleProvider : IBundleProvider
|
||||
{
|
||||
BundleProvider _InnerProvider;
|
||||
Lazy<Dictionary<string, Bundle>> _BundlesByName;
|
||||
public ResourceBundleProvider(IHostingEnvironment hosting, BundleOptions options)
|
||||
{
|
||||
if (options.UseBundles)
|
||||
{
|
||||
_BundlesByName = new Lazy<Dictionary<string, Bundle>>(() =>
|
||||
{
|
||||
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"))
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
var content = reader.ReadToEnd();
|
||||
return JArray.Parse(content).OfType<JObject>()
|
||||
.Select(jobj => new Bundle()
|
||||
{
|
||||
Name = jobj.Property("name")?.Value.Value<string>() ?? jobj.Property("outputFileName").Value.Value<string>(),
|
||||
OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName").Value.Value<string>())
|
||||
}).ToDictionary(o => o.Name, o => o);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_InnerProvider = new BundleProvider();
|
||||
}
|
||||
}
|
||||
public Bundle GetBundle(string name)
|
||||
{
|
||||
if (_InnerProvider != null)
|
||||
return _InnerProvider.GetBundle(name);
|
||||
_BundlesByName.Value.TryGetValue(name, out var bundle);
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
@ -65,6 +64,7 @@ namespace BTCPayServer.Hosting
|
||||
.AddDefaultTokenProviders();
|
||||
services.AddSignalR();
|
||||
services.AddBTCPayServer();
|
||||
services.AddSession();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
@ -79,7 +79,7 @@ namespace BTCPayServer.Hosting
|
||||
// StyleSrc = "'self' 'unsafe-inline'",
|
||||
// ScriptSrc = "'self' 'unsafe-inline'"
|
||||
//});
|
||||
});
|
||||
}).AddControllersAsServices();
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
@ -170,6 +170,7 @@ namespace BTCPayServer.Hosting
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
app.UseSession();
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
AppHub.Register(route);
|
||||
|
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class CurrencyPairJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -33,5 +33,35 @@ namespace BTCPayServer.Logging
|
||||
return _InvoiceLogs.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal IDisposable Measure(string logs)
|
||||
{
|
||||
return new Mesuring(this, logs);
|
||||
}
|
||||
|
||||
class Mesuring : IDisposable
|
||||
{
|
||||
private readonly InvoiceLogs _logs;
|
||||
private readonly string _msg;
|
||||
private readonly DateTimeOffset _Before;
|
||||
public Mesuring(InvoiceLogs logs, string msg)
|
||||
{
|
||||
_logs = logs;
|
||||
_msg = msg;
|
||||
_Before = DateTimeOffset.UtcNow;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
var timespan = DateTimeOffset.UtcNow - _Before;
|
||||
if (timespan.TotalSeconds >= 1.0)
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
@ -16,8 +17,13 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
[Display(Name = "Featured Image")]
|
||||
public string MainImageUrl { get; set; }
|
||||
|
||||
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice Email Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
|
||||
|
@ -1,4 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
@ -10,7 +13,6 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Required]
|
||||
[MaxLength(5)]
|
||||
public string Currency { get; set; }
|
||||
[MaxLength(5000)]
|
||||
public string Template { get; set; }
|
||||
|
||||
[Display(Name = "Enable shopping cart")]
|
||||
@ -25,6 +27,13 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string Example2 { get; internal set; }
|
||||
public string ExampleCallback { get; internal set; }
|
||||
public string InvoiceUrl { get; internal set; }
|
||||
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice Email Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
@ -47,5 +56,28 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public string RedirectAutomatically { get; set; } = string.Empty;
|
||||
|
||||
public SelectList RedirectAutomaticallySelectList =>
|
||||
new SelectList(new List< SelectListItem>()
|
||||
{
|
||||
new SelectListItem()
|
||||
{
|
||||
Text = "Yes",
|
||||
Value = "true"
|
||||
},
|
||||
new SelectListItem()
|
||||
{
|
||||
Text = "No",
|
||||
Value = "false"
|
||||
},
|
||||
new SelectListItem()
|
||||
{
|
||||
Text = "Use Store Settings",
|
||||
Value = ""
|
||||
}
|
||||
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string ResetEvery { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
|
@ -79,5 +79,8 @@ namespace BTCPayServer.Models
|
||||
public string Guid { get; set; }
|
||||
[JsonProperty(PropertyName = "token", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Token { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "redirectAutomatically", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
JObject item = new JObject();
|
||||
jarray.Add(item);
|
||||
JProperty jProp = new JProperty(token.Facade);
|
||||
JProperty jProp = new JProperty("merchant");
|
||||
item.Add(jProp);
|
||||
jProp.Value = token.Value;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public bool IsModal { get; set; }
|
||||
public bool IsLightning { get; set; }
|
||||
@ -65,5 +66,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CoinSwitchMode { get; set; }
|
||||
public string CoinSwitchMerchantId { get; set; }
|
||||
public string RootPath { get; set; }
|
||||
public decimal CoinSwitchAmountMarkupPercentage { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
@ -17,5 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
|
||||
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
|
||||
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
|
||||
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
|
||||
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
|
||||
}
|
||||
}
|
||||
|
@ -21,23 +21,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList Languages { get; set; }
|
||||
|
||||
[Display(Name = "Default the default payment method on checkout")]
|
||||
[Display(Name = "Default payment method on checkout")]
|
||||
public string DefaultPaymentMethod { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Requires a refund email")]
|
||||
public bool RequiresRefundEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||
[Uri]
|
||||
@ -49,9 +36,26 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
[Display(Name = "Requires a refund email")]
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Display lightning payment amounts in Satoshis")]
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
|
||||
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
|
||||
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
|
||||
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
|
@ -27,10 +27,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get; set;
|
||||
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -18,6 +15,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string CheckoutDesc { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public int ButtonSize { get; set; }
|
||||
public int ButtonType { get; set; }
|
||||
|
||||
// Slider properties (ButtonType = 2)
|
||||
public decimal Min { get; set; }
|
||||
public decimal Max { get; set; }
|
||||
public decimal Step { get; set; }
|
||||
|
||||
[Url]
|
||||
public string ServerIpn { get; set; }
|
||||
[Url]
|
||||
|
@ -21,14 +21,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
RateSource = chosen.Url;
|
||||
}
|
||||
|
||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||
@ -42,6 +44,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string Script { get; set; }
|
||||
public string DefaultScript { get; set; }
|
||||
public string ScriptTest { get; set; }
|
||||
public string DefaultCurrencyPairs { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Add a spread on exchange rate of ... %")]
|
||||
@ -57,10 +61,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string StoreId
|
||||
{
|
||||
@ -52,10 +46,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class TokensViewModel
|
||||
{
|
||||
|
@ -12,6 +12,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
[Display(Name = "Integration Mode")]
|
||||
public string Mode { get; set; } = "inline";
|
||||
|
||||
[Required]
|
||||
[Range(0, 100)]
|
||||
[Display(Name =
|
||||
"Percentage to multiply amount requested at Coinswitch to avoid underpaid situations due to Coinswitch not guaranteeing rates. ")]
|
||||
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
|
||||
|
||||
public List<SelectListItem> Modes { get; } = new List<SelectListItem>
|
||||
{
|
||||
|
@ -20,11 +20,6 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "facade")]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "count")]
|
||||
public int Count
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ namespace BTCPayServer.Payments.CoinSwitch
|
||||
public string MerchantId { get; set; }
|
||||
public string Mode { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public decimal AmountMarkupPercentage { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
|
@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -17,18 +19,25 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
private readonly SocketFactory _socketFactory;
|
||||
|
||||
public LightningLikePaymentHandler(
|
||||
NBXplorerDashboard dashboard)
|
||||
NBXplorerDashboard dashboard,
|
||||
LightningClientFactoryService lightningClientFactory,
|
||||
SocketFactory socketFactory)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_socketFactory = socketFactory;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = GetNodeInfo(supportedPaymentMethod, network);
|
||||
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
@ -63,14 +72,14 @@ namespace BTCPayServer.Payments.Lightning
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
||||
LightningNodeInformation info = null;
|
||||
try
|
||||
{
|
||||
@ -84,8 +93,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if (info.NodeInfo == null)
|
||||
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
|
||||
if (nodeInfo == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
@ -96,7 +105,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
|
||||
return info.NodeInfo;
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,22 +113,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
try
|
||||
{
|
||||
IPAddress address = null;
|
||||
try
|
||||
{
|
||||
address = IPAddress.Parse(nodeInfo.Host);
|
||||
}
|
||||
catch
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
|
||||
}
|
||||
if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
||||
|
||||
if (address == null)
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation))
|
||||
{
|
||||
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -10,117 +10,150 @@ using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Lightning;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningListener : IHostedService
|
||||
{
|
||||
class ListenedInvoice
|
||||
{
|
||||
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
|
||||
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public string Uri { get; internal set; }
|
||||
public BTCPayNetwork Network { get; internal set; }
|
||||
public string InvoiceId { get; internal set; }
|
||||
}
|
||||
|
||||
EventAggregator _Aggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly LightningClientFactoryService lightningClientFactory;
|
||||
Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>();
|
||||
Task _CheckingInvoice;
|
||||
Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>();
|
||||
|
||||
public LightningListener(EventAggregator aggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
IMemoryCache memoryCache,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
LightningClientFactoryService lightningClientFactory,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_Aggregator = aggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_memoryCache = memoryCache;
|
||||
_NetworkProvider = networkProvider;
|
||||
this.lightningClientFactory = lightningClientFactory;
|
||||
}
|
||||
|
||||
async Task CheckingInvoice(CancellationToken cancellation)
|
||||
{
|
||||
while(await _CheckInvoices.Reader.WaitToReadAsync(cancellation) &&
|
||||
_CheckInvoices.Reader.TryRead(out var invoiceId))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var listenedInvoice in (await GetListenedInvoices(invoiceId)).Where(i => !i.IsExpired()))
|
||||
{
|
||||
var instanceListenerKey = (listenedInvoice.Network.CryptoCode, listenedInvoice.SupportedPaymentMethod.GetLightningUrl().ToString());
|
||||
if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) ||
|
||||
!instanceListener.IsListening)
|
||||
{
|
||||
instanceListener = instanceListener ?? new LightningInstanceListener(_InvoiceRepository, _Aggregator, listenedInvoice.SupportedPaymentMethod, lightningClientFactory, listenedInvoice.Network);
|
||||
var status = await instanceListener.PollPayment(listenedInvoice, cancellation);
|
||||
if (status is null ||
|
||||
status is LightningInvoiceStatus.Paid ||
|
||||
status is LightningInvoiceStatus.Expired)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
instanceListener.AddListenedInvoice(listenedInvoice);
|
||||
instanceListener.EnsureListening(cancellation);
|
||||
_InstanceListeners.TryAdd(instanceListenerKey, instanceListener);
|
||||
}
|
||||
else
|
||||
{
|
||||
instanceListener.AddListenedInvoice(listenedInvoice);
|
||||
}
|
||||
}
|
||||
foreach (var kv in _InstanceListeners)
|
||||
{
|
||||
kv.Value.RemoveExpiredInvoices();
|
||||
}
|
||||
foreach (var k in _InstanceListeners
|
||||
.Where(kv => !kv.Value.IsListening)
|
||||
.Select(kv => kv.Key).ToArray())
|
||||
{
|
||||
_InstanceListeners.Remove(k);
|
||||
}
|
||||
}
|
||||
catch when (!_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
private Task<List<ListenedInvoice>> GetListenedInvoices(string invoiceId)
|
||||
{
|
||||
return _memoryCache.GetOrCreateAsync(invoiceId, async (cacheEntry) =>
|
||||
{
|
||||
var listenedInvoices = new List<ListenedInvoice>();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
{
|
||||
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
|
||||
if (lightningMethod == null)
|
||||
continue;
|
||||
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
|
||||
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
|
||||
if (lightningSupportedMethod == null)
|
||||
continue;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
|
||||
listenedInvoices.Add(new ListenedInvoice()
|
||||
{
|
||||
Expiration = invoice.ExpirationTime,
|
||||
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||
PaymentMethodDetails = lightningMethod,
|
||||
SupportedPaymentMethod = lightningSupportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
Network = network,
|
||||
InvoiceId = invoice.Id
|
||||
});
|
||||
}
|
||||
var expiredIn = DateTimeOffset.UtcNow - invoice.ExpirationTime;
|
||||
cacheEntry.AbsoluteExpiration = DateTimeOffset.UtcNow + (expiredIn >= TimeSpan.FromMinutes(5.0) ? expiredIn : TimeSpan.FromMinutes(5.0));
|
||||
return listenedInvoices;
|
||||
});
|
||||
}
|
||||
|
||||
ConcurrentDictionary<string, LightningInstanceListener> _ListeningInstances = new ConcurrentDictionary<string, LightningInstanceListener>();
|
||||
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(inv =>
|
||||
{
|
||||
if (inv.Name == InvoiceEvent.Created)
|
||||
{
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
_CheckInvoices.Writer.TryWrite(inv.Invoice.Id);
|
||||
}
|
||||
}));
|
||||
|
||||
_CheckingInvoice = CheckingInvoice(_Cts.Token);
|
||||
_ListenPoller = new Timer(async s =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(async invoiceId => await EnsureListening(invoiceId, true))
|
||||
.ToArray());
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex.InnerException ?? ex.InnerExceptions.FirstOrDefault(), $"Lightning: Uncaught error");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Lightning: Uncaught error");
|
||||
var invoiceIds = await _InvoiceRepository.GetPendingInvoices();
|
||||
foreach (var invoiceId in invoiceIds)
|
||||
_CheckInvoices.Writer.TryWrite(invoiceId);
|
||||
}
|
||||
catch { } // Never throw an unhandled exception on async void
|
||||
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task EnsureListening(string invoiceId, bool poll)
|
||||
{
|
||||
if (Listening(invoiceId))
|
||||
return;
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
{
|
||||
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
|
||||
if (lightningMethod == null)
|
||||
continue;
|
||||
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
|
||||
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
|
||||
if (lightningSupportedMethod == null)
|
||||
continue;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
|
||||
var listenedInvoice = new ListenedInvoice()
|
||||
{
|
||||
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||
PaymentMethodDetails = lightningMethod,
|
||||
SupportedPaymentMethod = lightningSupportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
Network = network,
|
||||
InvoiceId = invoice.Id
|
||||
};
|
||||
|
||||
if (poll)
|
||||
{
|
||||
var charge = lightningSupportedMethod.CreateClient(network);
|
||||
LightningInvoice chargeInvoice = null;
|
||||
try
|
||||
{
|
||||
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||
}
|
||||
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 == LightningInvoiceStatus.Paid)
|
||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||
if (chargeInvoice.Status == LightningInvoiceStatus.Paid || chargeInvoice.Status == LightningInvoiceStatus.Expired)
|
||||
continue;
|
||||
}
|
||||
|
||||
StartListening(listenedInvoice);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
@ -139,159 +172,194 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
ILightningInvoiceListener session = null;
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
var lightningClient = supportedPaymentMethod.CreateClient(network);
|
||||
session = await lightningClient.Listen(_Cts.Token);
|
||||
while (true)
|
||||
{
|
||||
var notification = await session.WaitInvoice(_Cts.Token);
|
||||
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
|
||||
if (listenedInvoice == null)
|
||||
continue;
|
||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
{
|
||||
if (notification.Status == LightningInvoiceStatus.Paid &&
|
||||
notification.PaidAt.HasValue && notification.Amount != null)
|
||||
{
|
||||
await AddPayment(network, notification, listenedInvoice);
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
if (notification.Status == LightningInvoiceStatus.Expired)
|
||||
{
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
DoneListening(supportedPaymentMethod.GetLightningUrl());
|
||||
}
|
||||
finally
|
||||
{
|
||||
session?.Dispose();
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
|
||||
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.BOLT11,
|
||||
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
|
||||
Amount = notification.Amount
|
||||
}, network, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
|
||||
if (invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> _ListeningLightning = new List<Task>();
|
||||
MultiValueDictionary<string, ListenedInvoice> _ListenedInvoiceByLightningUrl = new MultiValueDictionary<string, ListenedInvoice>();
|
||||
Dictionary<string, ListenedInvoice> _ListenedInvoiceByChargeInvoiceId = new Dictionary<string, ListenedInvoice>();
|
||||
|
||||
HashSet<string> _InvoiceIds = new HashSet<string>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening an invoice
|
||||
/// </summary>
|
||||
/// <param name="listenedInvoice">The invoice to stop listening</param>
|
||||
/// <returns>true if still need to listen the lightning instance</returns>
|
||||
bool DoneListening(ListenedInvoice listenedInvoice)
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_ListenedInvoiceByLightningUrl.Remove(listenedInvoice.Uri, listenedInvoice);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening all invoices on this server
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
private void DoneListening(LightningConnectionString connectionString)
|
||||
{
|
||||
var uri = connectionString.BaseUri;
|
||||
lock (_ListenedInvoiceByChargeInvoiceId)
|
||||
{
|
||||
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
|
||||
}
|
||||
}
|
||||
|
||||
bool Listening(string invoiceId)
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
return _InvoiceIds.Contains(invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
private ListenedInvoice GetListenedInvoice(string chargeInvoiceId)
|
||||
{
|
||||
ListenedInvoice listenedInvoice = null;
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.TryGetValue(chargeInvoiceId, out listenedInvoice);
|
||||
}
|
||||
return listenedInvoice;
|
||||
}
|
||||
|
||||
bool StartListening(ListenedInvoice listenedInvoice)
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
if (_InvoiceIds.Contains(listenedInvoice.InvoiceId))
|
||||
return false;
|
||||
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
|
||||
{
|
||||
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
||||
_ListeningLightning.Add(listen);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
||||
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
||||
_InvoiceIds.Add(listenedInvoice.InvoiceId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
Task[] listening = null;
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
try
|
||||
{
|
||||
listening = _ListeningLightning.ToArray();
|
||||
await _CheckingInvoice;
|
||||
}
|
||||
await Task.WhenAll(listening);
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_ListeningInstances.Select(c => c.Value.Listening).ToArray());
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
Logs.PayServer.LogInformation("Lightning listened stopped");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class LightningInstanceListener
|
||||
{
|
||||
private LightningSupportedPaymentMethod supportedPaymentMethod;
|
||||
private readonly InvoiceRepository invoiceRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly BTCPayNetwork network;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
|
||||
public LightningInstanceListener(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
LightningSupportedPaymentMethod supportedPaymentMethod,
|
||||
LightningClientFactoryService lightningClientFactory,
|
||||
BTCPayNetwork network)
|
||||
{
|
||||
this.supportedPaymentMethod = supportedPaymentMethod;
|
||||
this.invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
this.network = network;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
}
|
||||
internal bool AddListenedInvoice(ListenedInvoice invoice)
|
||||
{
|
||||
return _ListenedInvoices.TryAdd(invoice.PaymentMethodDetails.InvoiceId, invoice);
|
||||
}
|
||||
|
||||
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation)
|
||||
{
|
||||
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
||||
LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
if (lightningInvoice?.Status is LightningInvoiceStatus.Paid &&
|
||||
await AddPayment(lightningInvoice, listenedInvoice.InvoiceId))
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}");
|
||||
}
|
||||
return lightningInvoice?.Status;
|
||||
}
|
||||
|
||||
public bool IsListening => Listening?.Status is TaskStatus.Running || Listening?.Status is TaskStatus.WaitingForActivation;
|
||||
public Task Listening { get; set; }
|
||||
public void EnsureListening(CancellationToken cancellation)
|
||||
{
|
||||
if (!IsListening)
|
||||
{
|
||||
StopListeningCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
Listening = Listen(StopListeningCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
public CancellationTokenSource StopListeningCancellationTokenSource;
|
||||
async Task Listen(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
try
|
||||
{
|
||||
var lightningClient = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
||||
using (var session = await lightningClient.Listen(cancellation))
|
||||
{
|
||||
// Just in case the payment arrived after our last poll but before we listened.
|
||||
await PollAllListenedInvoices(cancellation);
|
||||
if (_ErrorAlreadyLogged)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Could reconnect successfully to {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
_ErrorAlreadyLogged = false;
|
||||
while (!_ListenedInvoices.IsEmpty)
|
||||
{
|
||||
var notification = await session.WaitInvoice(cancellation);
|
||||
if (!_ListenedInvoices.TryGetValue(notification.Id, out var listenedInvoice))
|
||||
continue;
|
||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
{
|
||||
if (notification.Status == LightningInvoiceStatus.Paid &&
|
||||
notification.PaidAt.HasValue && notification.Amount != null)
|
||||
{
|
||||
if (await AddPayment(notification, listenedInvoice.InvoiceId))
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})");
|
||||
}
|
||||
_ListenedInvoices.TryRemove(notification.Id, out var _);
|
||||
}
|
||||
else if (notification.Status == LightningInvoiceStatus.Expired)
|
||||
{
|
||||
_ListenedInvoices.TryRemove(notification.Id, out var _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
|
||||
{
|
||||
_ErrorAlreadyLogged = true;
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
|
||||
if (_ListenedInvoices.IsEmpty)
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): No more invoice to listen on {supportedPaymentMethod.GetLightningUrl().BaseUri}, releasing the connection.");
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastFullPoll { get; set; }
|
||||
|
||||
internal async Task PollAllListenedInvoices(CancellationToken cancellation)
|
||||
{
|
||||
foreach (var invoice in _ListenedInvoices.Values)
|
||||
{
|
||||
var status = await PollPayment(invoice, cancellation);
|
||||
if (status is null ||
|
||||
status is LightningInvoiceStatus.Paid ||
|
||||
status is LightningInvoiceStatus.Expired)
|
||||
_ListenedInvoices.TryRemove(invoice.PaymentMethodDetails.InvoiceId, out var _);
|
||||
}
|
||||
LastFullPoll = DateTimeOffset.UtcNow;
|
||||
if (_ListenedInvoices.IsEmpty)
|
||||
{
|
||||
StopListeningCancellationTokenSource?.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
bool _ErrorAlreadyLogged = false;
|
||||
ConcurrentDictionary<string, ListenedInvoice> _ListenedInvoices = new ConcurrentDictionary<string, ListenedInvoice>();
|
||||
|
||||
public async Task<bool> AddPayment(LightningInvoice notification, string invoiceId)
|
||||
{
|
||||
var payment = await invoiceRepository.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.BOLT11,
|
||||
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
|
||||
Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable
|
||||
}, network, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice != null)
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
}
|
||||
return payment != null;
|
||||
}
|
||||
|
||||
internal void RemoveExpiredInvoices()
|
||||
{
|
||||
foreach (var invoice in _ListenedInvoices)
|
||||
{
|
||||
if (invoice.Value.IsExpired())
|
||||
_ListenedInvoices.TryRemove(invoice.Key, out var _);
|
||||
}
|
||||
if (_ListenedInvoices.IsEmpty)
|
||||
StopListeningCancellationTokenSource?.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
class ListenedInvoice
|
||||
{
|
||||
public bool IsExpired() { return DateTimeOffset.UtcNow > Expiration; }
|
||||
public DateTimeOffset Expiration { get; set; }
|
||||
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
|
||||
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public string Uri { get; internal set; }
|
||||
public BTCPayNetwork Network { get; internal set; }
|
||||
public string InvoiceId { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +60,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +62,34 @@ namespace BTCPayServer.Payments
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (PaymentType == PaymentTypes.BTCLike)
|
||||
return CryptoCode;
|
||||
return CryptoCode + "_" + PaymentType.ToString();
|
||||
return ToString(false);
|
||||
}
|
||||
|
||||
public string ToString(bool pretty)
|
||||
{
|
||||
if (pretty)
|
||||
{
|
||||
return $"{CryptoCode} ({PrettyMethod(PaymentType)})";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PaymentType == PaymentTypes.BTCLike)
|
||||
return CryptoCode;
|
||||
return CryptoCode + "_" + PaymentType.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string PrettyMethod(PaymentTypes paymentType)
|
||||
{
|
||||
switch (paymentType)
|
||||
{
|
||||
case PaymentTypes.BTCLike:
|
||||
return "On-Chain";
|
||||
case PaymentTypes.LightningLike:
|
||||
return "Off-Chain";
|
||||
default:
|
||||
return paymentType.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParse(string str, out PaymentMethodId paymentMethodId)
|
||||
|
@ -57,38 +57,52 @@ namespace BTCPayServer.Security
|
||||
List<Claim> claims = new List<Claim>();
|
||||
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
|
||||
bool anonymous = true;
|
||||
bool? success = null;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
|
||||
storeId = result.StoreId;
|
||||
success = result.SuccessAuth;
|
||||
anonymous = false;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
|
||||
success = storeId != null;
|
||||
anonymous = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues))
|
||||
{
|
||||
storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty;
|
||||
success = true;
|
||||
anonymous = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (success.HasValue)
|
||||
if (success is true)
|
||||
{
|
||||
if (success.Value)
|
||||
if (storeId != null)
|
||||
{
|
||||
if (storeId != null)
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
if (store == null ||
|
||||
(anonymous && !store.GetStoreBlob().AnyoneCanInvoice))
|
||||
{
|
||||
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.Fail("Invalid credentials");
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
else
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
store.AdditionalClaims.AddRange(claims);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
else if (success is false)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
}
|
||||
// else if (success is null)
|
||||
}
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
@ -134,7 +148,7 @@ namespace BTCPayServer.Security
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault();
|
||||
if (bitToken == null)
|
||||
{
|
||||
return (null, false);
|
||||
@ -170,34 +184,6 @@ namespace BTCPayServer.Security
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
}
|
||||
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
|
@ -53,7 +53,11 @@ namespace BTCPayServer.Services.Apps
|
||||
public async Task<object> GetAppInfo(string appId)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.Crowdfund, true);
|
||||
return await GetInfo(app);
|
||||
if (app != null)
|
||||
{
|
||||
return await GetInfo(app);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage = null)
|
||||
{
|
||||
@ -135,6 +139,7 @@ namespace BTCPayServer.Services.Apps
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
PerkCount = perkCount,
|
||||
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
|
||||
|
@ -82,6 +82,8 @@ namespace BTCPayServer.Services.Apps
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
|
||||
};
|
||||
|
||||
public string NotificationEmail { get; set; }
|
||||
}
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
|
@ -14,7 +14,8 @@ namespace BTCPayServer.Services
|
||||
public class BTCPayServerEnvironment
|
||||
{
|
||||
IHttpContextAccessor httpContext;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
|
||||
TorServices torServices;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
|
||||
{
|
||||
this.httpContext = httpContext;
|
||||
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
|
||||
@ -25,6 +26,7 @@ namespace BTCPayServer.Services
|
||||
#endif
|
||||
Environment = env;
|
||||
NetworkType = provider.NetworkType;
|
||||
this.torServices = torServices;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
@ -34,6 +36,8 @@ namespace BTCPayServer.Services
|
||||
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
|
||||
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
|
||||
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
|
||||
public string OnionUrl => this.torServices.Services.Where(s => s.ServiceType == TorServiceType.BTCPayServer)
|
||||
.Select(s => $"http://{s.OnionHost}").FirstOrDefault();
|
||||
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
@ -52,6 +56,18 @@ namespace BTCPayServer.Services
|
||||
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSecure
|
||||
{
|
||||
get
|
||||
{
|
||||
return NetworkType != NetworkType.Mainnet ||
|
||||
httpContext.HttpContext.Request.Scheme == "https" ||
|
||||
httpContext.HttpContext.Request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) ||
|
||||
Extensions.IsLocalNetwork(httpContext.HttpContext.Request.Host.Host);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder txt = new StringBuilder();
|
||||
|
@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public interface IBackgroundJobClient
|
||||
{
|
||||
void Schedule(Func<Task> act, TimeSpan zero);
|
||||
void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn);
|
||||
}
|
||||
}
|
||||
|
@ -298,6 +298,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool RedirectAutomatically
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
|
||||
public Money TxFee
|
||||
@ -356,7 +362,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
return DateTimeOffset.UtcNow > ExpirationTime;
|
||||
}
|
||||
|
||||
|
||||
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
ServerUrl = ServerUrl ?? "";
|
||||
@ -417,6 +422,22 @@ namespace BTCPayServer.Services.Invoices
|
||||
var scheme = info.Network.UriScheme;
|
||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
|
||||
|
||||
cryptoInfo.Payments = GetPayments(info.Network).Select(entity =>
|
||||
{
|
||||
var data = entity.GetCryptoPaymentData();
|
||||
return new InvoicePaymentInfo()
|
||||
{
|
||||
Id = data.GetPaymentId(),
|
||||
Fee = entity.NetworkFee,
|
||||
Value = data.GetValue(),
|
||||
Completed = data.PaymentCompleted(entity, info.Network),
|
||||
Confirmed = data.PaymentConfirmed(entity, SpeedPolicy, info.Network),
|
||||
Destination = data.GetDestination(info.Network),
|
||||
PaymentType = data.GetPaymentType().ToString(),
|
||||
ReceivedDate = entity.ReceivedTime.DateTime
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
@ -717,6 +738,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really
|
||||
/// need to save this information
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool PreferOnion { get; set; }
|
||||
|
||||
public PaymentMethodId GetId()
|
||||
{
|
||||
|
@ -71,14 +71,14 @@ retry:
|
||||
using (var db = _ContextFactory.CreateContext())
|
||||
{
|
||||
var key = scriptPubKey.Hash.ToString() + "#" + cryptoCode;
|
||||
var result = await db.AddressInvoices
|
||||
var result = (await db.AddressInvoices
|
||||
#pragma warning disable CS0618
|
||||
.Where(a => a.Address == key)
|
||||
#pragma warning restore CS0618
|
||||
.Select(a => a.InvoiceData)
|
||||
.Include(a => a.Payments)
|
||||
.Include(a => a.RefundAddresses)
|
||||
.FirstOrDefaultAsync();
|
||||
.ToListAsync()).FirstOrDefault();
|
||||
if (result == null)
|
||||
return null;
|
||||
return ToEntity(result);
|
||||
@ -118,7 +118,7 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, InvoiceLogs creationLogs, BTCPayNetworkProvider networkProvider)
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
List<string> textSearch = new List<string>();
|
||||
invoice = Clone(invoice, null);
|
||||
@ -165,17 +165,6 @@ retry:
|
||||
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
|
||||
}
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
|
||||
foreach (var log in creationLogs.ToList())
|
||||
{
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
Message = log.Log,
|
||||
Timestamp = log.Timestamp,
|
||||
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -191,6 +180,24 @@ retry:
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var log in logs.ToList())
|
||||
{
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
Message = log.Log,
|
||||
Timestamp = log.Timestamp,
|
||||
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDestination(PaymentMethod paymentMethod, Network network)
|
||||
{
|
||||
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
|
||||
@ -206,7 +213,7 @@ retry:
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId);
|
||||
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return false;
|
||||
|
||||
@ -376,7 +383,7 @@ retry:
|
||||
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
|
||||
query = query.Where(i => i.Id == id);
|
||||
|
||||
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
var invoice = (await query.ToListAsync()).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return null;
|
||||
|
||||
@ -487,7 +494,7 @@ retry:
|
||||
if (queryObject.Unusual != null)
|
||||
{
|
||||
var unused = queryObject.Unusual.Value;
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
|
||||
}
|
||||
|
||||
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
|
||||
|
31
BTCPayServer/Services/LightningClientFactoryService.cs
Normal file
31
BTCPayServer/Services/LightningClientFactoryService.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class LightningClientFactoryService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public LightningClientFactoryService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public ILightningClient Create(LightningConnectionString lightningConnectionString, BTCPayNetwork network)
|
||||
{
|
||||
if (lightningConnectionString == null)
|
||||
throw new ArgumentNullException(nameof(lightningConnectionString));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return new Lightning.LightningClientFactory(network.NBitcoinNetwork)
|
||||
{
|
||||
HttpClient = _httpClientFactory.CreateClient($"{network.CryptoCode}: Lightning client")
|
||||
}.Create(lightningConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Logging;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer.Services.Mails
|
||||
|
||||
public void SendEmail(string email, string subject, string message)
|
||||
{
|
||||
_JobClient.Schedule(async () =>
|
||||
_JobClient.Schedule(async (cancellationToken) =>
|
||||
{
|
||||
var emailSettings = await GetEmailSettings();
|
||||
if (emailSettings?.IsComplete() != true)
|
||||
@ -25,13 +26,21 @@ namespace BTCPayServer.Services.Mails
|
||||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||
return;
|
||||
}
|
||||
var smtp = emailSettings.CreateSmtpClient();
|
||||
var mail = new MailMessage(emailSettings.From, email, subject, message)
|
||||
using (var smtp = emailSettings.CreateSmtpClient())
|
||||
{
|
||||
IsBodyHtml = true
|
||||
};
|
||||
await smtp.SendMailAsync(mail);
|
||||
|
||||
var mail = new MailMessage(emailSettings.From, email, subject, message)
|
||||
{
|
||||
IsBodyHtml = true
|
||||
};
|
||||
try
|
||||
{
|
||||
await smtp.SendMailAsync(mail).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
smtp.SendAsyncCancel();
|
||||
}
|
||||
}
|
||||
}, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
@ -10,17 +11,19 @@ namespace BTCPayServer.Services
|
||||
public class PoliciesSettings
|
||||
{
|
||||
[Display(Name = "Requires a confirmation mail for registering")]
|
||||
public bool RequiresConfirmedEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool RequiresConfirmedEmail { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Disable registration")]
|
||||
public bool LockSubscription { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Discourage search engines from indexing this site")]
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
|
||||
[Display(Name = "Display app on website root")]
|
||||
public string RootAppId { get; set; }
|
||||
|
||||
public AppType? RootAppType { get; set; }
|
||||
}
|
||||
}
|
||||
|
40
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
40
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitbankRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitbankRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bitbank";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
|
||||
.Properties()
|
||||
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
|
||||
.ToArray());
|
||||
|
||||
}
|
||||
|
||||
private static BidAsk CreateBidAsk(JProperty p)
|
||||
{
|
||||
var buy = p.Value["buy"].Value<decimal>();
|
||||
var sell = p.Value["sell"].Value<decimal>();
|
||||
// Bug from their API (https://github.com/btcpayserver/btcpayserver/issues/741)
|
||||
return buy < sell ? new BidAsk(buy, sell) : new BidAsk(sell, buy);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public class CoinAverageExchange
|
||||
{
|
||||
public CoinAverageExchange(string name, string display)
|
||||
public CoinAverageExchange(string name, string display, string url)
|
||||
{
|
||||
Name = name;
|
||||
Display = display;
|
||||
Url = url;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Display { get; set; }
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
TryAdd(exchange.Name, exchange);
|
||||
if (!TryAdd(exchange.Name, exchange))
|
||||
{
|
||||
this.Remove(exchange.Name);
|
||||
this.Add(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
@ -84,7 +86,6 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "Gemini", Name: "gemini"),
|
||||
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
})
|
||||
{
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,9 @@ namespace BTCPayServer.Services.Rates
|
||||
lock (notFoundSymbols)
|
||||
{
|
||||
var exchangeRates =
|
||||
rates.Select(t => CreateExchangeRate(t))
|
||||
rates
|
||||
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
|
||||
.Select(t => CreateExchangeRate(t))
|
||||
.Where(t => t != null)
|
||||
.ToArray();
|
||||
return new ExchangeRates(exchangeRates);
|
||||
|
@ -39,7 +39,37 @@ namespace BTCPayServer.Services.Rates
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
|
||||
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
{"ADAXBT","ADAXBT"},
|
||||
{ "BSVUSD","BSVUSD"},
|
||||
{ "QTUMEUR","QTUMEUR"},
|
||||
{ "QTUMXBT","QTUMXBT"},
|
||||
{ "EOSUSD","EOSUSD"},
|
||||
{ "XTZUSD","XTZUSD"},
|
||||
{ "XREPZUSD","XREPZUSD"},
|
||||
{ "ADAEUR","ADAEUR"},
|
||||
{ "ADAUSD","ADAUSD"},
|
||||
{ "GNOEUR","GNOEUR"},
|
||||
{ "XTZETH","XTZETH"},
|
||||
{ "XXRPZJPY","XXRPZJPY"},
|
||||
{ "XXRPZCAD","XXRPZCAD"},
|
||||
{ "XTZEUR","XTZEUR"},
|
||||
{ "QTUMETH","QTUMETH"},
|
||||
{ "XXLMZUSD","XXLMZUSD"},
|
||||
{ "QTUMCAD","QTUMCAD"},
|
||||
{ "QTUMUSD","QTUMUSD"},
|
||||
{ "XTZXBT","XTZXBT"},
|
||||
{ "GNOUSD","GNOUSD"},
|
||||
{ "ADAETH","ADAETH"},
|
||||
{ "ADACAD","ADACAD"},
|
||||
{ "XTZCAD","XTZCAD"},
|
||||
{ "BSVEUR","BSVEUR"},
|
||||
{ "XZECZJPY","XZECZJPY"},
|
||||
{ "XXLMZEUR","XXLMZEUR"},
|
||||
{"EOSEUR","EOSEUR"},
|
||||
{"BSVXBT","BSVXBT"}
|
||||
});
|
||||
string[] _Symbols = Array.Empty<string>();
|
||||
DateTimeOffset? _LastSymbolUpdate = null;
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class QuadrigacxRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
public const string QuadrigacxName = "quadrigacx";
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public string ExchangeName => QuadrigacxName;
|
||||
|
||||
private bool TryToBidAsk(JObject p, out BidAsk v)
|
||||
{
|
||||
v = null;
|
||||
JToken bid = p.Property("bid")?.Value;
|
||||
JToken ask = p.Property("ask")?.Value;
|
||||
if (bid == null || ask == null)
|
||||
return false;
|
||||
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
|
||||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
|
||||
v1 <= 0m || v2 <= 0m || v1 > v2)
|
||||
return false;
|
||||
v = new BidAsk(v1, v2);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
var rate = new ExchangeRate();
|
||||
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
continue;
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = QuadrigacxName;
|
||||
if (!TryToBidAsk((JObject)prop.Value, out var v))
|
||||
continue;
|
||||
rate.BidAsk = v;
|
||||
exchangeRates.Add(rate);
|
||||
}
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
}
|
@ -109,10 +109,10 @@ namespace BTCPayServer.Services.Rates
|
||||
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
|
||||
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
|
||||
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
@ -168,9 +168,9 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
|
||||
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
|
64
BTCPayServer/Services/SocketFactory.cs
Normal file
64
BTCPayServer/Services/SocketFactory.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using NBitcoin.Protocol.Connectors;
|
||||
using NBitcoin.Protocol;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class SocketFactory
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
public SocketFactory(BTCPayServerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
DefaultEndpointConnector connector = new DefaultEndpointConnector();
|
||||
NodeConnectionParameters connectionParameters = new NodeConnectionParameters();
|
||||
if (_options.SocksEndpoint != null)
|
||||
{
|
||||
connectionParameters.TemplateBehaviors.Add(new NBitcoin.Protocol.Behaviors.SocksSettingsBehavior()
|
||||
{
|
||||
SocksEndpoint = _options.SocksEndpoint
|
||||
});
|
||||
}
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
try
|
||||
{
|
||||
await connector.ConnectSocket(socket, endPoint, connectionParameters, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SafeCloseSocket(socket);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
internal static void SafeCloseSocket(System.Net.Sockets.Socket socket)
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
socket.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
BTCPayServer/Services/TorServices.cs
Normal file
90
BTCPayServer/Services/TorServices.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class TorServices
|
||||
{
|
||||
BTCPayServerOptions _Options;
|
||||
public TorServices(BTCPayServerOptions options)
|
||||
{
|
||||
_Options = options;
|
||||
}
|
||||
|
||||
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
|
||||
|
||||
|
||||
internal async Task Refresh()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_Options.TorrcFile) || !File.Exists(_Options.TorrcFile))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_Options.TorrcFile))
|
||||
Logs.PayServer.LogWarning("Torrc file is not found");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
List<TorService> result = new List<TorService>();
|
||||
try
|
||||
{
|
||||
var torrcContent = await File.ReadAllTextAsync(_Options.TorrcFile);
|
||||
if (!Torrc.TryParse(torrcContent, out var torrc))
|
||||
{
|
||||
Logs.PayServer.LogWarning("Torrc file could not be parsed");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
|
||||
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
|
||||
.Select(d => (ServiceName: d.Directory.Name,
|
||||
ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.Directory.FullName, "hostname")),
|
||||
VirtualPort: d.VirtualPort))
|
||||
.ToArray();
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onionHost = (await service.ReadingLines)[0].Trim();
|
||||
var torService = new TorService()
|
||||
{
|
||||
Name = service.ServiceName,
|
||||
OnionHost = onionHost,
|
||||
VirtualPort = service.VirtualPort
|
||||
};
|
||||
if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
|
||||
torService.ServiceType = TorServiceType.BTCPayServer;
|
||||
result.Add(torService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading torrc file");
|
||||
}
|
||||
Services = result.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorService
|
||||
{
|
||||
public TorServiceType ServiceType { get; set; } = TorServiceType.Other;
|
||||
public string Name { get; set; }
|
||||
public string OnionHost { get; set; }
|
||||
public int VirtualPort { get; set; }
|
||||
}
|
||||
|
||||
public enum TorServiceType
|
||||
{
|
||||
BTCPayServer,
|
||||
Other
|
||||
}
|
||||
}
|
100
BTCPayServer/Services/Torrc.cs
Normal file
100
BTCPayServer/Services/Torrc.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Torrc
|
||||
{
|
||||
public static bool TryParse(string str, out Torrc value)
|
||||
{
|
||||
value = null;
|
||||
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
|
||||
var lines = str.Split(new char[] { '\n' });
|
||||
HiddenServiceDir currentDirectory = null;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (HiddenServiceDir.TryParse(line, out var dir))
|
||||
{
|
||||
serviceDirectories.Add(dir);
|
||||
currentDirectory = dir;
|
||||
}
|
||||
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
|
||||
{
|
||||
currentDirectory.ServicePorts.Add(portDef);
|
||||
}
|
||||
}
|
||||
value = new Torrc() { ServiceDirectories = serviceDirectories };
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach(var serviceDir in ServiceDirectories)
|
||||
{
|
||||
builder.AppendLine(serviceDir.ToString());
|
||||
foreach (var port in serviceDir.ServicePorts)
|
||||
builder.AppendLine(port.ToString());
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class HiddenServiceDir
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
|
||||
{
|
||||
serviceDir = null;
|
||||
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
|
||||
return true;
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; set; }
|
||||
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServiceDir {DirectoryPath}";
|
||||
}
|
||||
}
|
||||
public class HiddenServicePortDefinition
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
|
||||
{
|
||||
portDefinition = null;
|
||||
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
|
||||
return false;
|
||||
var addressPort = parts[2].Trim().Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (addressPort.Length != 2)
|
||||
return false;
|
||||
if (!int.TryParse(addressPort[1].Trim(), out int port))
|
||||
return false;
|
||||
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
|
||||
return false;
|
||||
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
|
||||
return true;
|
||||
}
|
||||
public int VirtualPort { get; set; }
|
||||
public IPEndPoint Endpoint { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServicePort {VirtualPort} {Endpoint}";
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<a href="https://docs.btcpayserver.org/features/apps" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Crowdfund";
|
||||
@ -137,6 +137,11 @@
|
||||
<input asp-for="NotificationUrl" class="form-control" />
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationEmail" class="control-label"></label>
|
||||
<input type="email" asp-for="NotificationEmail" class="form-control" />
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
|
@ -52,18 +52,22 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="EnableShoppingCart"></label>
|
||||
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check" />
|
||||
<span asp-validation-for="EnableShoppingCart" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ShowCustomAmount"></label>
|
||||
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
|
||||
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ShowDiscount"></label>
|
||||
<input asp-for="ShowDiscount" type="checkbox" class="form-check" />
|
||||
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EnableTips"></label>
|
||||
<input asp-for="EnableTips" type="checkbox" class="form-check" />
|
||||
<span asp-validation-for="EnableTips" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ButtonText" class="control-label"></label>*
|
||||
@ -104,6 +108,21 @@
|
||||
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
<span asp-validation-for="Template" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationUrl" class="control-label"></label>
|
||||
<input asp-for="NotificationUrl" class="form-control" />
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationEmail" class="control-label"></label>
|
||||
<input type="email" asp-for="NotificationEmail" class="form-control" />
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RedirectAutomatically" class="control-label"></label>*
|
||||
<select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-control"></select>
|
||||
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
</div>
|
||||
|
@ -135,7 +135,7 @@
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="Crowdfund/ContributeForm"
|
||||
name="/Views/AppsPublic/Crowdfund/ContributeForm.cshtml"
|
||||
model="@(new ContributeToCrowdfund()
|
||||
{
|
||||
ViewCrowdfundViewModel = Model,
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
@{
|
||||
@ -10,13 +10,13 @@
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@ -25,13 +25,15 @@
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
|
||||
<script src="~/modal/btcpay.js"></script>
|
||||
}
|
||||
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
@ -39,19 +41,19 @@
|
||||
<body>
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
|
||||
}
|
||||
else
|
||||
{
|
||||
<noscript>
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
|
||||
</noscript>
|
||||
|
||||
if (Model.AnimationsEnabled)
|
||||
{
|
||||
<canvas id="fireworks"></canvas>
|
||||
}
|
||||
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
|
||||
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model)
|
||||
}
|
||||
|
||||
</body>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
||||
@ -12,7 +12,7 @@
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
@ -20,20 +20,20 @@
|
||||
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
}
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
|
||||
@if (Model.EnableShoppingCart)
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<link rel="stylesheet" href="~/cart/css/style.css">
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||
<bundle name="wwwroot/bundles/cart-bundle.min.js"/>
|
||||
}
|
||||
<style>
|
||||
.card-deck {
|
||||
@ -41,9 +41,15 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
.card-deck .card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-100">
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
{image}
|
||||
@ -102,7 +108,7 @@
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
@ -121,10 +127,10 @@
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr class="h5">
|
||||
<tr class="h5">
|
||||
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-0">
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group-prepend">
|
||||
@ -136,18 +142,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
|
||||
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
|
||||
var percentage = CustomTipPercentages[i];
|
||||
<div class="col">
|
||||
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0)
|
||||
{
|
||||
@for (int i = 0; i < CustomTipPercentages.Length; i++)
|
||||
{
|
||||
var percentage = CustomTipPercentages[i];
|
||||
<div class="col">
|
||||
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tr>}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
@ -159,54 +167,55 @@
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<body class="h-100">
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white border-0">
|
||||
<h5 class="modal-title">Confirmation</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
|
||||
<span aria-hidden="true">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table id="js-cart-summary" class="table m-0">
|
||||
<tbody class="my-3">
|
||||
<tr class="h5">
|
||||
<td colspan="2" class="border-top-0">Summary</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-0">Total products</td>
|
||||
<td align="right" class="border-0 pb-0">
|
||||
<span class="js-cart-summary-products text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr class="h5">
|
||||
<td colspan="2" class="border-top-0">Summary</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-0">Total products</td>
|
||||
<td align="right" class="border-0 pb-0">
|
||||
<span class="js-cart-summary-products text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-y">Discount</td>
|
||||
<td align="right" class="border-0 pb-y">
|
||||
<span class="js-cart-summary-discount text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
}
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr class="h6">
|
||||
<td class="border-top-0 pt-0">Tip</td>
|
||||
<td align="right" class="border-top-0 pt-0">
|
||||
<span class="js-cart-summary-tip text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="h3 table-light">
|
||||
<td>Total</td>
|
||||
<td align="right">
|
||||
<span class="js-cart-summary-total text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="h3 table-light">
|
||||
<td>Total</td>
|
||||
<td align="right">
|
||||
<span class="js-cart-summary-total text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -214,184 +223,186 @@
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
|
||||
<b>@Model.CustomButtonText</b>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- Page Content -->
|
||||
<div id="content">
|
||||
<div class="p-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||
</div>
|
||||
<div class="col-sm-8 col-lg-9 mb-2">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<!-- Page Content -->
|
||||
<div id="content">
|
||||
<div class="p-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||
<a class="js-cart btn btn-warning text-white text-right" href="#">
|
||||
<i class="fa fa-shopping-basket"></i>
|
||||
<span class="badge badge-light badge-pill">
|
||||
<span id="js-cart-items">0</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row">
|
||||
|
||||
|
||||
@{
|
||||
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rowsJ; x++)
|
||||
{
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="col-sm-8 col-lg-9 mb-2">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute; right: 0px; z-index: 1049; display: none;">
|
||||
<i class="fa fa-times-circle fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="bg-dark">
|
||||
<div class="bg-warning p-3 clearfix">
|
||||
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
|
||||
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||
</div>
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
|
||||
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th colspan="3" width="55%">Product</th>
|
||||
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
|
||||
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
|
||||
|
||||
<div class="text-center mb-5 pb-5">
|
||||
<img src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
} else {
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
@{
|
||||
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rows; x++)
|
||||
{
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var item = Model.Items[index];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
|
||||
<div class="js-add-cart card my-2 card-wrapper" data-id="@index">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
@:<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer pt-0 bg-transparent border-0">
|
||||
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="bg-dark">
|
||||
<div class="bg-warning p-3 clearfix">
|
||||
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#">
|
||||
<i class="fa fa-times fa-lg"></i>
|
||||
</a>
|
||||
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||
</div>
|
||||
|
||||
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th colspan="3" width="55%">Product</th>
|
||||
<th class="text-center" width="20%">
|
||||
<div style="width: 84px">Quantity</div>
|
||||
</th>
|
||||
<th class="text-right" width="25%">
|
||||
<div style="min-width: 50px">Price</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit">
|
||||
<b>Confirm</b>
|
||||
</button>
|
||||
|
||||
<div class="text-center mb-5 pb-5">
|
||||
<img src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int x = 0; x < Model.Items.Length; x++)
|
||||
{
|
||||
var item = Model.Items[x];
|
||||
|
||||
<div class="card my-2" data-id="@x">
|
||||
@if (!String.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body pb-0">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(item.Description))
|
||||
{
|
||||
<p class="card-text">@item.Description</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
@if (item.Custom)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id"/>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="card my-2">
|
||||
<div class="card-body my-auto pb-0">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
|
||||
<span class="payment__currencies ">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
<span class="clickable_indicator fa fa-angle-right"></span>
|
||||
</span>
|
||||
@ -73,7 +73,7 @@
|
||||
{
|
||||
<div class="payment__currencies_noborder">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
</div>
|
||||
}
|
||||
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -207,7 +207,7 @@
|
||||
<div class="payment__scan">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon"
|
||||
v-if="scanDisplayQr" />
|
||||
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }"
|
||||
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
|
||||
v-if="scanDisplayQr">
|
||||
</qrcode>
|
||||
|
||||
@ -303,7 +303,7 @@
|
||||
:mode="srvModel.coinSwitchMode"
|
||||
:merchant-id="srvModel.coinSwitchMerchantId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.btcDue"
|
||||
:to-currency-due="coinswitchAmountDue"
|
||||
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.Services.LanguageService langService
|
||||
@using Newtonsoft.Json
|
||||
@using Newtonsoft.Json.Linq
|
||||
@ -51,9 +51,9 @@
|
||||
|
||||
<p>Alternatively, click below to continue to our HTML-only invoice.</p>
|
||||
|
||||
<form action="/invoice-noscript" method="GET">
|
||||
<button style="text-decoration: underline; color: blue">Continue to javascript-disabled invoice ></button>
|
||||
</form>
|
||||
<a href="/invoice-noscript?id=@Model.InvoiceId" style="text-decoration: underline; color: blue">
|
||||
Continue to javascript-disabled invoice >
|
||||
</a>
|
||||
</center>
|
||||
</noscript>
|
||||
|
||||
@ -176,7 +176,15 @@
|
||||
scanDisplayQr: "",
|
||||
expiringSoon: false,
|
||||
isModal: srvModel.isModal,
|
||||
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
|
||||
selectedThirdPartyProcessor: ""
|
||||
},
|
||||
computed: {
|
||||
coinswitchAmountDue: function() {
|
||||
return this.srvModel.coinSwitchAmountMarkupPercentage
|
||||
? (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
|
||||
: this.srvModel.btcDue;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
75
BTCPayServer/Views/Invoice/CheckoutNoScript.cshtml
Normal file
75
BTCPayServer/Views/Invoice/CheckoutNoScript.cshtml
Normal file
@ -0,0 +1,75 @@
|
||||
@model PaymentModel
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<META NAME="robots" CONTENT="noindex,nofollow">
|
||||
<title>@Model.HtmlTitle</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pay with @Model.StoreName</h1>
|
||||
@if (Model.Status == "new")
|
||||
{
|
||||
<div>
|
||||
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
|
||||
<p>Time remaining: @Model.TimeLeft</p>
|
||||
<p><a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;">@Model.InvoiceBitcoinUrl</a></p>
|
||||
@if (Model.IsLightning)
|
||||
{
|
||||
<p>Peer Info: <b>@Model.PeerInfo</b></p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div>
|
||||
<hr />
|
||||
<h2>Pay with:</h2>
|
||||
<ul style="list-style-type: none;padding-left: 5px;">
|
||||
@foreach (var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<li style="height: 32px; line-height: 32px;">
|
||||
<a href="/invoice-noscript?id=@Model.InvoiceId&paymentMethodId=@crypto.PaymentMethodId">
|
||||
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage" style="vertical-align:middle; height:24px; text-decoration:none; margin-top: -3px;" /></a>
|
||||
<a href="/invoice-noscript?id=@Model.InvoiceId&paymentMethodId=@crypto.PaymentMethodId" style="padding-top: 2px;">
|
||||
@crypto.PaymentMethodName
|
||||
@(crypto.IsLightning ? Html.Raw("⚡") : null)
|
||||
(@crypto.CryptoCode)
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (Model.Status == "paid" || Model.Status == "complete" || Model.Status == "confirmed")
|
||||
{
|
||||
<div>
|
||||
<p>This invoice has been paid</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.Status == "expired" || Model.Status == "invalid")
|
||||
{
|
||||
<div>
|
||||
<p>This invoice has expired</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
<p>Non-supported state of invoice</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<hr />
|
||||
<p>
|
||||
<a href="/i/@Model.InvoiceId">Go back to Javascript enabled invoice</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
@ -48,7 +48,9 @@
|
||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</a>
|
||||
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank">
|
||||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
|
||||
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
|
||||
@ -77,7 +79,12 @@
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th style="min-width: 90px;" class="col-md-auto">
|
||||
Date
|
||||
<a href="javascript:switchTimeFormat()">
|
||||
<span class="fa fa-clock-o" title="Switch date format"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>OrderId</th>
|
||||
<th>InvoiceId</th>
|
||||
<th>Status</th>
|
||||
@ -89,7 +96,11 @@
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
|
||||
@invoice.Date.ToBrowserDate()
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (invoice.RedirectUrl != string.Empty)
|
||||
{
|
||||
@ -142,31 +153,69 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav aria-label="...">
|
||||
<ul class="pagination">
|
||||
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
|
||||
<a class="page-link" tabindex="-1" href="@Url.Action("ListInvoices", new
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Math.Max(0, Model.Skip - Model.Count),
|
||||
count = Model.Count,
|
||||
})">Previous</a>
|
||||
<nav aria-label="..." class="w-100">
|
||||
<ul class="pagination float-left">
|
||||
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
|
||||
<a class="page-link" tabindex="-1" href="@listInvoices(-1, Model.Count)">«</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Invoices.Count) of @Model.Total</span>
|
||||
</li>
|
||||
<li class="page-item @(Model.Total > (Model.Skip + Model.Invoices.Count) ? null : "disabled")">
|
||||
<a class="page-link" href="@Url.Action("ListInvoices", new
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Model.Skip + Model.Count,
|
||||
count = Model.Count,
|
||||
})">Next</a>
|
||||
<a class="page-link" href="@listInvoices(1, Model.Count)">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="pagination float-right">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size:</span>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 50 ? "active" : null)">
|
||||
<a class="page-link" href="@listInvoices(0, 50)">50</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 100 ? "active" : null)">
|
||||
<a class="page-link" href="@listInvoices(0, 100)">100</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 250 ? "active" : null)">
|
||||
<a class="page-link" href="@listInvoices(0, 250)">250</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 500 ? "active" : null)">
|
||||
<a class="page-link" href="@listInvoices(0, 500)">500</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@{
|
||||
string listInvoices(int prevNext, int count)
|
||||
{
|
||||
var skip = Model.Skip;
|
||||
if (prevNext == -1)
|
||||
skip = Math.Max(0, Model.Skip - Model.Count);
|
||||
else if (prevNext == 1)
|
||||
skip = Model.Skip + count;
|
||||
|
||||
var act = Url.Action("ListInvoices", new
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = skip,
|
||||
count = count,
|
||||
});
|
||||
|
||||
return act;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function switchTimeFormat() {
|
||||
$(".switchTimeFormat").each(function (index) {
|
||||
var htmlVal = $(this).html();
|
||||
var switchVal = $(this).attr("data-switch");
|
||||
|
||||
$(this).html(switchVal);
|
||||
$(this).attr("data-switch", htmlVal);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
|
@ -23,6 +23,7 @@
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
|
||||
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
@ -11,13 +11,13 @@
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@ -26,13 +26,15 @@
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
|
||||
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
|
||||
<script src="~/modal/btcpay.js"></script>
|
||||
}
|
||||
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
</head>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
|
||||
@ -10,7 +10,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>@Model.CryptoCode LN Node Info</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
@ -18,10 +18,10 @@
|
||||
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
|
||||
@ -74,70 +74,76 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy { cursor: copy; }
|
||||
|
||||
.qr-container svg {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
cursor: copy;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
|
||||
<small class="@(Model.Available ? "text-success" : "text-danger")">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
@if (Model.Available)
|
||||
{
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
|
||||
<small class="@(Model.Available ? "text-success" : "text-danger")">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
@if (Model.Available)
|
||||
{
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app" class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
{{srvModel.cryptoCode}} Lightning Node
|
||||
- {{srvModel.available? "Online" : "Unavailable"}}
|
||||
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
|
||||
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</noscript>
|
||||
<div id="app" class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
{{srvModel.cryptoCode}} Lightning Node
|
||||
- {{srvModel.available? "Online" : "Unavailable"}}
|
||||
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
|
||||
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -28,7 +28,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-lg-3 ml-auto text-center">
|
||||
<a href="https://www.pebble.indiesquare.me/ target="_blank"">
|
||||
<a href="https://www.pebble.indiesquare.me/" target="_blank">
|
||||
<img src="~/img/pebblewallet.jpg" height="100" />
|
||||
</a>
|
||||
<p><a href="https://www.pebble.indiesquare.me/" target="_blank">Pebble</a></p>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user