Compare commits

...

10 Commits

12 changed files with 168 additions and 50 deletions

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.56</Version>
<Version>1.0.3.64</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.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
@ -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">

View File

@ -109,18 +109,22 @@ 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);
}
}
@ -131,28 +135,57 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
Logs.Configuration.LogWarning($"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);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
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))
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
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'");
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
@ -161,14 +194,18 @@ namespace BTCPayServer.Configuration
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)
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
Logs.Configuration.LogWarning($"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);
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
$"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -5,7 +5,12 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
@ -15,5 +20,19 @@ namespace BTCPayServer.Configuration.External
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -10,11 +10,11 @@ namespace BTCPayServer.Configuration
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result)
public static bool TryParse(string str, out SparkConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
@ -25,15 +25,30 @@ namespace BTCPayServer.Configuration
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
if(resultTemp.Server.Scheme == "http")
{
error = "Insecure transport protocol (http)";
return false;
}
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookeFile = kv[1];
break;
default:

View File

@ -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,

View File

@ -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,15 +103,17 @@ namespace BTCPayServer.Controllers
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(taxIncluded, currencyInfo.CurrencyDecimalDigits);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded);
invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);

View File

@ -27,6 +27,7 @@ using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
{
@ -474,7 +475,17 @@ namespace BTCPayServer.Controllers
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
@ -548,37 +559,41 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
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)
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
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]}";
}
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
return View("LightningWalletServices", vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]

View File

@ -72,7 +72,7 @@ namespace BTCPayServer.Models
[JsonProperty(PropertyName = "refundable", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Refundable { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded { get; set; }
public decimal? TaxIncluded { get; set; }
[JsonProperty(PropertyName = "nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public long Nonce { get; set; }
[JsonProperty(PropertyName = "guid", DefaultValueHandling = DefaultValueHandling.Ignore)]

View File

@ -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; }
}
}

View File

@ -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;

View File

@ -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
});