Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
a9722df7e4 | |||
474be6f7be | |||
ada9a7264b | |||
e991b302d0 | |||
eef301c6ec | |||
3fdfd0adfd | |||
358f1ffc43 | |||
349c3409df | |||
0263a2950c | |||
0364a57cae | |||
e232dd7d7e | |||
fee936b569 | |||
420115c54d | |||
312e961098 | |||
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 | |||
1ef59e05a5 | |||
5e09992637 | |||
30448233b1 | |||
a1601a17aa | |||
7522f7d0f7 | |||
d04b9c4c09 | |||
1a24ff9a49 | |||
13d72de82d | |||
3728fdab3f | |||
2317e3d50c | |||
5f15976c02 | |||
7f592639c5 | |||
a98402af12 | |||
316ffa91d1 | |||
c24953b57e | |||
7a1b1b7e5e | |||
70f71f64c4 | |||
5bccd07d7d | |||
d818baa6d1 | |||
249b8abf03 | |||
c134277514 | |||
f5d366cf7f | |||
ad25a2ed08 | |||
1e7a2ffe97 | |||
dd52075ff1 | |||
0253e42bd5 | |||
d99774f8d9 | |||
d563a2ec89 | |||
b4b4523193 | |||
fbcb69f447 | |||
8ae5a9c1f7 | |||
3ef5bfb6eb | |||
4016ded584 | |||
5b0b4adb1c | |||
f1ec3b0c75 | |||
b5d55a2066 | |||
0d2c9fe377 | |||
2c7cc9a796 |
@ -177,6 +177,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
|
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PaymentRequestTests
|
||||
{
|
||||
public PaymentRequestTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateViewUpdateAndDeletePaymentRequest()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var id = (Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||
|
||||
|
||||
|
||||
//permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(id));
|
||||
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||
|
||||
//Delete
|
||||
|
||||
Assert.IsType<ConfirmModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||
|
||||
Assert
|
||||
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(1, invoice.Price);
|
||||
|
||||
request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice with expiry",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
|
||||
response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
Assert
|
||||
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,9 @@ using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -1997,31 +2000,48 @@ donation:
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.000000012m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
Currency = "USD",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.000000019m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
Currency = "USD"
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.000000012m, invoice1.Price);
|
||||
Assert.Equal(0.000000019m, invoice2.Price);
|
||||
|
||||
// Should round up to 1 because 0.000000019 is unsignificant
|
||||
var invoice3 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.000000019m,
|
||||
Currency = "USD",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.00000001m, invoice1.Price);
|
||||
Assert.Equal(0.00000002m, invoice2.Price);
|
||||
Assert.Equal(1m, invoice3.Price);
|
||||
|
||||
// Should not round up at 8 digit because the 9th is insignificant
|
||||
var invoice4 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.000000019m,
|
||||
Currency = "BTC",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(1.00000002m, invoice4.Price);
|
||||
|
||||
// But not if the 9th is insignificant
|
||||
invoice4 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.000000019m,
|
||||
Currency = "BTC",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.000000019m, invoice4.Price);
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = -0.1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.0m, invoice.Price);
|
||||
@ -2315,6 +2335,62 @@ donation:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CanExpandExternalConnectionString()
|
||||
{
|
||||
var unusedUri = new Uri("https://toto.com");
|
||||
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
|
||||
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
|
||||
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
|
||||
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge));
|
||||
|
||||
// Make sure absolute paths are not expanded
|
||||
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));
|
||||
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest));
|
||||
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
|
||||
|
||||
var macaroonDirectory = CreateDirectory();
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
|
||||
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC);
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
|
||||
Assert.NotNull(expanded.Macaroons);
|
||||
Assert.Null(expanded.MacaroonFilePath);
|
||||
Assert.Null(expanded.Macaroons.AdminMacaroon);
|
||||
Assert.Null(expanded.Macaroons.InvoiceMacaroon);
|
||||
Assert.Null(expanded.Macaroons.ReadonlyMacaroon);
|
||||
|
||||
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
|
||||
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
|
||||
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
|
||||
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
|
||||
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
|
||||
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
|
||||
Assert.Equal(0xab, expanded.Macaroons.InvoiceMacaroon.Bytes[0]);
|
||||
Assert.NotNull(expanded.Macaroons.ReadonlyMacaroon);
|
||||
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
|
||||
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
|
||||
Assert.Equal("apitoken", expanded.APIToken);
|
||||
}
|
||||
|
||||
private string CreateDirectory([CallerMemberName] string caller = null)
|
||||
{
|
||||
var name = $"{caller}-{NBitcoin.RandomUtils.GetUInt32()}";
|
||||
Directory.CreateDirectory(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CheckRatesProvider()
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
image: nicolasdorier/nbxplorer:2.0.0.15
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -119,7 +119,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -165,7 +165,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.57</Version>
|
||||
<Version>1.0.3.78</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -33,7 +33,7 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.10" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
@ -45,10 +45,10 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.84" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.4" />
|
||||
<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" />
|
||||
@ -139,7 +139,7 @@
|
||||
<Content Update="Views\Server\LightningChargeServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SparkServices.cshtml">
|
||||
<Content Update="Views\Server\LightningWalletServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
|
@ -15,7 +15,6 @@ using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
@ -109,67 +108,26 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
void externalLnd<T>(string code, string lndType)
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>(code, string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
var instanceType = typeof(T);
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
|
||||
}
|
||||
};
|
||||
|
||||
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
|
||||
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
|
||||
|
||||
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
|
||||
if (spark.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(spark, out var connectionString))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
|
||||
}
|
||||
|
||||
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
|
||||
if (charge.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
|
||||
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
|
||||
|
||||
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
chargeError ?? string.Empty);
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
|
||||
}
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
@ -183,14 +141,14 @@ namespace BTCPayServer.Configuration
|
||||
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
|
||||
Link: p.p.Substring(p.SeparatorIndex + 1))))
|
||||
{
|
||||
ExternalServices.AddOrReplace(service.Name, service.Link);
|
||||
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
|
||||
OtherExternalServices.AddOrReplace(service.Name, uri);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -250,7 +208,6 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
var settings = new SSHSettings();
|
||||
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
|
||||
if (settings.Server != null)
|
||||
@ -277,12 +234,6 @@ namespace BTCPayServer.Configuration
|
||||
settings.Username = "root";
|
||||
}
|
||||
}
|
||||
else if (externalUrl != null)
|
||||
{
|
||||
settings.Port = 22;
|
||||
settings.Username = "root";
|
||||
settings.Server = externalUrl.DnsSafeHost;
|
||||
}
|
||||
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
|
||||
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
|
||||
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
|
||||
@ -296,9 +247,9 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -311,11 +262,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
@ -327,14 +273,5 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalCharge : ExternalService
|
||||
{
|
||||
public ExternalCharge(LightningConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
public LightningConnectionString ConnectionString { get; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public abstract class ExternalLnd : ExternalService
|
||||
{
|
||||
public ExternalLnd(LightningConnectionString connectionString, string type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalServices : MultiValueDictionary<string, ExternalService>
|
||||
{
|
||||
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
|
||||
{
|
||||
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
|
||||
return Array.Empty<T>();
|
||||
return services.OfType<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalSpark : ExternalService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalSpark(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
}
|
||||
}
|
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class SparkConnectionString
|
||||
{
|
||||
public Uri Server { get; private set; }
|
||||
public string CookeFile { get; private set; }
|
||||
|
||||
public static bool TryParse(string str, out SparkConnectionString result)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
result = null;
|
||||
var resultTemp = new SparkConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
return false;
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
|
||||
return false;
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookeFile != null)
|
||||
return false;
|
||||
resultTemp.CookeFile = kv[1];
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -55,12 +55,16 @@ namespace BTCPayServer.Controllers
|
||||
" custom: true";
|
||||
EnableShoppingCart = false;
|
||||
ShowCustomAmount = true;
|
||||
ShowDiscount = true;
|
||||
EnableTips = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
|
||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||
@ -89,6 +93,8 @@ namespace BTCPayServer.Controllers
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template,
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
@ -160,6 +166,8 @@ namespace BTCPayServer.Controllers
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
|
@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
@ -62,6 +62,8 @@ namespace BTCPayServer.Controllers
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
|
||||
@ -129,10 +131,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
if (!settings.Enabled && !isAdmin) {
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
@ -174,6 +174,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
|
@ -70,6 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
@ -102,18 +103,19 @@ namespace BTCPayServer.Controllers
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
|
||||
if (invoice.TaxIncluded is decimal taxIncluded)
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
|
||||
invoice.TaxIncluded = Math.Round(taxIncluded, currencyInfo.CurrencyDecimalDigits);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
int divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
|
||||
divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
|
||||
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
return macaroons;
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
|
321
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
321
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("payment-requests")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public class PaymentRequestController : Controller
|
||||
{
|
||||
private readonly InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
|
||||
public PaymentRequestController(
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
{
|
||||
UserId = GetUserId(), Skip = skip, Count = count
|
||||
});
|
||||
return View(new ListPaymentRequestsViewModel()
|
||||
{
|
||||
Skip = skip,
|
||||
StatusMessage = statusMessage,
|
||||
Count = count,
|
||||
Total = result.Total,
|
||||
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
SelectList stores = null;
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
if (!stores.Any())
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a payment request"
|
||||
});
|
||||
}
|
||||
|
||||
return View(new UpdatePaymentRequestViewModel(data)
|
||||
{
|
||||
Stores = stores,
|
||||
StatusMessage = statusMessage
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(viewModel.Currency) ||
|
||||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
|
||||
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
|
||||
nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
data = new PaymentRequestData();
|
||||
}
|
||||
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||
blob.Currency = viewModel.Currency;
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
|
||||
data.SetBlob(blob);
|
||||
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||
_EventAggregator.Publish(new PaymentRequestUpdated()
|
||||
{
|
||||
Data = data,
|
||||
PaymentRequestId = data.Id
|
||||
});
|
||||
|
||||
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
|
||||
{
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blob = data.GetBlob();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
|
||||
if (result)
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new {StatusMessage = "Payment request successfully removed"});
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage =
|
||||
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has already been settled.");
|
||||
}
|
||||
|
||||
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has expired");
|
||||
}
|
||||
|
||||
var statusesAllowedToDisplay = new List<InvoiceStatus>()
|
||||
{
|
||||
InvoiceStatus.New
|
||||
};
|
||||
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
|
||||
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
|
||||
statusesAllowedToDisplay.Contains(status));
|
||||
|
||||
if (validInvoice != null)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
|
||||
}
|
||||
|
||||
return Ok(validInvoice.Id);
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
{
|
||||
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
|
||||
}
|
||||
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
|
||||
bool redirectToInvoice,
|
||||
ViewPaymentRequestViewModel result,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
|
||||
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.GetValueOrDefault(result.AmountDue),
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
|
||||
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
|
||||
}
|
||||
|
||||
return Ok(newInvoiceId);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -384,18 +384,17 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (IsAdmin(roles))
|
||||
{
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||
"Delete"));
|
||||
@ -454,52 +453,18 @@ namespace BTCPayServer.Controllers
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
result.ExternalServices = _Options.ExternalServices;
|
||||
foreach (var externalService in _Options.OtherExternalServices)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
Action = nameof(LndServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
i = 0;
|
||||
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Spark server",
|
||||
Action = nameof(SparkServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Lightning charge server",
|
||||
Action = nameof(LightningChargeServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
foreach (var externalService in _Options.ExternalServices)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (_Options.SSHSettings != null)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
@ -508,134 +473,108 @@ namespace BTCPayServer.Controllers
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (lightningCharge == null)
|
||||
{
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
switch (service.Type)
|
||||
{
|
||||
case ExternalServiceTypes.Charge:
|
||||
return LightningChargeServices(service, connectionString, showQR);
|
||||
case ExternalServiceTypes.RTL:
|
||||
case ExternalServiceTypes.Spark:
|
||||
if (connectionString.AccessKey == null)
|
||||
{
|
||||
StatusMessage = $"Error: The access key of the service is not set";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||
{
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
|
||||
vm.APIToken = lightningCharge.Password;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
|
||||
{
|
||||
if (lightningCharge.CookieFilePath != "fake")
|
||||
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
|
||||
else
|
||||
vm.APIToken = "fake";
|
||||
}
|
||||
var builder = new UriBuilder(lightningCharge.ToUri(false));
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||
vm.APIToken = connectionString.APIToken;
|
||||
var builder = new UriBuilder(connectionString.Server);
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
[Route("server/services/spark/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (spark == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
SparkServicesViewModel vm = new SparkServicesViewModel();
|
||||
vm.ShowQR = showQR;
|
||||
try
|
||||
{
|
||||
var cookie = (spark.CookeFile == "fake"
|
||||
? "fake:fake:fake" // If we are testing, it should not crash
|
||||
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
|
||||
model.SSL = connectionString.Server.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
model.Uri = external.BaseUri.AbsoluteUri;
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (external.CertificateThumbprint != null)
|
||||
if (connectionString.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
model.CertificateThumbprint = connectionString.CertificateThumbprint;
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
if (connectionString.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
}
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCodeLink = Url.Action(nameof(GetLNDConfig), new { configKey = configKey });
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return View(nameof(LndServices), model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
@ -648,68 +587,62 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
|
||||
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
|
||||
ExternalConnectionString connectionString = null;
|
||||
try
|
||||
{
|
||||
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = external.BaseUri.DnsSafeHost;
|
||||
grpcConf.Port = external.BaseUri.Port;
|
||||
grpcConf.SSL = external.BaseUri.Scheme == "https";
|
||||
grpcConf.Host = connectionString.Server.DnsSafeHost;
|
||||
grpcConf.Port = connectionString.Server.Port;
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = external.BaseUri.AbsoluteUri;
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(external.ConnectionType.ToString());
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
|
||||
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
|
||||
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
@ -724,15 +657,24 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
|
||||
var server = 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}@{settings.Server}{port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
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()
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@ -54,6 +55,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PaymentRequests)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasIndex(o => o.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -41,6 +42,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
|
@ -61,6 +61,23 @@ namespace BTCPayServer
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
|
||||
{
|
||||
if (value != 0m)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
@ -108,6 +125,12 @@ namespace BTCPayServer
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
public static string WithStartingSlash(this string str)
|
||||
{
|
||||
if (str.StartsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return $"/{str}";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
@ -211,6 +234,31 @@ namespace BTCPayServer
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will return an absolute URL.
|
||||
/// If `relativeOrAsbolute` is absolute, returns it.
|
||||
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="relativeOrAbsolte"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
|
||||
{
|
||||
if (relativeOrAbsolute == null)
|
||||
{
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()), UriKind.Absolute);
|
||||
}
|
||||
if (relativeOrAbsolute.IsAbsoluteUri)
|
||||
return relativeOrAbsolute;
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -298,5 +346,15 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static string TrimEnd(this string input, string suffixToRemove,
|
||||
StringComparison comparisonType) {
|
||||
|
||||
if (input != null && suffixToRemove != null
|
||||
&& input.EndsWith(suffixToRemove, comparisonType)) {
|
||||
return input.Substring(0, input.Length - suffixToRemove.Length);
|
||||
}
|
||||
else return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ namespace BTCPayServer.HostedServices
|
||||
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
public virtual Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions = new List<IEventAggregatorSubscription>();
|
||||
SubscibeToEvents();
|
||||
@ -70,7 +70,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
Task _ProcessingEvents = Task.CompletedTask;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public virtual async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
||||
_Cts?.Cancel();
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
@ -38,9 +38,11 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
@ -74,6 +76,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
@ -150,6 +153,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<PaymentRequestRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
@ -184,7 +188,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
||||
@ -203,6 +207,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
services.AddTransient<AppsPublicController>();
|
||||
services.AddTransient<PaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
// bundling
|
||||
|
@ -147,59 +147,33 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
|
||||
}
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
|
@ -34,6 +34,7 @@ using Microsoft.Extensions.Options;
|
||||
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;
|
||||
@ -165,6 +166,7 @@ namespace BTCPayServer.Hosting
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
|
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
@ -0,0 +1,606 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20190121133309_AddPaymentRequests")]
|
||||
partial class AddPaymentRequests
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<int>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PaymentRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PaymentRequests")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class AddPaymentRequests : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PaymentRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(nullable: false),
|
||||
StoreDataId = table.Column<string>(nullable: true),
|
||||
Status = table.Column<int>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PaymentRequests_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_Status",
|
||||
table: "PaymentRequests",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_StoreDataId",
|
||||
table: "PaymentRequests",
|
||||
column: "StoreDataId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PaymentRequests");
|
||||
}
|
||||
}
|
||||
}
|
@ -328,6 +328,26 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<int>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PaymentRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -528,6 +548,14 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PaymentRequests")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
|
@ -17,6 +17,10 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
[Display(Name = "User can input custom amount")]
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
[Display(Name = "User can input discount in %")]
|
||||
public bool ShowDiscount { get; set; }
|
||||
[Display(Name = "Enable tips")]
|
||||
public bool EnableTips { get; set; }
|
||||
public string Example1 { get; internal set; }
|
||||
public string Example2 { get; internal set; }
|
||||
public string ExampleCallback { get; internal set; }
|
||||
|
@ -55,6 +55,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
|
@ -36,6 +36,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Item[] Items { get; set; }
|
||||
|
@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
{
|
||||
public class ListPaymentRequestsViewModel
|
||||
{
|
||||
public int Skip { get; set; }
|
||||
public int Count { get; set; }
|
||||
|
||||
public List<ViewPaymentRequestViewModel> Items { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
public class UpdatePaymentRequestViewModel
|
||||
{
|
||||
public UpdatePaymentRequestViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public UpdatePaymentRequestViewModel(PaymentRequestData data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Id = data.Id;
|
||||
StoreId = data.StoreDataId;
|
||||
|
||||
var blob = data.GetBlob();
|
||||
Title = blob.Title;
|
||||
Amount = blob.Amount;
|
||||
Currency = blob.Currency;
|
||||
Description = blob.Description;
|
||||
ExpiryDate = blob.ExpiryDate;
|
||||
Email = blob.Email;
|
||||
CustomCSSLink = blob.CustomCSSLink;
|
||||
EmbeddedCSS = blob.EmbeddedCSS;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
[Required] public string StoreId { get; set; }
|
||||
[Required] public decimal Amount { get; set; }
|
||||
|
||||
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
[Display(Name = "Expiration Date")]
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
[Required] public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public SelectList Stores { get; set; }
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom bootstrap CSS file")]
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
}
|
||||
|
||||
public class ViewPaymentRequestViewModel
|
||||
{
|
||||
public ViewPaymentRequestViewModel(PaymentRequestData data)
|
||||
{
|
||||
Id = data.Id;
|
||||
var blob = data.GetBlob();
|
||||
Title = blob.Title;
|
||||
Amount = blob.Amount;
|
||||
Currency = blob.Currency;
|
||||
Description = blob.Description;
|
||||
ExpiryDate = blob.ExpiryDate;
|
||||
Email = blob.Email;
|
||||
EmbeddedCSS = blob.EmbeddedCSS;
|
||||
CustomCSSLink = blob.CustomCSSLink;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
switch (data.Status)
|
||||
{
|
||||
case PaymentRequestData.PaymentRequestStatus.Pending:
|
||||
Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending";
|
||||
IsPending = true;
|
||||
break;
|
||||
case PaymentRequestData.PaymentRequestStatus.Completed:
|
||||
Status = "Settled";
|
||||
break;
|
||||
case PaymentRequestData.PaymentRequestStatus.Expired:
|
||||
Status = "Expired";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string Status { get; set; }
|
||||
public bool IsPending { get; set; }
|
||||
|
||||
public decimal AmountCollected { get; set; }
|
||||
public decimal AmountDue { get; set; }
|
||||
public string AmountDueFormatted { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
public List<PaymentRequestInvoice> Invoices { get; set; } = new List<PaymentRequestInvoice>();
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public string AmountCollectedFormatted { get; set; }
|
||||
public string AmountFormatted { get; set; }
|
||||
public bool AnyPendingInvoice { get; set; }
|
||||
|
||||
public class PaymentRequestInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTime ExpiryDate { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string AmountFormatted { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
||||
public List<PaymentRequestInvoicePayment> Payments { get; set; }
|
||||
public string Currency { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentRequestInvoicePayment
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -18,8 +18,9 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[EmailAddress]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail
|
||||
{
|
||||
get; set;
|
||||
|
@ -5,9 +5,10 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class SparkServicesViewModel
|
||||
public class LightningWalletServices
|
||||
{
|
||||
public string SparkLink { get; set; }
|
||||
public string ServiceLink { get; set; }
|
||||
public bool ShowQR { get; set; }
|
||||
public string WalletName { get; internal set; }
|
||||
}
|
||||
}
|
@ -2,27 +2,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using BTCPayServer.Configuration;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class ServicesViewModel
|
||||
{
|
||||
public class LNDServiceViewModel
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string Type { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Action { get; internal set; }
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
public class OtherExternalService
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
|
||||
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
|
||||
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
|
||||
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
|
||||
}
|
||||
}
|
||||
|
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal file
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal file
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
public class PaymentRequestHub : Hub
|
||||
{
|
||||
private readonly PaymentRequestController _PaymentRequestController;
|
||||
public const string InvoiceCreated = "InvoiceCreated";
|
||||
public const string PaymentReceived = "PaymentReceived";
|
||||
public const string InfoUpdated = "InfoUpdated";
|
||||
public const string InvoiceError = "InvoiceError";
|
||||
|
||||
public PaymentRequestHub(PaymentRequestController paymentRequestController)
|
||||
{
|
||||
_PaymentRequestController = paymentRequestController;
|
||||
}
|
||||
|
||||
public async Task ListenToPaymentRequest(string paymentRequestId)
|
||||
{
|
||||
if (Context.Items.ContainsKey("pr-id"))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["pr-id"].ToString());
|
||||
Context.Items.Remove("pr-id");
|
||||
}
|
||||
|
||||
Context.Items.Add("pr-id", paymentRequestId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, paymentRequestId);
|
||||
}
|
||||
|
||||
|
||||
public async Task Pay(decimal? amount = null)
|
||||
{
|
||||
_PaymentRequestController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
var result =
|
||||
await _PaymentRequestController.PayPaymentRequest(Context.Items["pr-id"].ToString(), false, amount);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||
break;
|
||||
case ObjectResult objectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentRequestStreamer : EventHostedServiceBase
|
||||
{
|
||||
private readonly IHubContext<PaymentRequestHub> _HubContext;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
|
||||
|
||||
public PaymentRequestStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<PaymentRequestHub> hubContext,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService) : base(eventAggregator)
|
||||
{
|
||||
_HubContext = hubContext;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StartAsync(cancellationToken);
|
||||
_CheckingPendingPayments = CheckingPendingPayments(cancellationToken)
|
||||
.ContinueWith(_ => _CheckingPendingPayments = null, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
|
||||
var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
{
|
||||
Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending}
|
||||
}, cancellationToken);
|
||||
|
||||
Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run");
|
||||
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
Task _CheckingPendingPayments;
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StopAsync(cancellationToken);
|
||||
await (_CheckingPendingPayments ?? Task.CompletedTask);
|
||||
}
|
||||
|
||||
protected override void SubscibeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<PaymentRequestUpdated>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
|
||||
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.PaymentReceived,
|
||||
new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
});
|
||||
}
|
||||
|
||||
await InfoUpdated(paymentId);
|
||||
}
|
||||
}
|
||||
else if (evt is PaymentRequestUpdated updated)
|
||||
{
|
||||
await InfoUpdated(updated.PaymentRequestId);
|
||||
|
||||
var expiry = updated.Data.GetBlob().ExpiryDate;
|
||||
if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending &&
|
||||
expiry.HasValue)
|
||||
{
|
||||
QueueExpiryTask(
|
||||
updated.PaymentRequestId,
|
||||
expiry.Value,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueExpiryTask(string paymentRequestId, DateTime expiry, CancellationToken cancellationToken)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var delay = expiry - DateTime.Now;
|
||||
if (delay > TimeSpan.Zero)
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task InfoUpdated(string paymentRequestId)
|
||||
{
|
||||
var req = await _PaymentRequestService.GetPaymentRequest(paymentRequestId);
|
||||
if (req != null)
|
||||
{
|
||||
await _HubContext.Clients.Group(paymentRequestId)
|
||||
.SendCoreAsync(PaymentRequestHub.InfoUpdated, new object[] {req});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
137
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
137
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
public class PaymentRequestService
|
||||
{
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly AppService _AppService;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
|
||||
public PaymentRequestService(
|
||||
IHubContext<PaymentRequestHub> hubContext,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
AppService appService,
|
||||
CurrencyNameTable currencies)
|
||||
{
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_AppService = appService;
|
||||
_currencies = currencies;
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentRequestStateIfNeeded(string id)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
await UpdatePaymentRequestStateIfNeeded(pr);
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentRequestStateIfNeeded(PaymentRequestData pr)
|
||||
{
|
||||
var blob = pr.GetBlob();
|
||||
var currentStatus = pr.Status;
|
||||
if (blob.ExpiryDate.HasValue)
|
||||
{
|
||||
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
|
||||
currentStatus = PaymentRequestData.PaymentRequestStatus.Expired;
|
||||
}
|
||||
else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending)
|
||||
{
|
||||
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
if (amountCollected >= blob.Amount)
|
||||
{
|
||||
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatus != pr.Status)
|
||||
{
|
||||
pr.Status = currentStatus;
|
||||
await _PaymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
if (pr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blob = pr.GetBlob();
|
||||
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
|
||||
var amountDue = blob.Amount - amountCollected;
|
||||
|
||||
return new ViewPaymentRequestViewModel(pr)
|
||||
{
|
||||
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
|
||||
AmountCollected = amountCollected,
|
||||
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
|
||||
AmountDue = amountDue,
|
||||
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||
LastUpdated = DateTime.Now,
|
||||
AnyPendingInvoice = invoices.Any(entity => entity.Status == InvoiceStatus.New),
|
||||
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.ProductInformation.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||
Status = entity.GetInvoiceState().ToString(),
|
||||
Payments = entity.GetPayments().Select(paymentEntity =>
|
||||
{
|
||||
var paymentNetwork = _BtcPayNetworkProvider.GetNetwork(paymentEntity.GetCryptoCode());
|
||||
var paymentData = paymentEntity.GetCryptoPaymentData();
|
||||
string link = null;
|
||||
string txId = null;
|
||||
switch (paymentData)
|
||||
{
|
||||
case Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData:
|
||||
txId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
link = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink,
|
||||
txId);
|
||||
break;
|
||||
case LightningLikePaymentData lightningLikePaymentData:
|
||||
txId = lightningLikePaymentData.BOLT11;
|
||||
break;
|
||||
}
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment()
|
||||
{
|
||||
Amount = paymentData.GetValue(),
|
||||
PaymentMethod = paymentEntity.GetPaymentMethodId().ToString(),
|
||||
Link = link,
|
||||
Id = txId
|
||||
};
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -30,8 +30,8 @@
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice.InternalTags))
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
|
@ -140,6 +140,7 @@ namespace BTCPayServer.Services.Apps
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = perks,
|
||||
Enabled = settings.Enabled,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
@ -167,11 +168,9 @@ namespace BTCPayServer.Services.Apps
|
||||
|
||||
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
||||
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||
public static string[] GetAppInternalTags(IEnumerable<string> tags)
|
||||
public static string[] GetAppInternalTags(InvoiceEntity invoice)
|
||||
{
|
||||
return tags == null ? Array.Empty<string>() : tags
|
||||
.Where(t => t.StartsWith("APP#", StringComparison.InvariantCulture))
|
||||
.Select(t => t.Substring("APP#".Length)).ToArray();
|
||||
return invoice.GetInternalTags("APP#");
|
||||
}
|
||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
||||
{
|
||||
@ -346,7 +345,7 @@ namespace BTCPayServer.Services.Apps
|
||||
|
||||
// Else, we just sum the payments
|
||||
return payments
|
||||
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue()))
|
||||
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
|
||||
.ToArray();
|
||||
})
|
||||
.GroupBy(p => p.Key)
|
||||
|
@ -13,11 +13,10 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
public class BTCPayServerEnvironment
|
||||
{
|
||||
IHttpContextAccessor httpContext;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
|
||||
{
|
||||
ExpectedHost = httpContext.HttpContext.Request.Host.Value;
|
||||
ExpectedDomain = httpContext.HttpContext.Request.Host.Host;
|
||||
ExpectedProtocol = httpContext.HttpContext.Request.Scheme;
|
||||
this.httpContext = httpContext;
|
||||
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
|
||||
#if DEBUG
|
||||
Build = "Debug";
|
||||
@ -32,9 +31,9 @@ namespace BTCPayServer.Services
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ExpectedDomain { get; set; }
|
||||
public string ExpectedHost { get; set; }
|
||||
public string ExpectedProtocol { get; set; }
|
||||
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
|
||||
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
|
||||
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
|
||||
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
|
@ -114,7 +114,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
public class InvoiceEntity
|
||||
{
|
||||
public const int InternalTagSupport_Version = 1;
|
||||
public int Version { get; set; } = 1;
|
||||
public const int Lastest_Version = 1;
|
||||
public int Version { get; set; }
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
@ -168,6 +169,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
|
||||
|
||||
public string[] GetInternalTags(string suffix)
|
||||
{
|
||||
return InternalTags == null ? Array.Empty<string>() : InternalTags
|
||||
.Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture))
|
||||
.Select(t => t.Substring(suffix.Length)).ToArray();
|
||||
}
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
|
@ -35,6 +35,7 @@ namespace BTCPayServer.Services.Mails
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Enable SSL")]
|
||||
public bool EnableSSL
|
||||
{
|
||||
get; set;
|
||||
|
@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.PaymentRequests
|
||||
{
|
||||
public class PaymentRequestRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
if (string.IsNullOrEmpty(entity.Id))
|
||||
{
|
||||
await context.PaymentRequests.AddAsync(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.PaymentRequests.Update(entity);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||
.Where(data =>
|
||||
string.IsNullOrEmpty(userId) ||
|
||||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)))
|
||||
.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||
.AnyAsync(data =>
|
||||
data.Id == paymentRequestId &&
|
||||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
invoiceData.Status = status;
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
|
||||
if (!string.IsNullOrEmpty(query.StoreId))
|
||||
{
|
||||
queryable = queryable.Where(data =>
|
||||
data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
|
||||
}
|
||||
|
||||
if (query.Status != null && query.Status.Any())
|
||||
{
|
||||
queryable = queryable.Where(data =>
|
||||
query.Status.Contains(data.Status));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.UserId))
|
||||
{
|
||||
queryable = queryable.Where(i =>
|
||||
i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
|
||||
}
|
||||
|
||||
var total = await queryable.CountAsync(cancellationToken);
|
||||
|
||||
if (query.Skip.HasValue)
|
||||
{
|
||||
queryable = queryable.Skip(query.Skip.Value);
|
||||
}
|
||||
|
||||
if (query.Count.HasValue)
|
||||
{
|
||||
queryable = queryable.Take(query.Count.Value);
|
||||
}
|
||||
|
||||
return (total, await queryable.ToArrayAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePaymentRequest(string id, string userId)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var canDelete = !EnumerableExtensions.Any((await GetInvoicesForPaymentRequest(id)));
|
||||
if (!canDelete) return false;
|
||||
var pr = await FindPaymentRequest(id, userId);
|
||||
if (pr == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.PaymentRequests.Remove(pr);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity[]> GetInvoicesForPaymentRequest(string paymentRequestId,
|
||||
InvoiceQuery invoiceQuery = null)
|
||||
{
|
||||
if (invoiceQuery == null)
|
||||
{
|
||||
invoiceQuery = new InvoiceQuery();
|
||||
}
|
||||
|
||||
invoiceQuery.OrderId = new[] {GetOrderIdForPaymentRequest(paymentRequestId)};
|
||||
return await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
}
|
||||
|
||||
public static string GetOrderIdForPaymentRequest(string paymentRequestId)
|
||||
{
|
||||
return $"PAY_REQUEST_{paymentRequestId}";
|
||||
}
|
||||
|
||||
public static string GetPaymentRequestIdFromOrderId(string invoiceOrderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoiceOrderId) ||
|
||||
!invoiceOrderId.StartsWith("PAY_REQUEST_", StringComparison.InvariantCulture))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return invoiceOrderId.Replace("PAY_REQUEST_", "", StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string GetInternalTag(string id)
|
||||
{
|
||||
return $"PAYREQ#{id}";
|
||||
}
|
||||
public static string[] GetPaymentIdsFromInternalTags(InvoiceEntity invoiceEntity)
|
||||
{
|
||||
return invoiceEntity.GetInternalTags("PAYREQ#");
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentRequestUpdated
|
||||
{
|
||||
public string PaymentRequestId { get; set; }
|
||||
public PaymentRequestData Data { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentRequestQuery
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
|
||||
public string UserId { get; set; }
|
||||
public int? Skip { get; set; }
|
||||
public int? Count { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentRequestData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string StoreDataId { get; set; }
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public PaymentRequestStatus Status { get; set; }
|
||||
|
||||
public byte[] Blob { get; set; }
|
||||
|
||||
public PaymentRequestBlob GetBlob()
|
||||
{
|
||||
var result = Blob == null
|
||||
? new PaymentRequestBlob()
|
||||
: JObject.Parse(ZipUtils.Unzip(Blob)).ToObject<PaymentRequestBlob>();
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool SetBlob(PaymentRequestBlob blob)
|
||||
{
|
||||
var original = new Serializer(Network.Main).ToString(GetBlob());
|
||||
var newBlob = new Serializer(Network.Main).ToString(blob);
|
||||
if (original == newBlob)
|
||||
return false;
|
||||
Blob = ZipUtils.Zip(newBlob);
|
||||
return true;
|
||||
}
|
||||
|
||||
public class PaymentRequestBlob
|
||||
{
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
}
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Expired = 2
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public ExchangeRates Latest;
|
||||
public DateTimeOffset NextRefresh;
|
||||
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
|
||||
public DateTimeOffset Expiration;
|
||||
public Exception Exception;
|
||||
public string ExchangeName;
|
||||
@ -90,6 +91,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
public bool DoNotAutoFetchIfExpired { get; set; }
|
||||
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
|
||||
|
||||
public async Task<LatestFetch> UpdateIfNecessary()
|
||||
{
|
||||
@ -142,12 +144,15 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
fetch.Latest = previous.Latest;
|
||||
fetch.Expiration = previous.Expiration;
|
||||
fetch.Backoff = previous.Backoff * 2;
|
||||
if (fetch.Backoff > MaxBackoff)
|
||||
fetch.Backoff = MaxBackoff;
|
||||
}
|
||||
else
|
||||
{
|
||||
fetch.Expiration = DateTimeOffset.UtcNow;
|
||||
}
|
||||
fetch.NextRefresh = DateTimeOffset.UtcNow;
|
||||
fetch.NextRefresh = DateTimeOffset.UtcNow + fetch.Backoff;
|
||||
fetch.Exception = ex;
|
||||
}
|
||||
_Latest = fetch;
|
||||
|
@ -130,19 +130,7 @@ namespace BTCPayServer.Services.Rates
|
||||
var provider = GetNumberFormatInfo(currency, true);
|
||||
var currencyData = GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
if (value != 0m)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
}
|
||||
value = value.RoundToSignificant(ref divisibility);
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
|
@ -1,7 +1,7 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@model ListAppsViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Stores";
|
||||
ViewData["Title"] = "Apps";
|
||||
}
|
||||
|
||||
<section>
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer">PRS
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
|
@ -57,6 +57,14 @@
|
||||
<label asp-for="ShowCustomAmount"></label>
|
||||
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ShowDiscount"></label>
|
||||
<input asp-for="ShowDiscount" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EnableTips"></label>
|
||||
<input asp-for="EnableTips" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ButtonText" class="control-label"></label>*
|
||||
<input asp-for="ButtonText" class="form-control" />
|
||||
|
@ -1,4 +1,10 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
@if(!Model.Enabled)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert" style="text-align: center;">
|
||||
This crowdfund page is not publically viewable!
|
||||
</div>
|
||||
}
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
|
||||
|
@ -32,7 +32,7 @@
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
</style>
|
||||
}
|
||||
|
||||
</head>
|
||||
|
@ -93,6 +93,8 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
@ -106,9 +108,12 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr class="h5">
|
||||
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
|
||||
</tr>
|
||||
@ -135,6 +140,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
@ -170,18 +176,24 @@
|
||||
<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)
|
||||
{
|
||||
<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">
|
||||
|
@ -45,11 +45,13 @@
|
||||
<div class="single-item-order__right">
|
||||
@if (Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div class="payment__currencies cursorPointer" onclick="openPaymentMethodDialog()">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span class="clickable_underline">{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
<span class="clickable_indicator fa fa-angle-right"></span>
|
||||
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
|
||||
<span class="payment__currencies ">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
<span class="clickable_indicator fa fa-angle-right"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="vexPopupDialog">
|
||||
<ul class="vexmenu">
|
||||
@ -69,12 +71,11 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="payment__currencies">
|
||||
<div class="payment__currencies_noborder">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="payment__spinner">
|
||||
<partial name="Checkout-Spinner" />
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model InvoicesModel
|
||||
@{
|
||||
ViewData["Title"] = "Invoices";
|
||||
var rootUrl = Context.Request.GetAbsoluteRoot();
|
||||
}
|
||||
|
||||
@section HeadScripts {
|
||||
|
122
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
122
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
@ -0,0 +1,122 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit") Payment Request</h2>
|
||||
<hr class="primary">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="Id"/>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Title" class="control-label"></label>*
|
||||
<input asp-for="Title" class="form-control"/>
|
||||
<span asp-validation-for="Title" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Amount" class="control-label"></label>*
|
||||
<input type="number" step="any" asp-for="Amount" class="form-control"/>
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Currency" class="control-label"></label>*
|
||||
<input placeholder="BTC" asp-for="Currency" class="form-control"/>
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AllowCustomPaymentAmounts"></label>
|
||||
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<label asp-for="StoreId" class="control-label"></label>
|
||||
@if (string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" asp-for="StoreId" value="@Model.StoreId"/>
|
||||
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly/>
|
||||
}
|
||||
|
||||
<span asp-validation-for="StoreId" class="text-danger"></span>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="control-label"></label>
|
||||
<input type="email" asp-for="Email" class="form-control"></input>
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ExpiryDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="ExpiryDate" class="form-control datetime" min="today"/>
|
||||
<div class="input-group-append only-for-js">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="ExpiryDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Description" class="control-label"></label>
|
||||
<textarea asp-for="Description" class="form-control richtext"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="control-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank">
|
||||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control"/>
|
||||
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EmbeddedCSS" class="control-label"></label>
|
||||
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
@if (!string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id">View</a>
|
||||
<a class="btn btn-secondary"
|
||||
target="_blank"
|
||||
asp-action="ListInvoices"
|
||||
asp-controller="Invoice"
|
||||
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
|
||||
|
||||
}
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
|
||||
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.css"></bundle>
|
||||
}
|
88
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
88
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
@ -0,0 +1,88 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
|
||||
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">Payment Requests</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Expiry</th>
|
||||
<th class="text-right">Price</th>
|
||||
<th class="text-right">Status</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Title</td>
|
||||
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
|
||||
<td class="text-right">@item.Amount @item.Currency</td>
|
||||
<td class="text-right">@item.Status</td>
|
||||
<td class="text-right">
|
||||
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
|
||||
<span> - </span>
|
||||
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
|
||||
<span> - </span>
|
||||
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
|
||||
<span> - </span>
|
||||
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
|
||||
<span> - </span>
|
||||
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</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("GetPaymentRequests", new
|
||||
{
|
||||
skip = Math.Max(0, Model.Skip - Model.Count),
|
||||
count = Model.Count,
|
||||
})">Previous</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Count) of @Model.Total</span>
|
||||
</li>
|
||||
<li class="page-item @(Model.Total > (Model.Skip + Model.Count) ? null : "disabled")">
|
||||
<a class="page-link" href="@Url.Action("GetPaymentRequests", new
|
||||
{
|
||||
skip = Model.Skip + Model.Count,
|
||||
count = Model.Count,
|
||||
})">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal file
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal file
@ -0,0 +1,163 @@
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
|
||||
|
||||
<div class="container">
|
||||
<div class="row w-100 p-0 m-0" style="height: 100vh">
|
||||
<div class="mx-auto my-auto w-100">
|
||||
<div class="card">
|
||||
<h1 class="card-header">
|
||||
@Model.Title
|
||||
<span class="text-muted float-right text-center">@Model.Status</span>
|
||||
</h1>
|
||||
<div class="card-body px-0 pt-0">
|
||||
<div class="row mb-4">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6 ">
|
||||
<ul class="w-100 list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Request amount:</span>
|
||||
<span class="h2">@Model.AmountFormatted</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Paid so far:</span>
|
||||
<span class="h2">@Model.AmountCollectedFormatted</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Amount due:</span>
|
||||
<span class="h2">@Model.AmountDueFormatted</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="table-responsive">
|
||||
<table class="table border-top-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class=" border-top-0" scope="col">Invoice #</th>
|
||||
<th class=" border-top-0">Price</th>
|
||||
<th class=" border-top-0">Expiry</th>
|
||||
<th class=" border-top-0">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (Model.Invoices == null && !Model.Invoices.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No payments made yet</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr class="bg-light">
|
||||
<td scope="row">@invoice.Id</td>
|
||||
<td>@invoice.Amount @invoice.Currency</td>
|
||||
<td>@invoice.ExpiryDate.ToString("g")</td>
|
||||
<td>@invoice.Status</td>
|
||||
</tr>
|
||||
if (invoice.Payments != null && invoice.Payments.Any())
|
||||
{
|
||||
<tr class="bg-light">
|
||||
<td colspan="4" class=" px-2 py-1 border-top-0">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="p-1" style="max-width: 300px">Tx Id</th>
|
||||
<th class="p-1">Payment Method</th>
|
||||
<th class="p-1">Amount</th>
|
||||
<th class="p-1">Link</th>
|
||||
</tr>
|
||||
@foreach (var payment in invoice.Payments)
|
||||
{
|
||||
<tr class="d-flex">
|
||||
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
|
||||
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">@payment.Id</div>
|
||||
</td>
|
||||
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
|
||||
@payment.Id
|
||||
</td>
|
||||
<td class="p-1">@payment.PaymentMethod</td>
|
||||
<td class="p-1">@payment.Amount</td>
|
||||
<td class="p-1 d-print-none">
|
||||
@if (!string.IsNullOrEmpty(payment.Link))
|
||||
{
|
||||
<a :href="@payment.Link" target="_blank">Link</a>
|
||||
}
|
||||
</td>
|
||||
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
|
||||
@payment.Link
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (Model.IsPending)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
@if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice)
|
||||
{
|
||||
<form method="get" asp-action="PayPaymentRequest">
|
||||
|
||||
<div class="input-group m-auto" style="max-width: 250px">
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="amount"
|
||||
|
||||
value="@Model.AmountDue"
|
||||
max="@Model.AmountDue"
|
||||
step="any"
|
||||
placeholder="Amount"
|
||||
required>
|
||||
<div class="input-group-append">
|
||||
<span class='input-group-text'>@Model.Currency.ToUpper()</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit">
|
||||
Pay now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-primary btn-lg d-print-none" asp-action="PayPaymentRequest">
|
||||
Pay now
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex justify-content-between">
|
||||
|
||||
<div >Updated @Model.LastUpdated.ToString("g")</div>
|
||||
<div >
|
||||
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal file
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal file
@ -0,0 +1,213 @@
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
|
||||
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
|
||||
}
|
||||
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@await Html.PartialAsync("MinimalPaymentRequest", Model)
|
||||
}
|
||||
else
|
||||
{
|
||||
<noscript>
|
||||
@await Html.PartialAsync("MinimalPaymentRequest", Model)
|
||||
</noscript>
|
||||
|
||||
<div class="container" id="app" v-cloak>
|
||||
<div class="row w-100 p-0 m-0" style="height: 100vh">
|
||||
<div class="mx-auto my-auto w-100">
|
||||
<div class="card">
|
||||
<h1 class="card-header">
|
||||
{{srvModel.title}}
|
||||
|
||||
<span class="text-muted float-right text-center">
|
||||
<template v-if="settled">Settled</template>
|
||||
<template v-else>
|
||||
<template v-if="ended">Request Expired</template>
|
||||
<template v-else-if="endDiff">Expires in {{endDiff}}</template>
|
||||
</template>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="card-body px-0 pt-0">
|
||||
<div class="row mb-4">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6 ">
|
||||
<ul class="w-100 list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Request amount:</span>
|
||||
<span class="h2">{{srvModel.amountFormatted}}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Paid so far:</span>
|
||||
<span class="h2">{{srvModel.amountCollectedFormatted}}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="h2 text-muted">Amount due:</span>
|
||||
<span class="h2">{{srvModel.amountDueFormatted}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-html="srvModel.description" class="w-100 p-2"></div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="table-responsive">
|
||||
<table class="table border-top-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class=" border-top-0" scope="col">Invoice #</th>
|
||||
<th class=" border-top-0">Price</th>
|
||||
<th class=" border-top-0">Expiry</th>
|
||||
<th class=" border-top-0">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!srvModel.invoices || srvModel.invoices.length == 0">
|
||||
<td colspan="4" class="text-center">No payments made yet</td>
|
||||
</tr>
|
||||
<template v-else v-for="invoice of srvModel.invoices" :key="invoice.id">
|
||||
<tr class="bg-light">
|
||||
<td scope="row">{{invoice.id}}</td>
|
||||
<td>{{invoice.amountFormatted}}</td>
|
||||
<td>{{moment(invoice.expiryDate).format('L HH:mm')}}</td>
|
||||
<td>{{invoice.status}}</td>
|
||||
</tr>
|
||||
<tr class="bg-light" v-if="invoice.payments && invoice.payments.length > 0">
|
||||
<td colspan="4" class=" px-2 py-1 border-top-0">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="p-1" style="max-width: 300px">Tx Id</th>
|
||||
<th class="p-1">Payment Method</th>
|
||||
<th class="p-1">Amount</th>
|
||||
<th class="p-1">Link</th>
|
||||
</tr>
|
||||
<tr v-for="payment of invoice.payments">
|
||||
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
|
||||
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">{{payment.id}}</div>
|
||||
</td>
|
||||
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
|
||||
{{payment.id}}
|
||||
</td>
|
||||
<td class="p-1">{{formatPaymentMethod(payment.paymentMethod)}}</td>
|
||||
<td class="p-1">{{payment.amount.noExponents()}}</td>
|
||||
<td class="p-1 d-print-none">
|
||||
<a v-if="payment.link" :href="payment.link" target="_blank">Link</a>
|
||||
</td>
|
||||
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
|
||||
{{payment.link}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-if="!ended && (srvModel.amountDue) > 0" class="d-print-none">
|
||||
<td colspan="4" class="text-center">
|
||||
|
||||
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
|
||||
<form v-on:submit="submitCustomAmountForm">
|
||||
|
||||
<div class="input-group m-auto" style="max-width: 250px">
|
||||
<input
|
||||
:readonly="!srvModel.allowCustomPaymentAmounts"
|
||||
class="form-control"
|
||||
type="number"
|
||||
v-model="customAmount"
|
||||
:max="srvModel.amountDue"
|
||||
step="any"
|
||||
placeholder="Amount"
|
||||
required>
|
||||
<div class="input-group-append">
|
||||
<span class='input-group-text'>{{currency}}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
v-bind:class="{ 'btn-disabled': loading}"
|
||||
:disabled="loading"
|
||||
type="submit">
|
||||
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
Pay now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
||||
<button v-else class="btn btn-primary btn-lg " v-on:click="pay(null)"
|
||||
:disabled="loading">
|
||||
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
Pay now
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex justify-content-between">
|
||||
|
||||
<div >
|
||||
<span v-on:click="print" class="btn-link d-print-none" style="cursor: pointer"> <span class="fa fa-print"></span> Print</span>
|
||||
<span>Updated {{lastUpdated}}</span>
|
||||
</div>
|
||||
<div >
|
||||
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</body>
|
||||
</html>
|
@ -41,7 +41,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Password"></label>
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" />
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" value="@Model.Settings.Password" />
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
@model SparkServicesViewModel
|
||||
@model LightningWalletServices
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
|
||||
}
|
||||
|
||||
|
||||
<h4>Spark service</h4>
|
||||
<h4>@Model.WalletName</h4>
|
||||
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
|
||||
|
||||
@if (Model.ShowQR)
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="form-group">
|
||||
<h5>Browser connection</h5>
|
||||
<p>
|
||||
<span>You can go to spark from your browser by <a href="@Model.SparkLink" target="_blank">clicking here</a><br /></span>
|
||||
<span>You can go to @Model.WalletName from your browser by <a href="@Model.ServiceLink" target="_blank">clicking here</a><br /></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
{
|
||||
<div class="form-group">
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.SparkLink)"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -70,7 +70,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.SparkLink)",
|
||||
text: "@Html.Raw(Model.ServiceLink)",
|
||||
width: 150,
|
||||
height: 150
|
||||
});
|
@ -17,7 +17,7 @@
|
||||
<div class="col-md-8">
|
||||
<h4>Crypto services</h4>
|
||||
<div class="form-group">
|
||||
<span>You can get access here to LND (gRPC, Rest) services exposed by your server</span>
|
||||
<span>You can get access here to services exposed by your server</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -30,13 +30,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var lnd in Model.LNDServices)
|
||||
@foreach (var s in Model.ExternalServices)
|
||||
{
|
||||
<tr>
|
||||
<td>@lnd.Crypto</td>
|
||||
<td>@lnd.Type</td>
|
||||
<td>@s.CryptoCode</td>
|
||||
<td>@s.DisplayName</td>
|
||||
<td style="text-align:right">
|
||||
<a asp-action="@lnd.Action" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
|
||||
<a asp-action="Service" asp-route-serviceName="@s.ServiceName" asp-route-cryptoCode="@s.CryptoCode">See information</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.ExternalServices.Count != 0)
|
||||
@if (Model.OtherExternalServices.Count != 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@ -63,7 +63,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in Model.ExternalServices)
|
||||
@foreach (var s in Model.OtherExternalServices)
|
||||
{
|
||||
<tr>
|
||||
<td>@s.Name</td>
|
||||
|
@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
|
||||
<nav class='navbar navbar-expand-lg navbar-dark fixed-top @additionalStyle' id="mainNav">
|
||||
<div class="container">
|
||||
<a class="navbar-brand js-scroll-trigger" href="~/">
|
||||
<img src="~/img/logo.png" height="45">
|
||||
@ -66,6 +66,7 @@
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger">Payment Requests</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
@ -84,12 +85,7 @@
|
||||
</div>
|
||||
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
|
||||
<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 want to change this expectation:</span>
|
||||
<ul>
|
||||
<li>Either starts BTCPay with <b>--externalurl @(env.ExpectedProtocol)://@(env.ExpectedHost)/</b></li>
|
||||
<li>Or, if using docker-compose deployment, setting environment variable <b>BTCPAY_PROTOCOL=@(env.ExpectedProtocol)</b> and <b>BTCPAY_HOST=@(env.ExpectedDomain)</b></li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
@ -116,6 +112,7 @@
|
||||
var expectedProtocol = @Html.Raw(Json.Serialize(env.ExpectedProtocol));
|
||||
if (window.location.host != expectedDomain || window.location.protocol != expectedProtocol + ":") {
|
||||
document.getElementById("badUrl").style.display = "block";
|
||||
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
@ -124,5 +124,54 @@
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||
"wwwroot/crowdfund/**/*.css"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/summernote/summernote-bs4.js",
|
||||
"wwwroot/vendor/flatpickr/flatpickr.js",
|
||||
"wwwroot/payment-request-admin/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/summernote/summernote-bs4.css",
|
||||
"wwwroot/vendor/flatpickr/flatpickr.min.css"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/payment-request-bundle-1.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
"wwwroot/vendor/vue-toasted/vue-toasted.min.js",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
|
||||
"wwwroot/vendor/signalr/signalr.js",
|
||||
"wwwroot/vendor/animejs/anime.min.js",
|
||||
"wwwroot/modal/btcpay.js",
|
||||
"wwwroot/payment-request/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/payment-request-bundle-2.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/moment/moment.js"
|
||||
],
|
||||
"minify": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/payment-request-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||
"wwwroot/payment-request/**/*.css"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -25,6 +25,10 @@ function Cart() {
|
||||
}
|
||||
|
||||
Cart.prototype.setCustomAmount = function(amount) {
|
||||
if (!srvModel.showCustomAmount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.customAmount = this.toNumber(amount);
|
||||
|
||||
if (this.customAmount > 0) {
|
||||
@ -36,10 +40,18 @@ Cart.prototype.setCustomAmount = function(amount) {
|
||||
}
|
||||
|
||||
Cart.prototype.getCustomAmount = function() {
|
||||
if (!srvModel.showCustomAmount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.toCents(this.customAmount);
|
||||
}
|
||||
|
||||
Cart.prototype.setTip = function(amount) {
|
||||
if (!srvModel.enableTips) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.tip = this.toNumber(amount);
|
||||
|
||||
if (this.tip > 0) {
|
||||
@ -51,10 +63,18 @@ Cart.prototype.setTip = function(amount) {
|
||||
}
|
||||
|
||||
Cart.prototype.getTip = function() {
|
||||
if (!srvModel.enableTips) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.toCents(this.tip);
|
||||
}
|
||||
|
||||
Cart.prototype.setDiscount = function(amount) {
|
||||
if (!srvModel.showDiscount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.discount = this.toNumber(amount);
|
||||
|
||||
if (this.discount > 0) {
|
||||
@ -66,10 +86,18 @@ Cart.prototype.setDiscount = function(amount) {
|
||||
}
|
||||
|
||||
Cart.prototype.getDiscount = function() {
|
||||
if (!srvModel.showDiscount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.toCents(this.discount);
|
||||
}
|
||||
|
||||
Cart.prototype.getDiscountAmount = function(amount) {
|
||||
if (!srvModel.showDiscount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.percentage(amount, this.getDiscount());
|
||||
}
|
||||
|
||||
|
@ -8223,7 +8223,7 @@ a:hover {
|
||||
}
|
||||
|
||||
.action-button--secondary {
|
||||
background: #e7e7e7;
|
||||
background: #e0e0e0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
color: #4A4A4A;
|
||||
@ -8397,9 +8397,9 @@ strong {
|
||||
}
|
||||
|
||||
.currency-selection {
|
||||
border-bottom: 1px solid #E9E9E9;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
position: relative;
|
||||
padding: 4px 15px;
|
||||
padding: 4px 10px 4px 15px;
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
color: #565D6E;
|
||||
@ -8516,7 +8516,7 @@ strong {
|
||||
.payment-tabs {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-top: 1px solid #E9E9E9;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
font-size: 13.5px;
|
||||
box-shadow: 0px 5px 7px 0px rgba(0, 0, 0, 0.09);
|
||||
@ -8605,7 +8605,7 @@ strong {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
@ -8701,7 +8701,7 @@ strong {
|
||||
|
||||
.manual-box.underpaid-expired__refund-pending > .manual-box__amount {
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 1, 1);
|
||||
border-top-color: #E9E9E9;
|
||||
border-top-color: #e0e0e0;
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
@ -8722,7 +8722,7 @@ strong {
|
||||
padding-bottom: .7rem;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-bottom: 0;
|
||||
max-height: 300px;
|
||||
}
|
||||
@ -8737,7 +8737,7 @@ strong {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.flipped .manual-box__amount {
|
||||
@ -8776,8 +8776,8 @@ strong {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
left: 50%;
|
||||
border-right: 1px solid #E9E9E9;
|
||||
border-bottom: 1px solid #E9E9E9;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
transform: rotate(45deg);
|
||||
margin-left: -5px;
|
||||
bottom: -6px;
|
||||
@ -8800,8 +8800,8 @@ strong {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
left: 50%;
|
||||
border-right: 1px solid #E9E9E9;
|
||||
border-bottom: 1px solid #E9E9E9;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
transform: rotateZ(45deg);
|
||||
margin-left: -5px;
|
||||
top: -5px;
|
||||
@ -8813,7 +8813,7 @@ strong {
|
||||
.manual-box__address > .flipper {
|
||||
backface-visibility: hidden;
|
||||
height: 100%;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-top: 0;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
@ -9155,7 +9155,7 @@ strong {
|
||||
}
|
||||
|
||||
.bp-input {
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
@ -9482,7 +9482,7 @@ strong {
|
||||
.payment__scan__qrcode img {
|
||||
padding: 15px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@ -9714,7 +9714,7 @@ strong {
|
||||
background: #fff;
|
||||
padding: 30px 15px;
|
||||
text-align: center;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
margin: -5px 0 18px;
|
||||
font-weight: 300;
|
||||
@ -11335,7 +11335,7 @@ low-fee-timeline {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #E9E9E9;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@ -11353,7 +11353,7 @@ low-fee-timeline {
|
||||
}
|
||||
|
||||
.copySectionBox.bottomBorder {
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.separatorGem {
|
||||
@ -11361,8 +11361,8 @@ low-fee-timeline {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
left: 50%;
|
||||
border-right: 1px solid #E9E9E9;
|
||||
border-bottom: 1px solid #E9E9E9;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
transform: rotateZ(45deg);
|
||||
margin-left: -5px;
|
||||
top: -5px;
|
||||
@ -11374,7 +11374,7 @@ low-fee-timeline {
|
||||
|
||||
.checkoutTextbox {
|
||||
width: 100%;
|
||||
border: 1px solid #e9e9e9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
padding: 8px 4px 8px;
|
||||
@ -11402,7 +11402,7 @@ low-fee-timeline {
|
||||
color: #aaa;
|
||||
height: 16px;
|
||||
padding: 0px 4px;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.inputWithIcon.inputIconBg img {
|
||||
|
@ -40,32 +40,41 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.paywithRowRight {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.cursorPointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.payment__currencies {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.payment__currencies img {
|
||||
height: 32px;
|
||||
.payment__currencies_noborder {
|
||||
font-size: 13px;
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.payment__currencies img , .payment__currencies_noborder img{
|
||||
margin-top: -3px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.payment__currencies:hover .clickable_underline {
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
|
||||
.clickable_underline {
|
||||
border-bottom: 1px dotted #ccc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.payment__currencies:hover .clickable_underline {
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
.payment__currencies:hover {
|
||||
border: 1px solid #5c6373;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.clickable_indicator {
|
||||
margin-right: -10px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.payment__currencies:hover .clickable_indicator {
|
||||
opacity: 1;
|
||||
color: #5c6373;
|
||||
}
|
||||
|
@ -234,6 +234,7 @@ $(document).ready(function () {
|
||||
// Should connect using webhook ?
|
||||
// If notification received
|
||||
|
||||
var socket = null;
|
||||
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
||||
if (supportsWebSockets) {
|
||||
var loc = window.location, ws_uri;
|
||||
@ -245,7 +246,7 @@ $(document).ready(function () {
|
||||
ws_uri += "//" + loc.host;
|
||||
ws_uri += loc.pathname + "/status/ws?invoiceId=" + srvModel.invoiceId;
|
||||
try {
|
||||
var socket = new WebSocket(ws_uri);
|
||||
socket = new WebSocket(ws_uri);
|
||||
socket.onmessage = function (e) {
|
||||
fetchStatus();
|
||||
};
|
||||
@ -259,7 +260,9 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
var watcher = setInterval(function () {
|
||||
fetchStatus();
|
||||
if (socket === null || socket.readyState !== 1) {
|
||||
fetchStatus();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
$(".menu__item").click(function () {
|
||||
|
@ -1,8 +1,19 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function() {
|
||||
|
||||
$(".richtext").summernote();
|
||||
$(".datetime").flatpickr({
|
||||
enableTime: true
|
||||
$(".richtext").summernote({
|
||||
minHeight: 300
|
||||
});
|
||||
$(".datetime").each(function(){
|
||||
var element = $(this);
|
||||
var min = element.attr("min");
|
||||
var max = element.attr("max");
|
||||
var defaultDate = element.attr("value");
|
||||
element.flatpickr({
|
||||
enableTime: true,
|
||||
minDate: min,
|
||||
maxDate: max,
|
||||
defaultDate: defaultDate
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -190,14 +190,14 @@ addLoadEvent(function (ev) {
|
||||
var mDiffH = moment(this.srvModel.endDate).diff(moment(), "hours");
|
||||
var mDiffM = moment(this.srvModel.endDate).diff(moment(), "minutes");
|
||||
var mDiffS = moment(this.srvModel.endDate).diff(moment(), "seconds");
|
||||
this.endDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
||||
this.endDiff = mDiffD > 0? mDiffD + " days" : mDiffH> 0? mDiffH + " hours" : mDiffM> 0? mDiffM+ " minutes" : mDiffS> 0? mDiffS + " seconds": "";
|
||||
}
|
||||
if(!this.started && this.srvModel.startDate){
|
||||
var mDiffD = moment(this.srvModel.startDate).diff(moment(), "days");
|
||||
var mDiffH = moment(this.srvModel.startDate).diff(moment(), "hours");
|
||||
var mDiffM = moment(this.srvModel.startDate).diff(moment(), "minutes");
|
||||
var mDiffS = moment(this.srvModel.startDate).diff(moment(), "seconds");
|
||||
this.startDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
||||
this.startDiff = mDiffD > 0? mDiffD + " days" : mDiffH> 0? mDiffH + " hours" : mDiffM> 0? mDiffM+ " minutes" : mDiffS> 0? mDiffS + " seconds": "";
|
||||
}
|
||||
this.lastUpdated = moment(this.srvModel.info.lastUpdated).calendar();
|
||||
this.active = this.started && !this.ended;
|
||||
|
50
BTCPayServer/wwwroot/locales/da-DK.json
Normal file
50
BTCPayServer/wwwroot/locales/da-DK.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
|
||||
"code": "da-DK",
|
||||
"currentLanguage": "Dansk",
|
||||
"lang": "Sprog",
|
||||
"Awaiting Payment...": "Afventer betaling...",
|
||||
"Pay with": "Betal med",
|
||||
"Contact and Refund Email": "Kontakt og Tilbagebetalings Email",
|
||||
"Contact_Body": "Angiv venligst en email adresse herunder. Vi kontakter dig på denne email, hvis der er et problem med din betaling.",
|
||||
"Your email": "Din email",
|
||||
"Continue": "Forsæt",
|
||||
"Please enter a valid email address": "Indtast venglist en gyldig email adresse",
|
||||
"Order Amount": "Bestillingsbeløb",
|
||||
"Network Cost": "Netværks gebyr",
|
||||
"Already Paid": "Allerede betalt",
|
||||
"Due": "Manglende betaling",
|
||||
"Scan": "Skan",
|
||||
"Copy": "Kopier",
|
||||
"Conversion": "Konvertering",
|
||||
"Open in wallet": "Åben i wallet",
|
||||
"CompletePay_Body": "For at færdiggøre din betaling, send venglist {{btcDue}} {{cryptoCode}} til addressen herunder.",
|
||||
"Amount": "Beløb",
|
||||
"Address": "Adresse",
|
||||
"Copied": "Kopieret",
|
||||
"ConversionTab_BodyTop": "Du kan betale {{btcDue}} {{cryptoCode}} med andre altcoins end dem sælgeren understøtter. ",
|
||||
"ConversionTab_BodyDesc": "Denne service er stillet til rådighed af 3. Partnere. Vær venligst opmærksom på, at vi ikke har kontrol over, hvordan udbydere vil videresende dine midler. Fakturaen vil kun blive markeret betalt, når der er modtaget midler på {{cryptoCode}} Blockchain.",
|
||||
"ConversionTab_CalculateAmount_Error": "Genprøv",
|
||||
"ConversionTab_LoadCurrencies_Error": "Genprøv",
|
||||
"ConversionTab_Lightning": "Ingen konversions partnere tilgængelig for Lightning Network betalinger.",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Vælg venligst en valuta at konvertere fra",
|
||||
"Invoice expiring soon...": "Fakturaen udløber snart...",
|
||||
"Invoice expired": "Fakturaen er udløbet",
|
||||
"What happened?": "Hvad skete der?",
|
||||
"InvoiceExpired_Body_1": "Fakturaen er udløbet. En faktura er kun gyldig i {{maxTime Minutes}} minutter. \nDu kan vende tilbage til {{butiknavn}}, hvis du gerne vil sende din betaling igen.",
|
||||
"InvoiceExpired_Body_2": "Hvis du har forsøgt at sende en betaling, er den endnu ikke accepteret af netværket. Vi har endnu ikke modtaget dine midler.",
|
||||
"InvoiceExpired_Body_3": "Hvis vi modtager den på et senere tidspunkt, vil vi enten behandle din ordre eller kontakte dig for at aftale refundering...",
|
||||
"Invoice ID": "Faktura ID",
|
||||
"Order ID": "Ordre ID",
|
||||
"Return to StoreName": "Returner til {{storeName}}",
|
||||
"This invoice has been paid": "Denne faktura er blevet betalt",
|
||||
"This invoice has been archived": "Denne faktura er blevet arkiveret",
|
||||
"Archived_Body": "Kontakt venligt butikken for ordre information eller assistance",
|
||||
"BOLT 11 Invoice": "BOLT 11 Faktura",
|
||||
"Node Info": "Node Information",
|
||||
"txCount": "{{count}} transaktion",
|
||||
"txCount_plural": "{{count}} transaktioner",
|
||||
"Pay with CoinSwitch": "Betal med CoinSwitch",
|
||||
"Pay with Changelly": "Betal med Changelly",
|
||||
"Close": "Luk"
|
||||
}
|
@ -44,7 +44,7 @@
|
||||
"Node Info": "Informations sur le nœud",
|
||||
"txCount": "{{count}} transaction",
|
||||
"txCount_plural": "{{count}} transactions",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close"
|
||||
"Pay with CoinSwitch": "Payer avec CoinSwitch",
|
||||
"Pay with Changelly": "Payer avec Changelly",
|
||||
"Close": "Fermer"
|
||||
}
|
@ -14,3 +14,8 @@
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.only-for-js{
|
||||
display: none;
|
||||
}
|
||||
|
@ -6,4 +6,11 @@
|
||||
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
|
||||
$(this).text(dateString);
|
||||
});
|
||||
|
||||
|
||||
$(".input-group-clear").on("click", function(){
|
||||
$(this).parents(".input-group").find("input").val(null);
|
||||
});
|
||||
|
||||
$(".only-for-js").show();
|
||||
});
|
||||
|
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal file
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal file
@ -0,0 +1,19 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
$(".richtext").summernote({
|
||||
minHeight: 300
|
||||
});
|
||||
$(".datetime").each(function(){
|
||||
var element = $(this);
|
||||
var min = element.attr("min");
|
||||
var max = element.attr("max");
|
||||
var defaultDate = element.attr("value");
|
||||
element.flatpickr({
|
||||
enableTime: true,
|
||||
minDate: min,
|
||||
maxDate: max,
|
||||
defaultDate: defaultDate
|
||||
});
|
||||
});
|
||||
|
||||
});
|
176
BTCPayServer/wwwroot/payment-request/app.js
Normal file
176
BTCPayServer/wwwroot/payment-request/app.js
Normal file
@ -0,0 +1,176 @@
|
||||
var app = null;
|
||||
var eventAggregator = new Vue();
|
||||
|
||||
function addLoadEvent(func) {
|
||||
var oldonload = window.onload;
|
||||
if (typeof window.onload != 'function') {
|
||||
window.onload = func;
|
||||
} else {
|
||||
window.onload = function () {
|
||||
if (oldonload) {
|
||||
oldonload();
|
||||
}
|
||||
func();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addLoadEvent(function (ev) {
|
||||
Vue.use(Toasted);
|
||||
|
||||
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
data: function () {
|
||||
return {
|
||||
srvModel: window.srvModel,
|
||||
connectionStatus: "",
|
||||
endDate: "",
|
||||
endDateRelativeTime: "",
|
||||
ended: false,
|
||||
endDiff: "",
|
||||
active: true,
|
||||
lastUpdated: "",
|
||||
loading: false,
|
||||
timeoutState: "",
|
||||
customAmount: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currency: function () {
|
||||
return this.srvModel.currency.toUpperCase();
|
||||
},
|
||||
settled: function () {
|
||||
return this.srvModel.amountDue <= 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateComputed: function () {
|
||||
if (this.srvModel.expiryDate) {
|
||||
var endDateM = moment(this.srvModel.expiryDate);
|
||||
this.endDate = endDateM.format('MMMM Do YYYY');
|
||||
this.endDateRelativeTime = endDateM.fromNow();
|
||||
this.ended = endDateM.isBefore(moment());
|
||||
|
||||
} else {
|
||||
this.ended = false;
|
||||
}
|
||||
|
||||
if (!this.ended && this.srvModel.expiryDate) {
|
||||
var mDiffD = moment(this.srvModel.expiryDate).diff(moment(), "days");
|
||||
var mDiffH = moment(this.srvModel.expiryDate).diff(moment(), "hours");
|
||||
var mDiffM = moment(this.srvModel.expiryDate).diff(moment(), "minutes");
|
||||
var mDiffS = moment(this.srvModel.expiryDate).diff(moment(), "seconds");
|
||||
this.endDiff = mDiffD > 0 ? mDiffD + " days" : mDiffH > 0 ? mDiffH + " hours" : mDiffM > 0 ? mDiffM + " minutes" : mDiffS > 0 ? mDiffS + " seconds" : "";
|
||||
}
|
||||
|
||||
this.lastUpdated = moment(this.srvModel.lastUpdated).calendar();
|
||||
this.active = !this.ended;
|
||||
setTimeout(this.updateComputed, 1000);
|
||||
},
|
||||
setLoading: function (val) {
|
||||
this.loading = val;
|
||||
if (this.timeoutState) {
|
||||
clearTimeout(this.timeoutState);
|
||||
}
|
||||
},
|
||||
pay: function (amount) {
|
||||
this.setLoading(true);
|
||||
var self = this;
|
||||
self.timeoutState = setTimeout(function () {
|
||||
self.setLoading(false);
|
||||
}, 5000);
|
||||
|
||||
eventAggregator.$emit("pay", amount);
|
||||
},
|
||||
formatPaymentMethod: function (str) {
|
||||
|
||||
if (str.endsWith("LightningLike")) {
|
||||
return str.replace("LightningLike", "Lightning")
|
||||
}
|
||||
return str;
|
||||
|
||||
},
|
||||
print:function(){
|
||||
window.print();
|
||||
},
|
||||
submitCustomAmountForm : function(e){
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if(this.srvModel.allowCustomPaymentAmounts && parseFloat(this.customAmount) < this.srvModel.amountDue){
|
||||
this.pay(parseFloat(this.customAmount));
|
||||
}else{
|
||||
this.pay();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
||||
this.customAmount = (this.srvModel.amountDue || 0).noExponents();
|
||||
hubListener.connect();
|
||||
var self = this;
|
||||
eventAggregator.$on("invoice-created", function (invoiceId) {
|
||||
self.setLoading(false);
|
||||
btcpay.setApiUrlPrefix(window.location.origin);
|
||||
btcpay.showInvoice(invoiceId);
|
||||
btcpay.showFrame();
|
||||
});
|
||||
eventAggregator.$on("invoice-error", function (error) {
|
||||
self.setLoading(false);
|
||||
var msg = "";
|
||||
if (typeof error === "string") {
|
||||
msg = error;
|
||||
} else if (!error) {
|
||||
msg = "Unknown Error";
|
||||
} else {
|
||||
msg = JSON.stringify(error);
|
||||
}
|
||||
|
||||
Vue.toasted.show("Error creating invoice: " + msg, {
|
||||
iconPack: "fontawesome",
|
||||
icon: "exclamation-triangle",
|
||||
fullWidth: false,
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-center",
|
||||
duration: 10000
|
||||
});
|
||||
});
|
||||
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
|
||||
var onChain = type.toLowerCase() === "btclike";
|
||||
amount = parseFloat(amount).noExponents();
|
||||
if (onChain) {
|
||||
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
|
||||
iconPack: "fontawesome",
|
||||
icon: "plus",
|
||||
duration: 10000
|
||||
});
|
||||
} else {
|
||||
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
|
||||
iconPack: "fontawesome",
|
||||
icon: "bolt",
|
||||
duration: 10000
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
eventAggregator.$on("info-updated", function (model) {
|
||||
console.warn("UPDATED", self.srvModel, arguments);
|
||||
self.srvModel = model;
|
||||
});
|
||||
eventAggregator.$on("connection-pending", function () {
|
||||
self.connectionStatus = "pending";
|
||||
});
|
||||
eventAggregator.$on("connection-failed", function () {
|
||||
self.connectionStatus = "failed";
|
||||
});
|
||||
eventAggregator.$on("connection-lost", function () {
|
||||
self.connectionStatus = "connection lost";
|
||||
});
|
||||
this.updateComputed();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
@ -0,0 +1,17 @@
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal file
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal file
@ -0,0 +1,48 @@
|
||||
var hubListener = function () {
|
||||
|
||||
var connection = new signalR.HubConnectionBuilder().withUrl("/payment-requests/hub").build();
|
||||
|
||||
connection.onclose(function () {
|
||||
eventAggregator.$emit("connection-lost");
|
||||
console.error("Connection was closed. Attempting reconnect in 2s");
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
connection.on("PaymentReceived", function (amount, cryptoCode, type) {
|
||||
eventAggregator.$emit("payment-received", amount, cryptoCode, type);
|
||||
});
|
||||
connection.on("InvoiceCreated", function (invoiceId) {
|
||||
eventAggregator.$emit("invoice-created", invoiceId);
|
||||
});
|
||||
connection.on("InvoiceError", function (error) {
|
||||
eventAggregator.$emit("invoice-error", error);
|
||||
});
|
||||
connection.on("InfoUpdated", function (model) {
|
||||
eventAggregator.$emit("info-updated", model);
|
||||
});
|
||||
|
||||
function connect() {
|
||||
|
||||
eventAggregator.$emit("connection-pending");
|
||||
connection
|
||||
.start()
|
||||
.then(function () {
|
||||
connection.invoke("ListenToPaymentRequest", srvModel.id);
|
||||
|
||||
})
|
||||
.catch(function (err) {
|
||||
eventAggregator.$emit("connection-failed");
|
||||
console.error("Could not connect to backend. Retrying in 2s", err);
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
eventAggregator.$on("pay", function (amount) {
|
||||
connection.invoke("Pay", amount);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
connect: connect
|
||||
};
|
||||
}();
|
||||
|
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
@ -0,0 +1,7 @@
|
||||
[v-cloak] > * {
|
||||
display: none
|
||||
}
|
||||
|
||||
[v-cloak]::before {
|
||||
content: "loading…"
|
||||
}
|
@ -21,4 +21,5 @@ ENV BTCPAY_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]
|
||||
COPY docker-entrypoint.sh docker-entrypoint.sh
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
@ -18,4 +18,5 @@ ENV BTCPAY_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]
|
||||
COPY docker-entrypoint.sh docker-entrypoint.sh
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
@ -10,6 +10,7 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{29290EC7-00E6-4C4B-96D9-4D7E9611DF28}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.circleci\config.yml = .circleci\config.yml
|
||||
docker-entrypoint.sh = docker-entrypoint.sh
|
||||
Dockerfile.linuxamd64 = Dockerfile.linuxamd64
|
||||
Dockerfile.linuxarm32v7 = Dockerfile.linuxarm32v7
|
||||
EndProjectSection
|
||||
|
4
docker-entrypoint.sh
Executable file
4
docker-entrypoint.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "$(/sbin/ip route|awk '/default/ { print $3 }') host.docker.internal" >> /etc/hosts
|
||||
exec dotnet BTCPayServer.dll
|
Reference in New Issue
Block a user