Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b80b90609 | |||
efc3512994 | |||
acb8ca982f | |||
adc42cbba4 | |||
7edcb7ef5f | |||
656017c6df | |||
35db6d4a8b | |||
2741187546 | |||
c3a7ab647c | |||
92da0ec2d2 | |||
c767a49f2d | |||
ea8196b532 | |||
58f138e854 | |||
b4b6939498 | |||
eb5e32a07f | |||
708cdbe23f | |||
333de52c33 | |||
6b9932fa14 | |||
6c45689e6a | |||
1e3307c84c | |||
d0eed9857d | |||
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 | |||
8a8593437a |
@ -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,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()
|
||||
|
@ -56,7 +56,6 @@ using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -148,28 +147,6 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseEndpoint()
|
||||
{
|
||||
Assert.False(EndPointParser.TryParse("126.2.2.2", out var endpoint));
|
||||
Assert.True(EndPointParser.TryParse("126.2.2.2:20", out endpoint));
|
||||
var ipEndpoint = Assert.IsType<IPEndPoint>(endpoint);
|
||||
Assert.Equal("126.2.2.2", ipEndpoint.Address.ToString());
|
||||
Assert.Equal(20, ipEndpoint.Port);
|
||||
Assert.True(EndPointParser.TryParse("toto.com:20", out endpoint));
|
||||
var dnsEndpoint = Assert.IsType<DnsEndPoint>(endpoint);
|
||||
Assert.IsNotType<OnionEndpoint>(endpoint);
|
||||
Assert.Equal("toto.com", dnsEndpoint.Host.ToString());
|
||||
Assert.Equal(20, dnsEndpoint.Port);
|
||||
Assert.False(EndPointParser.TryParse("toto invalid hostname:2029", out endpoint));
|
||||
Assert.True(EndPointParser.TryParse("toto.onion:20", out endpoint));
|
||||
var onionEndpoint = Assert.IsType<OnionEndpoint>(endpoint);
|
||||
Assert.Equal("toto.onion", onionEndpoint.Host.ToString());
|
||||
Assert.Equal(20, onionEndpoint.Port);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseTorrc()
|
||||
@ -424,7 +401,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetLightningServer()
|
||||
{
|
||||
@ -462,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()
|
||||
{
|
||||
@ -516,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);
|
||||
@ -1096,6 +1075,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()
|
||||
@ -1533,7 +1555,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
@ -1688,21 +1710,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();
|
||||
@ -1712,52 +1733,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]
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.21
|
||||
image: nicolasdorier/nbxplorer:2.0.0.26
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.88</Version>
|
||||
<Version>1.0.3.92</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.14" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.15" />
|
||||
<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="NBitcoin" Version="4.1.1.98" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
|
||||
<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" />
|
||||
|
@ -146,9 +146,15 @@ namespace BTCPayServer.Configuration
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
SocksEndpoint = conf.GetOrDefault<EndPoint>("socksendpoint", null);
|
||||
if (SocksEndpoint is Tor.OnionEndpoint)
|
||||
throw new ConfigException($"socksendpoint should not be a tor endpoint");
|
||||
|
||||
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))
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -39,12 +40,6 @@ namespace BTCPayServer.Configuration
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPAddress))
|
||||
return (T)(object)IPAddress.Parse(str);
|
||||
else if (typeof(T) == typeof(EndPoint))
|
||||
{
|
||||
if (EndPointParser.TryParse(str, out var endpoint))
|
||||
return (T)(object)endpoint;
|
||||
throw new FormatException("Invalid endpoint");
|
||||
}
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
|
@ -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,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string NotificationEmail { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -101,7 +103,9 @@ 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
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
|
@ -184,6 +184,7 @@ namespace BTCPayServer.Controllers
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||
@ -262,7 +263,8 @@ namespace BTCPayServer.Controllers
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
NotificationURL = string.IsNullOrEmpty(notificationUrl)? settings.NotificationUrl: notificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData
|
||||
|
@ -188,7 +188,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 +213,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);
|
||||
@ -316,6 +333,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,
|
||||
@ -438,7 +456,7 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
return Ok("{}");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -105,7 +105,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
@ -138,7 +138,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 +200,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 +244,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)];
|
||||
@ -243,8 +258,12 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
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;
|
||||
@ -272,7 +291,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;
|
||||
|
@ -249,6 +249,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();
|
||||
@ -673,7 +680,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}";
|
||||
@ -683,14 +690,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static bool IsLocalNetwork(string server)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".local", 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)
|
||||
|
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class EndPointParser
|
||||
{
|
||||
public static bool TryParse(string hostPort, out EndPoint endpoint)
|
||||
{
|
||||
if (hostPort == null)
|
||||
throw new ArgumentNullException(nameof(hostPort));
|
||||
endpoint = null;
|
||||
var index = hostPort.LastIndexOf(':');
|
||||
if (index == -1)
|
||||
return false;
|
||||
var portStr = hostPort.Substring(index + 1);
|
||||
if (!ushort.TryParse(portStr, out var port))
|
||||
return false;
|
||||
return TryParse(hostPort.Substring(0, index), port, out endpoint);
|
||||
}
|
||||
public static bool TryParse(string host, int port, out EndPoint endpoint)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
endpoint = null;
|
||||
if (IPAddress.TryParse(host, out var address))
|
||||
endpoint = new IPEndPoint(address, port);
|
||||
else if (host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
endpoint = new OnionEndpoint(host, port);
|
||||
else
|
||||
{
|
||||
if (Uri.CheckHostName(host) != UriHostNameType.Dns)
|
||||
return false;
|
||||
endpoint = new DnsEndPoint(host, port);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -166,6 +166,24 @@ 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)
|
||||
@ -311,31 +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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -333,6 +333,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return Task.CompletedTask;
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -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
|
||||
{
|
||||
@ -218,7 +218,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;
|
||||
|
||||
|
@ -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,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
@ -10,7 +11,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 +25,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)]
|
||||
|
@ -66,5 +66,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CoinSwitchMode { get; set; }
|
||||
public string CoinSwitchMerchantId { get; set; }
|
||||
public string RootPath { get; set; }
|
||||
public decimal CoinSwitchAmountMarkupPercentage { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -9,8 +9,8 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tor;
|
||||
using BTCPayServer.Services;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -110,10 +110,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!EndPointParser.TryParse(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||
if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
||||
|
||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, SocketType.Stream, ProtocolType.Tcp, cancellation))
|
||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -10,117 +10,144 @@ 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;
|
||||
|
||||
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;
|
||||
Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>();
|
||||
Task _CheckingInvoice;
|
||||
Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>();
|
||||
|
||||
public LightningListener(EventAggregator aggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
IMemoryCache memoryCache,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_Aggregator = aggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_memoryCache = memoryCache;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
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, 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,56 +166,155 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
|
||||
HashSet<string> _InvoiceIds = new HashSet<string>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ILightningInvoiceListener session = null;
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
try
|
||||
{
|
||||
await _CheckingInvoice;
|
||||
}
|
||||
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;
|
||||
|
||||
public LightningInstanceListener(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
LightningSupportedPaymentMethod supportedPaymentMethod,
|
||||
BTCPayNetwork network)
|
||||
{
|
||||
this.supportedPaymentMethod = supportedPaymentMethod;
|
||||
this.invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
this.network = network;
|
||||
}
|
||||
internal bool AddListenedInvoice(ListenedInvoice invoice)
|
||||
{
|
||||
return _ListenedInvoices.TryAdd(invoice.PaymentMethodDetails.InvoiceId, invoice);
|
||||
}
|
||||
|
||||
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation)
|
||||
{
|
||||
var client = supportedPaymentMethod.CreateClient(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
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
var lightningClient = supportedPaymentMethod.CreateClient(network);
|
||||
session = await lightningClient.Listen(_Cts.Token);
|
||||
while (true)
|
||||
using (var session = await lightningClient.Listen(cancellation))
|
||||
{
|
||||
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)
|
||||
// Just in case the payment arrived after our last poll but before we listened.
|
||||
await PollAllListenedInvoices(cancellation);
|
||||
if (_ErrorAlreadyLogged)
|
||||
{
|
||||
if (notification.Status == LightningInvoiceStatus.Paid &&
|
||||
notification.PaidAt.HasValue && notification.Amount != null)
|
||||
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)
|
||||
{
|
||||
await AddPayment(network, notification, listenedInvoice);
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
if (notification.Status == LightningInvoiceStatus.Expired)
|
||||
{
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
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 when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
|
||||
{
|
||||
_ErrorAlreadyLogged = true;
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
DoneListening(supportedPaymentMethod.GetLightningUrl());
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
finally
|
||||
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)
|
||||
{
|
||||
session?.Dispose();
|
||||
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();
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
|
||||
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
|
||||
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(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
var payment = await invoiceRepository.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.BOLT11,
|
||||
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
|
||||
@ -196,102 +322,34 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}, network, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
|
||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
}
|
||||
return payment != null;
|
||||
}
|
||||
|
||||
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)
|
||||
internal void RemoveExpiredInvoices()
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
foreach (var invoice in _ListenedInvoices)
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_ListenedInvoiceByLightningUrl.Remove(listenedInvoice.Uri, listenedInvoice);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (invoice.Value.IsExpired())
|
||||
_ListenedInvoices.TryRemove(invoice.Key, out var _);
|
||||
}
|
||||
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)
|
||||
{
|
||||
listening = _ListeningLightning.ToArray();
|
||||
}
|
||||
await Task.WhenAll(listening);
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -56,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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -7,7 +8,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Tor;
|
||||
using NBitcoin.Protocol.Connectors;
|
||||
using NBitcoin.Protocol;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -18,58 +20,44 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, SocketType socketType, ProtocolType protocolType, CancellationToken cancellationToken)
|
||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
Socket socket = null;
|
||||
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
|
||||
{
|
||||
if (endPoint is IPEndPoint ipEndpoint)
|
||||
{
|
||||
socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken);
|
||||
}
|
||||
else if (endPoint is OnionEndpoint onionEndpoint)
|
||||
{
|
||||
if (_options.SocksEndpoint == null)
|
||||
throw new NotSupportedException("It is impossible to connect to an onion address without btcpay's -socksendpoint configured");
|
||||
socket = await Socks5Connect.ConnectSocksAsync(_options.SocksEndpoint, onionEndpoint, cancellationToken);
|
||||
}
|
||||
else if (endPoint is DnsEndPoint dnsEndPoint)
|
||||
{
|
||||
var address = (await Dns.GetHostAddressesAsync(dnsEndPoint.Host)).FirstOrDefault();
|
||||
socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(dnsEndPoint).WithCancellation(cancellationToken);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("Endpoint type not supported");
|
||||
await connector.ConnectSocket(socket, endPoint, connectionParameters, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseSocket(ref socket);
|
||||
throw;
|
||||
SafeCloseSocket(socket);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
private void CloseSocket(ref Socket s)
|
||||
internal static void SafeCloseSocket(System.Net.Sockets.Socket socket)
|
||||
{
|
||||
if (s == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
s.Shutdown(SocketShutdown.Both);
|
||||
socket.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
finally
|
||||
try
|
||||
{
|
||||
socket.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tor
|
||||
{
|
||||
public class OnionEndpoint : DnsEndPoint
|
||||
{
|
||||
public OnionEndpoint(string host, int port): base(host, port)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tor
|
||||
{
|
||||
public enum SocksErrorCode
|
||||
{
|
||||
Success = 0,
|
||||
GeneralServerFailure = 1,
|
||||
ConnectionNotAllowed = 2,
|
||||
NetworkUnreachable = 3,
|
||||
HostUnreachable = 4,
|
||||
ConnectionRefused = 5,
|
||||
TTLExpired = 6,
|
||||
CommandNotSupported = 7,
|
||||
AddressTypeNotSupported = 8,
|
||||
}
|
||||
public class SocksException : Exception
|
||||
{
|
||||
public SocksException(SocksErrorCode errorCode) : base(GetMessageForCode((int)errorCode))
|
||||
{
|
||||
SocksErrorCode = errorCode;
|
||||
}
|
||||
|
||||
public SocksErrorCode SocksErrorCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
private static string GetMessageForCode(int errorCode)
|
||||
{
|
||||
switch (errorCode)
|
||||
{
|
||||
case 0:
|
||||
return "Success";
|
||||
case 1:
|
||||
return "general SOCKS server failure";
|
||||
case 2:
|
||||
return "connection not allowed by ruleset";
|
||||
case 3:
|
||||
return "Network unreachable";
|
||||
case 4:
|
||||
return "Host unreachable";
|
||||
case 5:
|
||||
return "Connection refused";
|
||||
case 6:
|
||||
return "TTL expired";
|
||||
case 7:
|
||||
return "Command not supported";
|
||||
case 8:
|
||||
return "Address type not supported";
|
||||
default:
|
||||
return "Unknown code";
|
||||
}
|
||||
}
|
||||
|
||||
public SocksException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Socks5Connect
|
||||
{
|
||||
static readonly byte[] SelectionMessage = new byte[] { 5, 1, 0 };
|
||||
public static async Task<Socket> ConnectSocksAsync(EndPoint socksEndpoint, DnsEndPoint endpoint, CancellationToken cancellation)
|
||||
{
|
||||
Socket s = null;
|
||||
int maxTries = 3;
|
||||
int retry = 0;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await s.ConnectAsync(socksEndpoint).WithCancellation(cancellation).ConfigureAwait(false);
|
||||
NetworkStream stream = new NetworkStream(s, false);
|
||||
|
||||
await stream.WriteAsync(SelectionMessage, 0, SelectionMessage.Length, cancellation).ConfigureAwait(false);
|
||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
||||
|
||||
var selectionResponse = new byte[2];
|
||||
await stream.ReadAsync(selectionResponse, 0, 2, cancellation);
|
||||
if (selectionResponse[0] != 5)
|
||||
throw new SocksException("Invalid version in selection reply");
|
||||
if (selectionResponse[1] != 0)
|
||||
throw new SocksException("Unsupported authentication method in selection reply");
|
||||
|
||||
var connectBytes = CreateConnectMessage(endpoint.Host, endpoint.Port);
|
||||
await stream.WriteAsync(connectBytes, 0, connectBytes.Length, cancellation).ConfigureAwait(false);
|
||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
||||
|
||||
var connectResponse = new byte[10];
|
||||
await stream.ReadAsync(connectResponse, 0, 10, cancellation);
|
||||
if (connectResponse[0] != 5)
|
||||
throw new SocksException("Invalid version in connect reply");
|
||||
if (connectResponse[1] != 0)
|
||||
{
|
||||
var code = (SocksErrorCode)connectResponse[1];
|
||||
if (!IsTransient(code) || retry++ >= maxTries)
|
||||
throw new SocksException(code);
|
||||
CloseSocket(ref s);
|
||||
await Task.Delay(1000, cancellation).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
if (connectResponse[2] != 0)
|
||||
throw new SocksException("Invalid RSV in connect reply");
|
||||
if (connectResponse[3] != 1)
|
||||
throw new SocksException("Invalid ATYP in connect reply");
|
||||
for (int i = 4; i < 4 + 4; i++)
|
||||
{
|
||||
if (connectResponse[i] != 0)
|
||||
throw new SocksException("Invalid BIND address in connect reply");
|
||||
}
|
||||
|
||||
if (connectResponse[8] != 0 || connectResponse[9] != 0)
|
||||
throw new SocksException("Invalid PORT address connect reply");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseSocket(ref s);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseSocket(ref Socket s)
|
||||
{
|
||||
if (s == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
s.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTransient(SocksErrorCode code)
|
||||
{
|
||||
return code == SocksErrorCode.GeneralServerFailure ||
|
||||
code == SocksErrorCode.TTLExpired;
|
||||
}
|
||||
|
||||
internal static byte[] CreateConnectMessage(string host, int port)
|
||||
{
|
||||
byte[] sendBuffer;
|
||||
byte[] nameBytes = Encoding.ASCII.GetBytes(host);
|
||||
|
||||
var addressBytes =
|
||||
Enumerable.Empty<byte>()
|
||||
.Concat(new[] { (byte)nameBytes.Length })
|
||||
.Concat(nameBytes).ToArray();
|
||||
|
||||
sendBuffer =
|
||||
Enumerable.Empty<byte>()
|
||||
.Concat(
|
||||
new byte[]
|
||||
{
|
||||
(byte)5, (byte) 0x01, (byte) 0x00, (byte)0x03
|
||||
})
|
||||
.Concat(addressBytes)
|
||||
.Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port))).ToArray();
|
||||
return sendBuffer;
|
||||
}
|
||||
}
|
||||
}
|
@ -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"/>
|
||||
|
@ -104,6 +104,16 @@
|
||||
<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">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
@{
|
||||
|
@ -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,13 +41,15 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
|
||||
.card-deck .card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-100">
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
{image}
|
||||
@ -106,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">
|
||||
@ -125,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">
|
||||
@ -140,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">
|
||||
@ -163,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>
|
||||
@ -218,157 +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 card-deck my-3 mx-auto">
|
||||
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[index];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="js-add-cart card my-2" 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))
|
||||
{
|
||||
<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>
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int x = 0; x < Model.Items.Length; x++)
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[x];
|
||||
var item = Model.Items[index];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="card my-2" data-id="@x">
|
||||
@if (!String.IsNullOrWhiteSpace(item.Image))
|
||||
<div class="js-add-cart card my-2 card-wrapper" data-id="@index">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image" alt="Card image cap">
|
||||
@:<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(item.Description))
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@item.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>
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="card h-100">
|
||||
<div class="card-body my-auto">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
</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="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></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>
|
||||
</div>
|
||||
}
|
||||
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>
|
||||
}
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -303,7 +303,7 @@
|
||||
:mode="srvModel.coinSwitchMode"
|
||||
:merchant-id="srvModel.coinSwitchMerchantId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.btcDue"
|
||||
:to-currency-due="srvModel.coinSwitchAmountMarkupPercentage? (1 + (srvModel.coinSwitchAmountMarkupPercentage / 100) :srvModel.btcDue"
|
||||
: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>
|
||||
|
||||
|
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>
|
@ -89,7 +89,7 @@
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>@invoice.Date.ToBrowserDate()</td>
|
||||
<td>
|
||||
@if (invoice.RedirectUrl != string.Empty)
|
||||
{
|
||||
@ -142,29 +142,56 @@
|
||||
</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>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
|
||||
|
@ -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>
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
@if(!Model.ExposedSSH)
|
||||
@if (!Model.ExposedSSH)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>SSH Settings</h5>
|
||||
@ -62,6 +62,15 @@
|
||||
<button name="command" type="submit" class="btn btn-primary" value="update">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h5>Clean</h5>
|
||||
<span>Click here to delete unused docker images present on your system</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<button name="command" type="submit" class="btn btn-primary" value="clean">Clean</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,8 +10,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Some of your nodes are still synchronizing...<br />
|
||||
BTCPay Server will not create invoices with those cryptocurrencies.
|
||||
Your node is synching the entire blockchain and validating the consensus rules...
|
||||
</p>
|
||||
@foreach (var line in dashboard.GetAll())
|
||||
{
|
||||
@ -49,8 +48,8 @@
|
||||
{
|
||||
<li>The node is synchronized (Height: @line.Status.BitcoinStatus.Headers)</li>
|
||||
@if (line.Status.BitcoinStatus.IsSynched &&
|
||||
line.Status.SyncHeight.HasValue &&
|
||||
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
|
||||
line.Status.SyncHeight.HasValue &&
|
||||
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
|
||||
{
|
||||
<li>NBXplorer is synchronizing... (Height: @line.Status.SyncHeight.Value)</li>
|
||||
}
|
||||
@ -72,7 +71,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
<p>
|
||||
<a href="https://www.youtube.com/watch?v=OrYDehC-8TU" target="_blank">Watch this video</a> to understand the importance of blockchain synchronization.
|
||||
</p>
|
||||
|
||||
<p>If you really don't want to synch and that you are familiar with command line, check <a href="https://github.com/btcpayserver/btcpayserver-docker/blob/master/contrib/FastSync/README.md" target="_blank">FastSync</a>.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@ -85,13 +85,14 @@
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
else if (env.IsSecure)
|
||||
{
|
||||
if (themeManager.ShowRegister)
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@ -99,6 +100,14 @@
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you use a reverse proxy, please set the <b>X-Forwarded-Proto</b> header to <b id="browserScheme">@(env.ExpectedProtocol)</b> (<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-deployment#btcpay-is-expecting-you-to-access-this-website-from" target="_blank">More information</a>)</span>
|
||||
</div>
|
||||
@if (!env.IsSecure)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<span>You access BTCPay Server over an unsecured network. If you are using docker deployment with NGINX and HTTPS is not available, you probably did not configured your DNS settings right. <br />
|
||||
We disabled the register and login link so you don't leak your credentials.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
|
||||
<bundle name="wwwroot/bundles/jqueryvalidate-bundle.min.js" />
|
||||
|
@ -30,7 +30,11 @@
|
||||
<select asp-for="Mode" asp-items="Model.Modes" class="form-control">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="AmountMarkupPercentage"></label>
|
||||
<input asp-for="AmountMarkupPercentage" class="form-control"/>
|
||||
<span asp-validation-for="AmountMarkupPercentage" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
|
@ -94,7 +94,7 @@ $(document).ready(function(){
|
||||
$('#js-pos-list').find(".card-wrapper").show();
|
||||
|
||||
if (str.length > 1) {
|
||||
var $list = $('#js-pos-list').find(".card-title:not(:icontains('" + str + "'))");
|
||||
var $list = $('#js-pos-list').find(".card-title:not(:icontains('" + $.escapeSelector(str) + "'))");
|
||||
$list.parents('.card-wrapper').hide();
|
||||
$('.js-search-reset').show();
|
||||
}
|
||||
@ -133,4 +133,4 @@ $(document).ready(function(){
|
||||
$tip.trigger('input');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11513,7 +11513,7 @@ low-fee-timeline {
|
||||
}
|
||||
|
||||
[v-cloak] > * { display:none }
|
||||
[v-cloak]::before { content: "loading…" }
|
||||
[v-cloak]::before { content: "" }
|
||||
|
||||
|
||||
.btn-link {
|
||||
|
@ -106,7 +106,8 @@ function onDataCallback(jsonData) {
|
||||
}
|
||||
|
||||
function numberFormatted(x) {
|
||||
var parts = x.toString().split(".");
|
||||
var rounded = Math.round(x);
|
||||
var parts = rounded.toString().split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
return parts.join(".");
|
||||
}
|
||||
|
@ -24,16 +24,16 @@
|
||||
"Copied": "Copiado",
|
||||
"ConversionTab_BodyTop": "Você pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
|
||||
"ConversionTab_BodyDesc": "Esse serviço é oferecido por terceiros. Por favor, tenha em mente que não temos nenhum controle sobre como seus fundos serão utilizados. A fatura apenas será marcada como paga quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
|
||||
"ConversionTab_CalculateAmount_Error": "Retry",
|
||||
"ConversionTab_LoadCurrencies_Error": "Retry",
|
||||
"ConversionTab_CalculateAmount_Error": "Tentar novamente",
|
||||
"ConversionTab_LoadCurrencies_Error": "Tentar novamente",
|
||||
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Por favor selecione a moeda da qual pretende converter",
|
||||
"Invoice expiring soon...": "A fatura está expirando...",
|
||||
"Invoice expired": "Fatura expirada",
|
||||
"What happened?": "O que aconteceu?",
|
||||
"InvoiceExpired_Body_1": "Essa fatura expirou. Uma fatura é válida por apenas {{maxTimeMinutes}} minutos. \nVocê pode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.",
|
||||
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento, ele ainda não foi aceito pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
|
||||
"InvoiceExpired_Body_3": "",
|
||||
"InvoiceExpired_Body_3": "Se o recebermos mais tarde, vamos processar o pedido ou entrar em contato para combinarmos uma devolução...",
|
||||
"Invoice ID": "Nº da Fatura",
|
||||
"Order ID": "Nº do Pedido",
|
||||
"Return to StoreName": "Voltar para {{storeName}}",
|
||||
@ -41,10 +41,10 @@
|
||||
"This invoice has been archived": "Essa fatura foi arquivada",
|
||||
"Archived_Body": "Por favor, entre em contato com o estabelecimento para informações e suporte",
|
||||
"BOLT 11 Invoice": "Fatura BOLT 11",
|
||||
"Node Info": "Informação de nó",
|
||||
"Node Info": "Informação do nó",
|
||||
"txCount": "{{count}} transação",
|
||||
"txCount_plural": "{{count}} transações",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close"
|
||||
"Pay with CoinSwitch": "Pagar com CoinSwitch",
|
||||
"Pay with Changelly": "Pagar com Changelly",
|
||||
"Close": "Fechar"
|
||||
}
|
@ -24,16 +24,16 @@
|
||||
"Copied": "Copiado",
|
||||
"ConversionTab_BodyTop": "Pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
|
||||
"ConversionTab_BodyDesc": "Este serviço é oferecido por terceiros. Por favor tenha em mente que não temos qualquer controlo sobre como os seus fundos serão utilizados. A fatura será marcada como paga apenas quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
|
||||
"ConversionTab_CalculateAmount_Error": "Retry",
|
||||
"ConversionTab_LoadCurrencies_Error": "Retry",
|
||||
"ConversionTab_CalculateAmount_Error": "Tentar de novo",
|
||||
"ConversionTab_LoadCurrencies_Error": "Tentar de novo",
|
||||
"ConversionTab_Lightning": "Não há fornecedores de conversão disponíveis para pagamentos via Lightning Network.",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Por favor selecione uma moeda da qual converter",
|
||||
"Invoice expiring soon...": "A fatura está a expirar...",
|
||||
"Invoice expired": "Fatura expirada",
|
||||
"What happened?": "O que aconteceu?",
|
||||
"InvoiceExpired_Body_1": "Esta fatura expirou. Uma fatura é válida durante {{maxTimeMinutes}} minutos. \nPode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.",
|
||||
"InvoiceExpired_Body_2": "Se tentou enviar um pagamento, ele ainda não foi aceite pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
|
||||
"InvoiceExpired_Body_3": "",
|
||||
"InvoiceExpired_Body_3": "Se o recebermos mais tarde, processaremos o seu pedido ou entraremos em contacto para o reembolsar ...",
|
||||
"Invoice ID": "Nº da Fatura",
|
||||
"Order ID": "Nº da Encomenda",
|
||||
"Return to StoreName": "Voltar para {{storeName}}",
|
||||
@ -44,7 +44,7 @@
|
||||
"Node Info": "Informação do Nó",
|
||||
"txCount": "{{count}} transação",
|
||||
"txCount_plural": "{{count}} transações",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close"
|
||||
"Pay with CoinSwitch": "Pagar com CoinSwitch",
|
||||
"Pay with Changelly": "Pagar com Changelly",
|
||||
"Close": "Fechar"
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505 AS builder
|
||||
RUN apt-get update \
|
||||
&& apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
|
||||
RUN dotnet restore
|
||||
@ -8,6 +11,9 @@ RUN dotnet publish --output /app/ --configuration Release
|
||||
|
||||
# Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-stretch-slim-arm32v7
|
||||
COPY --from=builder /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
@ -34,16 +34,18 @@ Thanks to the [apps](https://github.com/btcpayserver/btcpayserver-doc/blob/maste
|
||||
* Self-hosted
|
||||
* SegWit support
|
||||
* Lightning Network support (LND and c-lightning)
|
||||
* Altcoin support
|
||||
* Tor support
|
||||
* Opt-in Altcoin integrations
|
||||
* Full compatibility with BitPay API (easy migration)
|
||||
* Process payments for others
|
||||
* Easy-embeddable Payment buttons
|
||||
* Point of sale app
|
||||
* Crowdfunding app
|
||||
* Payment requests
|
||||
|
||||
## Supported Altcoins
|
||||
|
||||
Bitcoin is the only focus of the project and its core developers. However, support is implemented for several altcoins:
|
||||
Bitcoin is the only focus of the project and its core developers. However, opt in integrations for several altcoins maintained by altcoins community is implemented for several altcoins:
|
||||
|
||||
* Bitcoin Gold (BTG)
|
||||
* Bitcoin Plus (XBC)
|
||||
@ -70,7 +72,7 @@ For general questions, please join the community chat on [Mattermost](https://ch
|
||||
|
||||
While the documentation advises to use docker-compose, you may want to build BTCPay yourself.
|
||||
|
||||
First install .NET Core SDK v2.1.6 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
|
||||
First install .NET Core SDK v2.1.9 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
|
@ -1,6 +1,8 @@
|
||||
$ver = [regex]::Match((Get-Content BTCPayServer\BTCPayServer.csproj), '<Version>([^<]+)<').Groups[1].Value
|
||||
git tag -a "v$ver" -m "$ver"
|
||||
git push --tags
|
||||
git checkout latest
|
||||
git merge master
|
||||
git checkout master
|
||||
git tag -d "stable"
|
||||
git tag -a "stable" -m "stable"
|
||||
git push --tags --force
|
||||
git push origin latest master --tags --force
|
||||
|
Reference in New Issue
Block a user