Compare commits
88 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 | |||
be90172840 | |||
b85ee895f5 | |||
93de408e07 | |||
d3662ae734 | |||
132d7795ea | |||
bf5a624209 | |||
abbdbda03a | |||
8a8593437a | |||
e203cada54 | |||
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
5c618233cb | |||
9e91259b9e | |||
014d08f38a | |||
7998ea142b | |||
a4051dac72 | |||
e3a8892d24 | |||
ea02d77e69 | |||
4f582a6712 |
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -35,6 +35,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -120,7 +121,8 @@ namespace BTCPayServer.Tests
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
HttpClient = new HttpClient();
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
@ -209,6 +211,8 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
|
@ -1,12 +1,9 @@
|
||||
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
@ -35,27 +36,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public void Advance(TimeSpan time)
|
||||
public async Task Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
List<WaitObj> overdue = new List<WaitObj>();
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
wait.CTS.TrySetResult(true);
|
||||
overdue.Add(wait);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var o in overdue)
|
||||
o.CTS.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void AdvanceMilliseconds(long milli)
|
||||
{
|
||||
Advance(TimeSpan.FromMilliseconds(milli));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -85,9 +86,11 @@ namespace BTCPayServer.Tests
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task EnsureChannelsSetup()
|
||||
public async Task EnsureChannelsSetup()
|
||||
{
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
Logs.Tester.LogInformation("Connecting channels");
|
||||
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
|
||||
Logs.Tester.LogInformation("Channels connected");
|
||||
}
|
||||
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
|
@ -55,6 +55,7 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -146,6 +147,45 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseTorrc()
|
||||
{
|
||||
var nl = "\n";
|
||||
var input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
nl = Environment.NewLine;
|
||||
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out var torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
nl = "\r\n";
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80";
|
||||
nl = Environment.NewLine;
|
||||
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
@ -361,7 +401,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetLightningServer()
|
||||
{
|
||||
@ -399,21 +439,21 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCLightning()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentLnd()
|
||||
{
|
||||
@ -453,7 +493,9 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
|
||||
Logs.Tester.LogInformation($"Trying to send Lightning payment to {invoice.Id}");
|
||||
await tester.SendLightningPaymentAsync(invoice);
|
||||
Logs.Tester.LogInformation($"Lightning payment to {invoice.Id} is sent");
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
|
||||
@ -881,7 +923,7 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
foreach(var req in new[]
|
||||
foreach (var req in new[]
|
||||
{
|
||||
"invoices/",
|
||||
"invoices",
|
||||
@ -1033,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()
|
||||
@ -1470,7 +1555,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 1000)]
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
@ -1625,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();
|
||||
@ -1649,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]
|
||||
@ -2264,15 +2348,22 @@ donation:
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
if (result.ExpectedName == "bitbank")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
|
||||
}
|
||||
else
|
||||
{
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
|
||||
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
|
||||
);
|
||||
}
|
||||
}
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
@ -2365,7 +2456,7 @@ donation:
|
||||
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
|
||||
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
|
||||
|
||||
|
||||
// Error if directory not exists
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
|
||||
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.15
|
||||
image: nicolasdorier/nbxplorer:2.0.0.26
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.85</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.13" />
|
||||
<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" />
|
||||
@ -67,7 +69,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -48,11 +48,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -149,7 +145,16 @@ namespace BTCPayServer.Configuration
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
TorHiddenServicesDirectory = conf.GetOrDefault<string>("torservices", null);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||
if(!string.IsNullOrEmpty(socksEndpointString))
|
||||
{
|
||||
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -274,6 +279,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TorHiddenServicesDirectory { get; set; }
|
||||
public string TorrcFile { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
|
@ -40,7 +40,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--torservices", "Path to folder containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -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)];
|
||||
@ -242,8 +257,13 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
|
||||
using (logs.Measure($"{logPrefix} Payment method details creation"))
|
||||
{
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
}
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
@ -271,7 +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;
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
|
@ -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();
|
||||
@ -446,7 +453,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public async Task<IActionResult> Services()
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
result.ExternalServices = _Options.ExternalServices;
|
||||
@ -466,7 +473,25 @@ namespace BTCPayServer.Controllers
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
result.TorServices = await _torServices.GetServices();
|
||||
foreach(var torService in _torServices.Services)
|
||||
{
|
||||
if (torService.VirtualPort == 80)
|
||||
{
|
||||
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"http://{torService.OnionHost}"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
|
||||
});
|
||||
}
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
@ -655,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}";
|
||||
@ -665,13 +690,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static bool IsLocalNetwork(string server)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.Mode = existing.Mode;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
MerchantId = vm.MerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
Mode = vm.Mode
|
||||
Mode = vm.Mode,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
|
@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -33,6 +33,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -165,6 +166,31 @@ namespace BTCPayServer
|
||||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
||||
}
|
||||
|
||||
public static bool IsLocalNetwork(string server)
|
||||
{
|
||||
if (server == null)
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
|
||||
}
|
||||
if(IPAddress.TryParse(server, out var ip))
|
||||
{
|
||||
return ip.IsLocal();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsOnion(this HttpRequest request)
|
||||
{
|
||||
if (request?.Host.Host == null)
|
||||
return false;
|
||||
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -303,30 +329,6 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -34,6 +35,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Stop == null)
|
||||
return;
|
||||
_Stop.Cancel();
|
||||
try
|
||||
{
|
||||
@ -43,7 +46,14 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
||||
}
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
try
|
||||
{
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +61,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
class BackgroundJob
|
||||
{
|
||||
public Func<Task> Action;
|
||||
public Func<CancellationToken, Task> Action;
|
||||
public TimeSpan Delay;
|
||||
public IDelay DelayImplementation;
|
||||
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
{
|
||||
this.Action = action;
|
||||
this.Delay = delay;
|
||||
@ -64,7 +74,7 @@ namespace BTCPayServer.HostedServices
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
await DelayImplementation.Wait(Delay, cancellationToken);
|
||||
await Action();
|
||||
await Action(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +89,9 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
|
||||
HashSet<Task> _Processing = new HashSet<Task>();
|
||||
public void Schedule(Func<Task> action, TimeSpan delay)
|
||||
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
|
||||
{
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
|
||||
}
|
||||
|
||||
public async Task WaitAllRunning(CancellationToken cancellationToken)
|
||||
@ -89,6 +99,8 @@ namespace BTCPayServer.HostedServices
|
||||
Task[] processing = null;
|
||||
lock (_Processing)
|
||||
{
|
||||
if (_Processing.Count == 0)
|
||||
return;
|
||||
processing = _Processing.ToArray();
|
||||
}
|
||||
|
||||
@ -96,9 +108,8 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll(processing).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,8 +124,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
_Processing.Add(processing);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
processing.ContinueWith(t =>
|
||||
_ = processing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
@ -125,7 +135,6 @@ namespace BTCPayServer.HostedServices
|
||||
_Processing.Remove(processing);
|
||||
}
|
||||
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
|
@ -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;
|
||||
@ -68,7 +69,7 @@ namespace BTCPayServer.HostedServices
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
|
||||
{
|
||||
exchanges.Add(item);
|
||||
}
|
||||
|
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class TorServicesHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
private readonly TorServices _torServices;
|
||||
|
||||
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
|
||||
{
|
||||
_options = options;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
|
||||
if (string.IsNullOrEmpty(_options.TorrcFile))
|
||||
return Array.Empty<Task>();
|
||||
return new Task[] { CreateLoopTask(RefreshTorServices) };
|
||||
}
|
||||
|
||||
async Task RefreshTorServices()
|
||||
{
|
||||
await _torServices.Refresh();
|
||||
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,6 @@ using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,6 +47,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Npgsql;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -63,6 +63,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<InvoiceRepository>(o =>
|
||||
@ -189,6 +190,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
@ -216,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; }
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
|
||||
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
|
||||
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
|
||||
public TorService[] TorServices { get; set; } = Array.Empty<TorService>();
|
||||
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
|
||||
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
|
||||
}
|
||||
}
|
||||
|
@ -21,14 +21,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
RateSource = chosen.Url;
|
||||
}
|
||||
|
||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||
@ -59,10 +61,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -17,15 +19,19 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
private readonly SocketFactory _socketFactory;
|
||||
|
||||
public LightningLikePaymentHandler(
|
||||
NBXplorerDashboard dashboard)
|
||||
NBXplorerDashboard dashboard,
|
||||
SocketFactory socketFactory)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_socketFactory = socketFactory;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = GetNodeInfo(supportedPaymentMethod, network);
|
||||
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
@ -63,7 +69,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
@ -84,8 +90,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if (info.NodeInfo == null)
|
||||
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
|
||||
if (nodeInfo == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
@ -96,7 +102,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
|
||||
return info.NodeInfo;
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,22 +110,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
try
|
||||
{
|
||||
IPAddress address = null;
|
||||
try
|
||||
{
|
||||
address = IPAddress.Parse(nodeInfo.Host);
|
||||
}
|
||||
catch
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
|
||||
}
|
||||
if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
||||
|
||||
if (address == null)
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation))
|
||||
{
|
||||
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -10,117 +10,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
|
||||
{
|
||||
|
@ -14,7 +14,8 @@ namespace BTCPayServer.Services
|
||||
public class BTCPayServerEnvironment
|
||||
{
|
||||
IHttpContextAccessor httpContext;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
|
||||
TorServices torServices;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
|
||||
{
|
||||
this.httpContext = httpContext;
|
||||
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
|
||||
@ -25,6 +26,7 @@ namespace BTCPayServer.Services
|
||||
#endif
|
||||
Environment = env;
|
||||
NetworkType = provider.NetworkType;
|
||||
this.torServices = torServices;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
@ -34,6 +36,8 @@ namespace BTCPayServer.Services
|
||||
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
|
||||
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
|
||||
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
|
||||
public string OnionUrl => this.torServices.Services.Where(s => s.ServiceType == TorServiceType.BTCPayServer)
|
||||
.Select(s => $"http://{s.OnionHost}").FirstOrDefault();
|
||||
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
@ -52,6 +56,18 @@ namespace BTCPayServer.Services
|
||||
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSecure
|
||||
{
|
||||
get
|
||||
{
|
||||
return NetworkType != NetworkType.Mainnet ||
|
||||
httpContext.HttpContext.Request.Scheme == "https" ||
|
||||
httpContext.HttpContext.Request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) ||
|
||||
Extensions.IsLocalNetwork(httpContext.HttpContext.Request.Host.Host);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder txt = new StringBuilder();
|
||||
|
@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public interface IBackgroundJobClient
|
||||
{
|
||||
void Schedule(Func<Task> act, TimeSpan zero);
|
||||
void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn);
|
||||
}
|
||||
}
|
||||
|
@ -717,6 +717,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really
|
||||
/// need to save this information
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool PreferOnion { get; set; }
|
||||
|
||||
public PaymentMethodId GetId()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
32
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
32
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitbankRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitbankRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bitbank";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
|
||||
.Properties()
|
||||
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), new BidAsk(p.Value["buy"].Value<decimal>(), p.Value["sell"].Value<decimal>())))
|
||||
.ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public class CoinAverageExchange
|
||||
{
|
||||
public CoinAverageExchange(string name, string display)
|
||||
public CoinAverageExchange(string name, string display, string url)
|
||||
{
|
||||
Name = name;
|
||||
Display = display;
|
||||
Url = url;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Display { get; set; }
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
TryAdd(exchange.Name, exchange);
|
||||
if (!TryAdd(exchange.Name, exchange))
|
||||
{
|
||||
this.Remove(exchange.Name);
|
||||
this.Add(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
@ -84,7 +86,6 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "Gemini", Name: "gemini"),
|
||||
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
})
|
||||
{
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,9 @@ namespace BTCPayServer.Services.Rates
|
||||
lock (notFoundSymbols)
|
||||
{
|
||||
var exchangeRates =
|
||||
rates.Select(t => CreateExchangeRate(t))
|
||||
rates
|
||||
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
|
||||
.Select(t => CreateExchangeRate(t))
|
||||
.Where(t => t != null)
|
||||
.ToArray();
|
||||
return new ExchangeRates(exchangeRates);
|
||||
|
@ -39,7 +39,37 @@ namespace BTCPayServer.Services.Rates
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
|
||||
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
{"ADAXBT","ADAXBT"},
|
||||
{ "BSVUSD","BSVUSD"},
|
||||
{ "QTUMEUR","QTUMEUR"},
|
||||
{ "QTUMXBT","QTUMXBT"},
|
||||
{ "EOSUSD","EOSUSD"},
|
||||
{ "XTZUSD","XTZUSD"},
|
||||
{ "XREPZUSD","XREPZUSD"},
|
||||
{ "ADAEUR","ADAEUR"},
|
||||
{ "ADAUSD","ADAUSD"},
|
||||
{ "GNOEUR","GNOEUR"},
|
||||
{ "XTZETH","XTZETH"},
|
||||
{ "XXRPZJPY","XXRPZJPY"},
|
||||
{ "XXRPZCAD","XXRPZCAD"},
|
||||
{ "XTZEUR","XTZEUR"},
|
||||
{ "QTUMETH","QTUMETH"},
|
||||
{ "XXLMZUSD","XXLMZUSD"},
|
||||
{ "QTUMCAD","QTUMCAD"},
|
||||
{ "QTUMUSD","QTUMUSD"},
|
||||
{ "XTZXBT","XTZXBT"},
|
||||
{ "GNOUSD","GNOUSD"},
|
||||
{ "ADAETH","ADAETH"},
|
||||
{ "ADACAD","ADACAD"},
|
||||
{ "XTZCAD","XTZCAD"},
|
||||
{ "BSVEUR","BSVEUR"},
|
||||
{ "XZECZJPY","XZECZJPY"},
|
||||
{ "XXLMZEUR","XXLMZEUR"},
|
||||
{"EOSEUR","EOSEUR"},
|
||||
{"BSVXBT","BSVXBT"}
|
||||
});
|
||||
string[] _Symbols = Array.Empty<string>();
|
||||
DateTimeOffset? _LastSymbolUpdate = null;
|
||||
|
||||
|
@ -113,6 +113,7 @@ namespace BTCPayServer.Services.Rates
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
|
||||
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
|
||||
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
@ -168,9 +169,9 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
|
||||
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
|
64
BTCPayServer/Services/SocketFactory.cs
Normal file
64
BTCPayServer/Services/SocketFactory.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using NBitcoin.Protocol.Connectors;
|
||||
using NBitcoin.Protocol;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class SocketFactory
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
public SocketFactory(BTCPayServerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
DefaultEndpointConnector connector = new DefaultEndpointConnector();
|
||||
NodeConnectionParameters connectionParameters = new NodeConnectionParameters();
|
||||
if (_options.SocksEndpoint != null)
|
||||
{
|
||||
connectionParameters.TemplateBehaviors.Add(new NBitcoin.Protocol.Behaviors.SocksSettingsBehavior()
|
||||
{
|
||||
SocksEndpoint = _options.SocksEndpoint
|
||||
});
|
||||
}
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
try
|
||||
{
|
||||
await connector.ConnectSocket(socket, endPoint, connectionParameters, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SafeCloseSocket(socket);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
internal static void SafeCloseSocket(System.Net.Sockets.Socket socket)
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
socket.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -15,35 +17,74 @@ namespace BTCPayServer.Services
|
||||
_Options = options;
|
||||
}
|
||||
|
||||
public async Task<TorService[]> GetServices()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_Options.TorHiddenServicesDirectory) || !Directory.Exists(_Options.TorHiddenServicesDirectory))
|
||||
return Array.Empty<TorService>();
|
||||
List<TorService> result = new List<TorService>();
|
||||
var servicesDirs = Directory.GetDirectories(_Options.TorHiddenServicesDirectory);
|
||||
var services = servicesDirs
|
||||
.Select(d => new DirectoryInfo(d))
|
||||
.Select(d => (ServiceName: d.Name, ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.FullName, "hostname"))))
|
||||
.ToArray();
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onionUrl = (await service.ReadingLines)[0].Trim();
|
||||
result.Add(new TorService() { Name = service.ServiceName, OnionUrl = onionUrl });
|
||||
}
|
||||
catch
|
||||
{
|
||||
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
|
||||
|
||||
|
||||
internal async Task Refresh()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_Options.TorrcFile) || !File.Exists(_Options.TorrcFile))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_Options.TorrcFile))
|
||||
Logs.PayServer.LogWarning("Torrc file is not found");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
List<TorService> result = new List<TorService>();
|
||||
try
|
||||
{
|
||||
var torrcContent = await File.ReadAllTextAsync(_Options.TorrcFile);
|
||||
if (!Torrc.TryParse(torrcContent, out var torrc))
|
||||
{
|
||||
Logs.PayServer.LogWarning("Torrc file could not be parsed");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
|
||||
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
|
||||
.Select(d => (ServiceName: d.Directory.Name,
|
||||
ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.Directory.FullName, "hostname")),
|
||||
VirtualPort: d.VirtualPort))
|
||||
.ToArray();
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onionHost = (await service.ReadingLines)[0].Trim();
|
||||
var torService = new TorService()
|
||||
{
|
||||
Name = service.ServiceName,
|
||||
OnionHost = onionHost,
|
||||
VirtualPort = service.VirtualPort
|
||||
};
|
||||
if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
|
||||
torService.ServiceType = TorServiceType.BTCPayServer;
|
||||
result.Add(torService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading torrc file");
|
||||
}
|
||||
Services = result.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorService
|
||||
{
|
||||
public TorServiceType ServiceType { get; set; } = TorServiceType.Other;
|
||||
public string Name { get; set; }
|
||||
public string OnionUrl { get; set; }
|
||||
public string OnionHost { get; set; }
|
||||
public int VirtualPort { get; set; }
|
||||
}
|
||||
|
||||
public enum TorServiceType
|
||||
{
|
||||
BTCPayServer,
|
||||
Other
|
||||
}
|
||||
}
|
||||
|
100
BTCPayServer/Services/Torrc.cs
Normal file
100
BTCPayServer/Services/Torrc.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Torrc
|
||||
{
|
||||
public static bool TryParse(string str, out Torrc value)
|
||||
{
|
||||
value = null;
|
||||
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
|
||||
var lines = str.Split(new char[] { '\n' });
|
||||
HiddenServiceDir currentDirectory = null;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (HiddenServiceDir.TryParse(line, out var dir))
|
||||
{
|
||||
serviceDirectories.Add(dir);
|
||||
currentDirectory = dir;
|
||||
}
|
||||
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
|
||||
{
|
||||
currentDirectory.ServicePorts.Add(portDef);
|
||||
}
|
||||
}
|
||||
value = new Torrc() { ServiceDirectories = serviceDirectories };
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach(var serviceDir in ServiceDirectories)
|
||||
{
|
||||
builder.AppendLine(serviceDir.ToString());
|
||||
foreach (var port in serviceDir.ServicePorts)
|
||||
builder.AppendLine(port.ToString());
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class HiddenServiceDir
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
|
||||
{
|
||||
serviceDir = null;
|
||||
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
|
||||
return true;
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; set; }
|
||||
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServiceDir {DirectoryPath}";
|
||||
}
|
||||
}
|
||||
public class HiddenServicePortDefinition
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
|
||||
{
|
||||
portDefinition = null;
|
||||
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
|
||||
return false;
|
||||
var addressPort = parts[2].Trim().Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (addressPort.Length != 2)
|
||||
return false;
|
||||
if (!int.TryParse(addressPort[1].Trim(), out int port))
|
||||
return false;
|
||||
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
|
||||
return false;
|
||||
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
|
||||
return true;
|
||||
}
|
||||
public int VirtualPort { get; set; }
|
||||
public IPEndPoint Endpoint { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServicePort {VirtualPort} {Endpoint}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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,9 +41,15 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
.card-deck .card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-100">
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
{image}
|
||||
@ -102,7 +108,7 @@
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
@ -121,10 +127,10 @@
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr class="h5">
|
||||
<tr class="h5">
|
||||
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-0">
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group-prepend">
|
||||
@ -136,18 +142,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
|
||||
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
|
||||
var percentage = CustomTipPercentages[i];
|
||||
<div class="col">
|
||||
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0)
|
||||
{
|
||||
@for (int i = 0; i < CustomTipPercentages.Length; i++)
|
||||
{
|
||||
var percentage = CustomTipPercentages[i];
|
||||
<div class="col">
|
||||
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tr>}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
@ -159,54 +167,55 @@
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<body class="h-100">
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white border-0">
|
||||
<h5 class="modal-title">Confirmation</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
|
||||
<span aria-hidden="true">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table id="js-cart-summary" class="table m-0">
|
||||
<tbody class="my-3">
|
||||
<tr class="h5">
|
||||
<td colspan="2" class="border-top-0">Summary</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-0">Total products</td>
|
||||
<td align="right" class="border-0 pb-0">
|
||||
<span class="js-cart-summary-products text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr class="h5">
|
||||
<td colspan="2" class="border-top-0">Summary</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-0">Total products</td>
|
||||
<td align="right" class="border-0 pb-0">
|
||||
<span class="js-cart-summary-products text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-y">Discount</td>
|
||||
<td align="right" class="border-0 pb-y">
|
||||
<span class="js-cart-summary-discount text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
}
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr class="h6">
|
||||
<td class="border-top-0 pt-0">Tip</td>
|
||||
<td align="right" class="border-top-0 pt-0">
|
||||
<span class="js-cart-summary-tip text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="h3 table-light">
|
||||
<td>Total</td>
|
||||
<td align="right">
|
||||
<span class="js-cart-summary-total text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="h3 table-light">
|
||||
<td>Total</td>
|
||||
<td align="right">
|
||||
<span class="js-cart-summary-total text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -214,184 +223,186 @@
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
|
||||
<b>@Model.CustomButtonText</b>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- Page Content -->
|
||||
<div id="content">
|
||||
<div class="p-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||
</div>
|
||||
<div class="col-sm-8 col-lg-9 mb-2">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<!-- Page Content -->
|
||||
<div id="content">
|
||||
<div class="p-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||
<a class="js-cart btn btn-warning text-white text-right" href="#">
|
||||
<i class="fa fa-shopping-basket"></i>
|
||||
<span class="badge badge-light badge-pill">
|
||||
<span id="js-cart-items">0</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row">
|
||||
|
||||
|
||||
@{
|
||||
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rowsJ; x++)
|
||||
{
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="col-sm-8 col-lg-9 mb-2">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute; right: 0px; z-index: 1049; display: none;">
|
||||
<i class="fa fa-times-circle fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="bg-dark">
|
||||
<div class="bg-warning p-3 clearfix">
|
||||
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
|
||||
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||
</div>
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
|
||||
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th colspan="3" width="55%">Product</th>
|
||||
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
|
||||
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
|
||||
|
||||
<div class="text-center mb-5 pb-5">
|
||||
<img src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
} else {
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
@{
|
||||
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rows; x++)
|
||||
{
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var item = Model.Items[index];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
|
||||
<div class="js-add-cart card my-2 card-wrapper" data-id="@index">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
@:<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer pt-0 bg-transparent border-0">
|
||||
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="bg-dark">
|
||||
<div class="bg-warning p-3 clearfix">
|
||||
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#">
|
||||
<i class="fa fa-times fa-lg"></i>
|
||||
</a>
|
||||
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||
</div>
|
||||
|
||||
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th colspan="3" width="55%">Product</th>
|
||||
<th class="text-center" width="20%">
|
||||
<div style="width: 84px">Quantity</div>
|
||||
</th>
|
||||
<th class="text-right" width="25%">
|
||||
<div style="min-width: 50px">Price</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit">
|
||||
<b>Confirm</b>
|
||||
</button>
|
||||
|
||||
<div class="text-center mb-5 pb-5">
|
||||
<img src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int x = 0; x < Model.Items.Length; x++)
|
||||
{
|
||||
var item = Model.Items[x];
|
||||
|
||||
<div class="card my-2" data-id="@x">
|
||||
@if (!String.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body pb-0">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(item.Description))
|
||||
{
|
||||
<p class="card-text">@item.Description</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
@if (item.Custom)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id"/>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="card my-2">
|
||||
<div class="card-body my-auto pb-0">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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>
|
||||
|
@ -79,13 +79,13 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.TorServices.Length != 0)
|
||||
@if (Model.TorHttpServices.Count != 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4>TOR hidden services</h4>
|
||||
<h4>HTTP TOR hidden services</h4>
|
||||
<div class="form-group">
|
||||
<span>TOR services hosted on this server</span>
|
||||
<span>TOR services hosted on this server, only http servers are listed here.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
@ -96,12 +96,12 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in Model.TorServices)
|
||||
@foreach (var s in Model.TorHttpServices)
|
||||
{
|
||||
<tr>
|
||||
<td>@s.Name</td>
|
||||
<td style="text-align:right">
|
||||
<a href="@s.OnionUrl" target="_blank">See information</a>
|
||||
<a href="@s.Link" target="_blank">See information</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -112,6 +112,37 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.TorOtherServices.Count != 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4>Other TOR hidden services</h4>
|
||||
<div class="form-group">
|
||||
<span>TOR services hosted on this server which are not http based.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in Model.TorOtherServices)
|
||||
{
|
||||
<tr>
|
||||
<td>@s.Name</td>
|
||||
<td style="text-align:right">@s.Link</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
@ -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">
|
||||
@ -55,6 +55,14 @@
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.NetworkType.ToString()</span>
|
||||
}
|
||||
</a>
|
||||
@if (env.OnionUrl != null)
|
||||
{
|
||||
<span>
|
||||
<a href="@env.OnionUrl" target="_blank">
|
||||
<img src="~/img/icons/onion.svg" width="26" height="32" />
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@ -77,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>
|
||||
@ -91,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(".");
|
||||
}
|
||||
|
1
BTCPayServer/wwwroot/img/icons/onion.svg
Normal file
1
BTCPayServer/wwwroot/img/icons/onion.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 228.46 342.85"><defs><style>.cls-1{fill:#fefefe;}.cls-2{fill:#111316;}.cls-3{fill:#868686;}.cls-4{fill:#b1b1b1;}.cls-5{fill:#454545;}.cls-6{fill:#909090;}.cls-7{fill:#7a7a7a;}.cls-8{fill:#9e9e9e;}.cls-9{fill:#555;}.cls-10{fill:#5f5f5f;}.cls-11{fill:#878787;}.cls-12{fill:#5e5e5e;}</style></defs><title>Artboard 1</title><path class="cls-1" d="M174.18,1.15c-12.79,15.54-25.44,31.17-35,49a.81.81,0,0,0-.13,1.16c.37.29.62-.11.91-.34,14.89-18.65,34.39-30.35,56.88-37.71C168.44,39.75,143.05,68.71,121.8,102l13.66,2.14c-3.72,20.24,6.33,32.87,22.43,42.15,13.7,7.89,27.63,15.55,38.79,27,26.69,27.29,36.54,59.88,26.11,96.68S186.55,329,149.45,337c-29.9,6.44-60.06,6.49-88.73-5.95C31.47,318.33,11,297,4.13,265.19-4,227.54,6.39,195.42,35.89,170.07c12.79-11,27-20.09,41.32-28.79,15.76-9.57,16.88-22.58,13.4-36.47C89,98.35,86.13,92.39,83.17,86.64c.6-1.64,1.94-2,3-2.93l18.37,12.14C108.18,69.64,117.17,46.14,130.1,24l-7.71,30.83c4.68-6.78,9-13.78,14.15-20.21a172,172,0,0,1,17-18.29A193.73,193.73,0,0,1,173.11.09C174.12-.2,174.4.23,174.18,1.15Z"/><path class="cls-2" d="M105.39,142.67c-.54,2.26-.13,4.81-2,6.65-1.33,4.1-2.7,8.19-4,12.3-2.94,9.35-8.78,16-17.44,20.79a159.8,159.8,0,0,0-26.84,19c-12.21,10.49-20.24,23.62-24.85,39-2.89,9.63-.69,18.88,2.31,27.89,6.38,19.22,17,36,30.72,50.82,3.3,3.57,7.49,6,12.45,9.21a56.79,56.79,0,0,1-15.69-6c-26.8-14.88-45.19-36.45-49.11-67.45-3.61-28.6,3.69-54.2,26.61-73.69,10.91-9.27,21.8-18.54,34.25-25.77a86.33,86.33,0,0,0,17.76-13.65c10.31-10.3,11.58-22.34,7.13-35.46-1.58-4.66-3.8-9.1-6.16-14.64l19.65,8.58c1.5,2,1.45,4.36,1.45,6.7q0,65.25,0,130.49c0,3.35.14,6.77-3.22,8.92-1.79,3.64-4.57,6.52-7.27,9.44-11,11.89-13.48,26.43-10.71,41.39,2.42,13.05,7.1,25.75,19.42,33.65-12.07-2.3-23.29-6-31.89-15.45-9-9.84-15.93-20.73-20-33.49a35.89,35.89,0,0,1,2.74-29.35,67.11,67.11,0,0,1,25.28-26.26c8.75-5.15,15.45-12,18.34-22,.32-2,.84-4,1.31-6-.1-.69-1,0-.33-.12.29-.05.41.07.33.35-.48,1.72-.7,3.56-2.17,4.83-4,10-10.86,17.15-20.4,22.28-14,7.5-25.18,18.15-28.63,34.09-4.9,22.61,2.2,42,19.08,57.72,6.08,5.67,13.15,9.93,20.62,13.62-8.59-1.5-17.33-3.21-23.57-9.53-17-17.27-30.29-36.88-36.6-60.76C31.33,251,32.67,242,36.44,233a78,78,0,0,1,25.86-33c7.61-5.64,15.25-11.32,23.32-16.25,8.86-5.42,13.72-13.26,16.29-22.9.94-3.52,1.6-7.12,2.39-10.69.38-2.54.46-5.14,1.22-6.84"/><path class="cls-2" d="M115.56,151.29c0,5.4,1.52,10.67,1.43,16.08.34,5.26,2.4,9.88,4.89,14.45,7.68,14.11,14.61,28.47,18.4,44.31,5.06,21.13,2.58,41.88-1.53,62.66-1.29,6.49-2.28,13.07-4.72,19.28-1.91,0-1.21-1.16-1-2.12a211.41,211.41,0,0,0,4.22-30.08c1-19-4.07-36.53-13.21-52.88-4.44-8-8.47-15.77-8.49-25.11-1.24,8.49-.75,16.8,4.4,24,6.83,9.59,10.07,20.46,12.23,31.77,2.86,15,.79,29.79-1.72,44.59-1.84,10.82-5.17,20.95-13.32,28.85-.88.19-1.35,0-1-1,4-5.93,5.09-12.71,6.18-19.64,3.21-20.33.45-39.93-6.28-59.15-1.41-1.95-1.74-4.33-2.56-6.53a20.81,20.81,0,0,1,1.51,8.57c2.8,13.37,5,26.75,4.32,40.54-.54,11.72-2.26,23.12-6.2,34.17-3.21-1.21-2.25-4.06-2.26-6.31-.1-21-.31-42,0-62.94.72-49.27.15-98.55.39-147.82,0-2-.49-4,1-5.73a16.56,16.56,0,0,1,6.28,2.7c.81,5.24-.18,10.37-.34,15.5-.38,11.8,1.83,22.91,8.79,32.6,23.31,32.48,25.59,69.38,22.73,107.35a270.61,270.61,0,0,1-5.5,36.45,43.13,43.13,0,0,1-7,16.32.78.78,0,0,1-1.28,0c6-19.91,11.48-39.82,10.66-61-.77-20.13-5.41-38.94-14.9-56.75-4.51-8.45-10.14-16.38-12.92-25.71-.33-1.11-.52-2.26-.77-3.4-2.34-4.74-1.76-10-2.63-15-.92-4.13-.55-8.37-.89-12.55"/><path class="cls-2" d="M109.16,325.19c-15-15.28-24.62-43.24-7.89-65.34,3.17-4.18,6.84-8.2,7.79-13.7,2.54.21,2.07,2.24,2.07,3.74q.11,35.73,0,71.46C111.13,322.85,111.64,324.86,109.16,325.19Z"/><path class="cls-3" d="M109.16,325.19c1.44-.63,1-1.94,1-3q0-36.53,0-73.06c0-1.07.42-2.41-1.1-3l-.33.17-.36,0a24.68,24.68,0,0,0,1.86-10.15q-.12-68-.08-136l2.1,1q0,109.61,0,219.21c0,1.21-.67,2.7.86,3.59C112.42,326.62,111,326.7,109.16,325.19Z"/><path class="cls-4" d="M117,167.37c-1.63-5.25-1.87-10.68-2-16.12-.29-1,.11-1.27,1-1l1.92,15Z"/><path class="cls-5" d="M133,306c.39.68,0,1.73,1,2.12-1.4,4-2.43,8.17-5.91,11l.09.08Q130.61,312.56,133,306Z"/><path class="cls-6" d="M115,249.33c-1.36-3-1.61-6.19-1.77-10.22a20,20,0,0,1,2.82,8.18Z"/><path class="cls-7" d="M103.36,149.32c.16-2.64,1.35-4.95,2.66-8.31-.2,3.83-.25,6.64-1.72,9.21Z"/><path class="cls-8" d="M103.41,203.3l2.5-7.07a10.65,10.65,0,0,1-1.64,8Z"/><path class="cls-9" d="M135.91,312.14l1.28,0a7.25,7.25,0,0,1-2.71,3.84A4.33,4.33,0,0,1,135.91,312.14Z"/><path class="cls-10" d="M116.11,326.08l1,1a3.76,3.76,0,0,1-3,2.28C113.9,327.66,115.06,326.9,116.11,326.08Z"/><path class="cls-11" d="M127.18,321.13c.35,1.34-.49,2.07-1.67,2.95,0-1.49.12-2.6,1.7-2.92Z"/><path class="cls-5" d="M127.21,321.16c0-.84-.12-1.71,1-2l-.09-.08c.29,1,.23,1.76-.94,2Z"/><path class="cls-12" d="M116.06,150.27l-1,1c-1.91-4.67-.52-9.42-.2-14.16Q115.44,143.67,116.06,150.27Z"/></svg>
|
After Width: | Height: | Size: 4.7 KiB |
@ -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,11 +1,11 @@
|
||||
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
|
||||
RUN dotnet restore
|
||||
COPY BTCPayServer/. .
|
||||
RUN dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM microsoft/dotnet:2.1.6-aspnetcore-runtime-alpine3.7
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-alpine3.7
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
@ -14,8 +14,6 @@ ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
WORKDIR /app
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
RUN mkdir /datadir
|
||||
ENV BTCPAY_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
@ -1,5 +1,8 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM microsoft/dotnet:2.1.500-sdk AS builder
|
||||
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
|
||||
@ -7,7 +10,10 @@ COPY BTCPayServer/. .
|
||||
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 microsoft/dotnet:2.1.6-aspnetcore-runtime-stretch-slim-arm32v7
|
||||
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