diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 97799073c..330a71d06 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -48,6 +48,7 @@ using BTCPayServer.Models.ServerViewModels; using BTCPayServer.Security; using NBXplorer.Models; using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; +using NBitpayClient.Extensions; namespace BTCPayServer.Tests { @@ -350,57 +351,6 @@ namespace BTCPayServer.Tests } } - [Fact] - [Trait("Integration", "Integration")] - public void CanPayUsingBIP70() - { - using (var tester = ServerTester.Create()) - { - tester.Start(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - Assert.True(user.BitPay.TestAccess(Facade.Merchant)); - var invoice = user.BitPay.CreateInvoice(new Invoice() - { - Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - //RedirectURL = redirect + "redirect", - //NotificationURL = CallbackUri + "/notification", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - Assert.False(invoice.Refundable); - - var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72); - var request = url.GetPaymentRequest(); - var payment = request.CreatePayment(); - - Transaction tx = new Transaction(); - tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script))); - var cashCow = tester.ExplorerNode; - tx = cashCow.FundRawTransaction(tx).Transaction; - tx = cashCow.SignRawTransaction(tx); - - payment.Transactions.Add(tx); - - payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey)); - var ack = payment.SubmitPayment(); - Assert.NotNull(ack); - - Eventually(() => - { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("paid", localInvoice.Status); - Assert.True(localInvoice.Refundable); - }); - } - } - [Fact] [Trait("Integration", "Integration")] public async Task CanSetLightningServer() @@ -934,6 +884,17 @@ namespace BTCPayServer.Tests var result = client.SendAsync(message).GetAwaiter().GetResult(); result.EnsureSuccessStatusCode(); ///////////////////// + + // Have error 403 with bad signature + client = new HttpClient(); + HttpRequestMessage mess = new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens"); + mess.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); + mess.Headers.Add("x-signature", "3045022100caa123193afc22ef93d9c6b358debce6897c09dd9869fe6fe029c9cb43623fac022000b90c65c50ba8bbbc6ebee8878abe5659e17b9f2e1b27d95eda4423da5608fe"); + mess.Headers.Add("x-identity", "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99"); + mess.Method = HttpMethod.Get; + result = client.SendAsync(mess).GetAwaiter().GetResult(); + Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode); + // } } @@ -1801,7 +1762,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate Assert.True(IsMapped(invoice, ctx)); cashCow.SendToAddress(invoiceAddress, firstPayment); - var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult(); + var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); Assert.Single(invoiceEntity.HistoricalAddresses); Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned); @@ -1819,7 +1780,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(localInvoice, ctx)); - invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult(); + invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress); Assert.NotNull(historical1.UnAssigned); var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress); diff --git a/BTCPayServer/Authentication/TokenRepository.cs b/BTCPayServer/Authentication/TokenRepository.cs index 776533fe3..c1f3b5e12 100644 --- a/BTCPayServer/Authentication/TokenRepository.cs +++ b/BTCPayServer/Authentication/TokenRepository.cs @@ -242,6 +242,8 @@ namespace BTCPayServer.Authentication using (var ctx = _Factory.CreateContext()) { var token = await ctx.PairedSINData.FindAsync(tokenId); + if (token == null) + return null; return CreateTokenEntity(token); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index e883fb649..12834d418 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.3.23 + 1.0.3.27 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -33,7 +33,7 @@ - + @@ -46,7 +46,7 @@ all runtime; build; native; contentfiles; analyzers - + @@ -136,9 +136,6 @@ $(IncludeRazorContentInPack) - - $(IncludeRazorContentInPack) - $(IncludeRazorContentInPack) @@ -154,7 +151,7 @@ $(IncludeRazorContentInPack) - + $(IncludeRazorContentInPack) diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index e477a200b..e0272d854 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -141,6 +141,18 @@ namespace BTCPayServer.Configuration Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray())); + var services = conf.GetOrDefault("externalservices", null); + if(services != null) + { + foreach(var service in services.Split(new[] { ';', ',' }) + .Select(p => p.Split(':')) + .Where(p => p.Length == 2) + .Select(p => (Name: p[0], Link: p[1]))) + { + ExternalServices.AddOrReplace(service.Name, service.Link); + } + } + PostgresConnectionString = conf.GetOrDefault("postgres", null); MySQLConnectionString = conf.GetOrDefault("mysql", null); BundleJsCss = conf.GetOrDefault("bundlejscss", true); @@ -248,6 +260,8 @@ namespace BTCPayServer.Configuration public string RootPath { get; set; } public Dictionary InternalLightningByCryptoCode { get; set; } = new Dictionary(); + public Dictionary ExternalServices { get; set; } = new Dictionary(); + public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices(); public BTCPayNetworkProvider NetworkProvider { get; set; } diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index ef8511db3..1f26bd61c 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -33,6 +33,7 @@ namespace BTCPayServer.Configuration app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue); app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue); app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue); + app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue); app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue); app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue); app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue); diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 1f855ad05..c8ea7b594 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Filters; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Security; using BTCPayServer.Services.Apps; @@ -32,6 +33,7 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("/apps/{appId}/pos")] + [XFrameOptionsAttribute(null)] public async Task ViewPointOfSale(string appId) { var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale); diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index 6415f13e7..6747570ff 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -50,6 +50,7 @@ namespace BTCPayServer.Controllers try { BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink); +#pragma warning disable CS0618 // Type or member is obsolete if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase)) { throw new Exception("This tool only work with bitpay"); @@ -57,6 +58,7 @@ namespace BTCPayServer.Controllers var client = HttpClientFactory.CreateClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl); +#pragma warning restore CS0618 // Type or member is obsolete request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request")); var result = await client.SendAsync(request); // {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"} diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index da7bc8524..58c637a9d 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -40,16 +40,18 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("invoices/{id}")] - [AllowAnonymous] - public async Task> GetInvoice(string id, string token) + public async Task> GetInvoice(string id) { - var invoice = await _InvoiceRepository.GetInvoice(null, id); + var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() + { + InvoiceId = id, + StoreId = new[] { HttpContext.GetStoreData().Id } + })).FirstOrDefault(); if (invoice == null) throw new BitpayHttpException(404, "Object not found"); var resp = invoice.EntityToDTO(_NetworkProvider); return new DataWrapper(resp); } - [HttpGet] [Route("invoices")] public async Task> GetInvoices( diff --git a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs deleted file mode 100644 index 3ba0a4120..000000000 --- a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BTCPayServer.Filters; -using BTCPayServer.Logging; -using BTCPayServer.Payments; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using NBitcoin; -using NBitcoin.Payment; - -namespace BTCPayServer.Controllers -{ - public partial class InvoiceController - { - [HttpGet] - [Route("i/{invoiceId}/{cryptoCode?}")] - [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")] - public async Task GetInvoiceRequest(string invoiceId, string cryptoCode = null) - { - if (cryptoCode == null) - cryptoCode = "BTC"; - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); - var network = _NetworkProvider.GetNetwork(cryptoCode); - var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); - if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId)) - return NotFound(); - - var dto = invoice.EntityToDTO(_NetworkProvider); - var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId); - PaymentRequest request = new PaymentRequest - { - DetailsVersion = 1 - }; - request.Details.Expires = invoice.ExpirationTime; - request.Details.Memo = invoice.ProductInformation.ItemDesc; - request.Details.Network = network.NBitcoinNetwork; - request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey }); - request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id); - request.Details.Time = DateTimeOffset.UtcNow; - request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute); - - var store = await _StoreRepository.FindStore(invoice.StoreId); - if (store == null) - throw new BitpayHttpException(401, "Unknown store"); - - if (store.StoreCertificate != null) - { - try - { - request.Sign(store.StoreCertificate, PKIType.X509SHA256); - } - catch (Exception ex) - { - Logs.PayServer.LogWarning(ex, "Error while signing payment request"); - } - } - - return new PaymentRequestActionResult(request); - } - - [HttpPost] - [Route("i/{invoiceId}", Order = 99)] - [Route("i/{invoiceId}/{cryptoCode}", Order = 99)] - [MediaTypeConstraint("application/bitcoin-payment")] - public async Task PostPayment(string invoiceId, string cryptoCode = null) - { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); - if (cryptoCode == null) - cryptoCode = "BTC"; - var network = _NetworkProvider.GetNetwork(cryptoCode); - if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike))) - return NotFound(); - - var wallet = _WalletProvider.GetWallet(network); - if (wallet == null) - return NotFound(); - var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork); - var unused = wallet.BroadcastTransactionsAsync(payment.Transactions); - await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork); - return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase...")); - } - } - - - public class PaymentRequestActionResult : IActionResult - { - PaymentRequest req; - public PaymentRequestActionResult(PaymentRequest req) - { - this.req = req; - } - public Task ExecuteResultAsync(ActionContext context) - { - context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary"; - context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest"; - req.WriteTo(context.HttpContext.Response.Body); - return Task.CompletedTask; - } - } - public class PaymentAckActionResult : IActionResult - { - PaymentACK req; - public PaymentAckActionResult(PaymentACK req) - { - this.req = req; - } - public Task ExecuteResultAsync(ActionContext context) - { - context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary"; - context.HttpContext.Response.ContentType = "application/bitcoin-paymentack"; - req.WriteTo(context.HttpContext.Response.Body); - return Task.CompletedTask; - } - } -} diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index fdc983f87..e9e7bcffc 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Filters; +using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Changelly; @@ -30,11 +31,13 @@ namespace BTCPayServer.Controllers { [HttpGet] [Route("invoices/{invoiceId}")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] public async Task Invoice(string invoiceId) { var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() { InvoiceId = invoiceId, + UserId = GetUserId(), IncludeAddresses = true, IncludeEvents = true })).FirstOrDefault(); @@ -209,7 +212,7 @@ namespace BTCPayServer.Controllers private async Task GetInvoiceModel(string invoiceId, string paymentMethodIdStr) { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId); if (invoice == null) return null; var store = await _StoreRepository.FindStore(invoice.StoreId); @@ -369,7 +372,7 @@ namespace BTCPayServer.Controllers { if (!HttpContext.WebSockets.IsWebSocketRequest) return NotFound(); - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId); if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired") return NotFound(); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); @@ -437,15 +440,18 @@ namespace BTCPayServer.Controllers var list = await ListInvoicesProcess(searchTerm, skip, count); foreach (var invoice in list) { + var state = invoice.GetInvoiceState(); model.Invoices.Add(new InvoiceModel() { - Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"), + Status = state.ToString(), ShowCheckout = invoice.Status == "new", Date = invoice.InvoiceTime, InvoiceId = invoice.Id, OrderId = invoice.OrderId ?? string.Empty, RedirectUrl = invoice.RedirectURL ?? string.Empty, - AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}" + AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}", + CanMarkInvalid = state.CanMarkInvalid(), + CanMarkComplete = state.CanMarkComplete() }); } return View(model); @@ -585,17 +591,60 @@ namespace BTCPayServer.Controllers }); } - [HttpPost] - [Route("invoices/invalidatepaid")] + [HttpGet] + [Route("invoices/{invoiceId}/changestate/{newState}")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] - public async Task InvalidatePaidInvoice(string invoiceId) + public IActionResult ChangeInvoiceState(string invoiceId, string newState) { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + if (newState == "invalid") + { + return View("Confirm", new ConfirmModel() + { + Action = "Make invoice invalid", + Title = "Change invoice state", + Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?", + }); + } + else if (newState == "complete") + { + return View("Confirm", new ConfirmModel() + { + Action = "Make invoice complete", + Title = "Change invoice state", + Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?", + ButtonClass = "btn-primary" + }); + } + else + return NotFound(); + } + + [HttpPost] + [Route("invoices/{invoiceId}/changestate/{newState}")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] + [BitpayAPIConstraint(false)] + public async Task ChangeInvoiceStateConfirm(string invoiceId, string newState) + { + var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() + { + InvoiceId = invoiceId, + UserId = GetUserId() + })).FirstOrDefault(); if (invoice == null) return NotFound(); - await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); - _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid")); + if (newState == "invalid") + { + await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); + _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid")); + StatusMessage = "Invoice marked invalid"; + } + else if(newState == "complete") + { + await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId); + _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete")); + StatusMessage = "Invoice marked complete"; + } return RedirectToAction(nameof(ListInvoices)); } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index e44ae8f43..c6b2df2d7 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -435,12 +435,27 @@ namespace BTCPayServer.Controllers }); } } - result.HasSSH = _Options.SSHSettings != null; + foreach(var externalService in _Options.ExternalServices) + { + result.ExternalServices.Add(new ServicesViewModel.ExternalService() + { + Name = externalService.Key, + Link = this.Request.GetRelativePath(externalService.Value) + }); + } + if(_Options.SSHSettings != null) + { + result.ExternalServices.Add(new ServicesViewModel.ExternalService() + { + Name = "SSH", + Link = this.Url.Action(nameof(SSHService)) + }); + } return View(result); } - [Route("server/services/lnd-grpc/{cryptoCode}/{index}")] - public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce) + [Route("server/services/lnd/{cryptoCode}/{index}")] + public IActionResult LndServices(string cryptoCode, int index, uint? nonce) { if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) { @@ -451,9 +466,18 @@ namespace BTCPayServer.Controllers if (external == null) return NotFound(); var model = new LndGrpcServicesViewModel(); + if (external.ConnectionType == LightningConnectionType.LndGRPC) + { + model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}"; + model.SSL = external.BaseUri.Scheme == "https"; + model.ConnectionType = "GRPC"; + } + else if(external.ConnectionType == LightningConnectionType.LndREST) + { + model.Uri = external.BaseUri.AbsoluteUri; + model.ConnectionType = "REST"; + } - model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}"; - model.SSL = external.BaseUri.Scheme == "https"; if (external.CertificateThumbprint != null) { model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint); @@ -462,10 +486,14 @@ namespace BTCPayServer.Controllers { model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon); } + if (external.RestrictedMacaroon != null) + { + model.RestrictedMacaroon = Encoders.Hex.EncodeData(external.RestrictedMacaroon); + } if (nonce != null) { - var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value); + var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value); var lnConfig = _LnConfigProvider.GetConfig(configKey); if (lnConfig != null) { @@ -492,29 +520,44 @@ namespace BTCPayServer.Controllers return Json(conf); } - [Route("server/services/lnd-grpc/{cryptoCode}/{index}")] + [Route("server/services/lnd/{cryptoCode}/{index}")] [HttpPost] - public IActionResult LndGrpcServicesPost(string cryptoCode, int index) + public IActionResult LndServicesPost(string cryptoCode, int index) { var external = GetExternalLndConnectionString(cryptoCode, index); if (external == null) return NotFound(); LightningConfigurations confs = new LightningConfigurations(); - LightningConfiguration conf = new LightningConfiguration(); - conf.Type = "grpc"; - conf.ChainType = _Options.NetworkType.ToString(); - conf.CryptoCode = cryptoCode; - conf.Host = external.BaseUri.DnsSafeHost; - conf.Port = external.BaseUri.Port; - conf.SSL = external.BaseUri.Scheme == "https"; - conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); - conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); - confs.Configurations.Add(conf); - + if (external.ConnectionType == LightningConnectionType.LndGRPC) + { + LightningConfiguration conf = new LightningConfiguration(); + conf.Type = "grpc"; + conf.ChainType = _Options.NetworkType.ToString(); + conf.CryptoCode = cryptoCode; + conf.Host = external.BaseUri.DnsSafeHost; + conf.Port = external.BaseUri.Port; + conf.SSL = external.BaseUri.Scheme == "https"; + conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); + conf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon); + conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); + confs.Configurations.Add(conf); + } + else if (external.ConnectionType == LightningConnectionType.LndREST) + { + var restconf = new LNDRestConfiguration(); + restconf.Type = "lnd-rest"; + restconf.ChainType = _Options.NetworkType.ToString(); + restconf.CryptoCode = cryptoCode; + restconf.Uri = external.BaseUri.AbsoluteUri; + restconf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); + restconf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon); + restconf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); + confs.Configurations.Add(restconf); + } var nonce = RandomUtils.GetUInt32(); - var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce); + var configKey = GetConfigKey("lnd", cryptoCode, index, nonce); _LnConfigProvider.KeepConfig(configKey, confs); - return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce }); + return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce }); } private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index) @@ -536,29 +579,19 @@ namespace BTCPayServer.Controllers return null; } } - return connectionString; - } - - [Route("server/services/lnd-rest/{cryptoCode}/{index}")] - public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce) - { - if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) + if (connectionString.RestrictedMacaroonFilePath != null) { - StatusMessage = $"Error: {cryptoCode} is not fully synched"; - return RedirectToAction(nameof(Services)); + try + { + connectionString.RestrictedMacaroon = System.IO.File.ReadAllBytes(connectionString.RestrictedMacaroonFilePath); + } + catch + { + Logs.Configuration.LogWarning($"{cryptoCode}: The restrictedmacaroon file path of the external LND grpc config was not found ({connectionString.RestrictedMacaroonFilePath})"); + } + connectionString.RestrictedMacaroonFilePath = null; } - var external = GetExternalLndConnectionString(cryptoCode, index); - if (external == null) - return NotFound(); - var model = new LndRestServicesViewModel(); - - model.BaseApiUrl = external.BaseUri.ToString(); - if (external.CertificateThumbprint != null) - model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint); - if (external.Macaroon != null) - model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon); - - return View(model); + return connectionString; } [Route("server/services/ssh")] diff --git a/BTCPayServer/Data/InvoiceData.cs b/BTCPayServer/Data/InvoiceData.cs index 10d81c26a..65923631a 100644 --- a/BTCPayServer/Data/InvoiceData.cs +++ b/BTCPayServer/Data/InvoiceData.cs @@ -81,5 +81,10 @@ namespace BTCPayServer.Data get; set; } public List PendingInvoices { get; set; } + + public Services.Invoices.InvoiceState GetInvoiceState() + { + return new Services.Invoices.InvoiceState() { Status = Status, ExceptionStatus = ExceptionStatus }; + } } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 8e7b694e4..b9f50431b 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -168,6 +168,15 @@ namespace BTCPayServer request.Path.ToUriComponent()); } + public static string GetRelativePath(this HttpRequest request, string path) + { + if (path.Length > 0 && path[0] != '/') + path = $"/{path}"; + return string.Concat( + request.PathBase.ToUriComponent(), + path); + } + public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl) { bool isRelative = diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 019a6c38b..455050373 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -330,7 +330,7 @@ namespace BTCPayServer.HostedServices { leases.Add(_EventAggregator.Subscribe(async e => { - var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id); + var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id); if (invoice == null) return; List tasks = new List(); @@ -345,6 +345,7 @@ namespace BTCPayServer.HostedServices e.Name == "invoice_paidInFull" || e.Name == "invoice_failedToConfirm" || e.Name == "invoice_markedInvalid" || + e.Name == "invoice_markedComplete" || e.Name == "invoice_failedToConfirm" || e.Name == "invoice_completed" || e.Name == "invoice_expiredPaidPartial" diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index ddddd8698..4abc0acca 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -208,7 +208,7 @@ namespace BTCPayServer.HostedServices private async Task Wait(string invoiceId) { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId); try { var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow; @@ -283,7 +283,7 @@ namespace BTCPayServer.HostedServices loopCount++; try { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) break; var updateContext = new UpdateInvoiceContext(invoice); diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 5dfdd5260..7a4e3fb9c 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -46,6 +46,9 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + public bool CanMarkComplete { get; set; } + public bool CanMarkInvalid { get; set; } + public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid; public bool ShowCheckout { get; set; } public string ExceptionStatus { get; set; } public string AmountCurrency diff --git a/BTCPayServer/Models/ServerViewModels/LndGrpcServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/LndGrpcServicesViewModel.cs index 1636b94f1..33bffd72b 100644 --- a/BTCPayServer/Models/ServerViewModels/LndGrpcServicesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/LndGrpcServicesViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; @@ -10,8 +11,12 @@ namespace BTCPayServer.Models.ServerViewModels public string Host { get; set; } public bool SSL { get; set; } public string Macaroon { get; set; } + public string RestrictedMacaroon { get; set; } public string CertificateThumbprint { get; set; } public string QRCode { get; set; } public string QRCodeLink { get; set; } + [Display(Name = "REST Uri")] + public string Uri { get; set; } + public string ConnectionType { get; internal set; } } } diff --git a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs index e103bd6bf..e3ddab4ea 100644 --- a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs @@ -15,7 +15,13 @@ namespace BTCPayServer.Models.ServerViewModels public int Index { get; set; } } + public class ExternalService + { + public string Name { get; set; } + public string Link { get; set; } + } + public List LNDServices { get; set; } = new List(); - public bool HasSSH { get; set; } + public List ExternalServices { get; set; } = new List(); } } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 7ae9d6880..ca0860629 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -205,7 +205,7 @@ namespace BTCPayServer.Payments.Bitcoin async Task UpdatePaymentStates(BTCPayWallet wallet, string invoiceId) { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId, false); if (invoice == null) return null; List updatedPaymentEntities = new List(); @@ -315,7 +315,7 @@ namespace BTCPayServer.Payments.Bitcoin var invoices = await _InvoiceRepository.GetPendingInvoices(); foreach (var invoiceId in invoices) { - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) continue; var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet(); diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index dcfa57541..1fef6f9c6 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning { if (Listening(invoiceId)) return; - var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + var invoice = await _InvoiceRepository.GetInvoice(invoiceId); foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider) .Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) { @@ -194,7 +194,7 @@ namespace BTCPayServer.Payments.Lightning }, network.CryptoCode, accounted: true); if (payment != null) { - var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId); + var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId); if (invoice != null) _Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment")); } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index 176290c25..8caa253aa 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -28,11 +28,12 @@ "BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/", "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;macaroonfilepath=D:\\admin.macaroon", + "BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", "ASPNETCORE_ENVIRONMENT": "Development", "BTCPAY_CHAINS": "btc,ltc", - "BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver" + "BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver", + "BTCPAY_EXTERNALSERVICES": "totoservice:totolink;" }, "applicationUrl": "https://localhost:14142/" } diff --git a/BTCPayServer/Security/BitpayAuthentication.cs b/BTCPayServer/Security/BitpayAuthentication.cs index beb769a66..e71c9f2db 100644 --- a/BTCPayServer/Security/BitpayAuthentication.cs +++ b/BTCPayServer/Security/BitpayAuthentication.cs @@ -57,43 +57,37 @@ namespace BTCPayServer.Security List claims = new List(); var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth(); string storeId = null; - // Careful, those are not the opposite. failedAuth says if a the tentative failed. - // successAuth, ensure that at least one succeed. - var failedAuth = false; - var successAuth = false; + + bool? success = null; if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) { var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims); storeId = result.StoreId; - failedAuth = !result.SuccessAuth; - successAuth = result.SuccessAuth; + success = result.SuccessAuth; } else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) { storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization); - if (storeId == null) - { - Logs.PayServer.LogDebug("API key check failed"); - failedAuth = true; - } - successAuth = storeId != null; + success = storeId != null; } - if (failedAuth) + if (success.HasValue) { - return AuthenticateResult.Fail("Invalid credentials"); - } - - if (successAuth) - { - if (storeId != null) + if (success.Value) { - claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId)); - var store = await _StoreRepository.FindStore(storeId); - store.AdditionalClaims.AddRange(claims); - Context.Request.HttpContext.SetStoreData(store); + if (storeId != null) + { + claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId)); + var store = await _StoreRepository.FindStore(storeId); + store.AdditionalClaims.AddRange(claims); + Context.Request.HttpContext.SetStoreData(store); + } + return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication)); + } + else + { + return AuthenticateResult.Fail("Invalid credentials"); } - return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication)); } } return AuthenticateResult.NoResult(); @@ -148,6 +142,10 @@ namespace BTCPayServer.Security storeId = bitToken.StoreId; } } + else + { + return (storeId, false); + } } catch (FormatException) { } return (storeId, true); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 9adec9791..b37bf0868 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -395,9 +395,6 @@ namespace BTCPayServer.Services.Invoices var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() { - BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", - BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", - BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"), BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", }; } @@ -529,6 +526,42 @@ namespace BTCPayServer.Services.Invoices } #pragma warning restore CS0618 } + + public InvoiceState GetInvoiceState() + { + return new InvoiceState() { Status = Status, ExceptionStatus = ExceptionStatus }; + } + } + + public class InvoiceState + { + public string Status { get; set; } + public string ExceptionStatus { get; set; } + public bool CanMarkComplete() + { + return (Status == "paid") || +#pragma warning disable CA1305 // Specify IFormatProvider + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidPartial") || + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidLate") || + (Status != "complete" && ExceptionStatus?.ToString() == "marked") || + (Status == "invalid"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + + public bool CanMarkInvalid() + { + return (Status == "paid") || + (Status == "new") || +#pragma warning disable CA1305 // Specify IFormatProvider + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidPartial") || + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidLate") || + (Status != "invalid" && ExceptionStatus?.ToString() == "marked"); +#pragma warning restore CA1305 // Specify IFormatProvider; + } + public override string ToString() + { + return Status + (ExceptionStatus == null ? string.Empty : $" ({ExceptionStatus})"); + } } public class PaymentMethodAccounting diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 34d78ebd6..0a846f10d 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -334,13 +334,26 @@ namespace BTCPayServer.Services.Invoices using (var context = _ContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData?.Status != "paid") + if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid()) return; invoiceData.Status = "invalid"; + invoiceData.ExceptionStatus = "marked"; await context.SaveChangesAsync().ConfigureAwait(false); } } - public async Task GetInvoice(string storeId, string id, bool inludeAddressData = false) + public async Task UpdatePaidInvoiceToComplete(string invoiceId) + { + using (var context = _ContextFactory.CreateContext()) + { + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete()) + return; + invoiceData.Status = "complete"; + invoiceData.ExceptionStatus = "marked"; + await context.SaveChangesAsync().ConfigureAwait(false); + } + } + public async Task GetInvoice(string id, bool inludeAddressData = false) { using (var context = _ContextFactory.CreateContext()) { @@ -353,9 +366,6 @@ namespace BTCPayServer.Services.Invoices query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices); query = query.Where(i => i.Id == id); - if (storeId != null) - query = query.Where(i => i.StoreDataId == storeId); - var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false); if (invoice == null) return null; diff --git a/BTCPayServer/Services/LightningConfigurationProvider.cs b/BTCPayServer/Services/LightningConfigurationProvider.cs index c2e6f26a5..cb7842af3 100644 --- a/BTCPayServer/Services/LightningConfigurationProvider.cs +++ b/BTCPayServer/Services/LightningConfigurationProvider.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Services public class LightningConfigurations { - public List Configurations { get; set; } = new List(); + public List Configurations { get; set; } = new List(); } public class LightningConfiguration { @@ -51,5 +51,16 @@ namespace BTCPayServer.Services public bool SSL { get; set; } public string CertificateThumbprint { get; set; } public string Macaroon { get; set; } + public string RestrictedMacaroon { get; set; } + } + public class LNDRestConfiguration + { + public string ChainType { get; set; } + public string Type { get; set; } + public string CryptoCode { get; set; } + public string Uri { get; set; } + public string Macaroon { get; set; } + public string CertificateThumbprint { get; set; } + public string RestrictedMacaroon { get; set; } } } diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 9c1061389..a415e5706 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -99,23 +99,7 @@ } @invoice.InvoiceId - @if (invoice.Status == "paid") - { - - - - @invoice.Status Toggle Dropdown - - - Make Invalid - - - - } - else - { - @invoice.Status - } + @invoice.Status @invoice.AmountCurrency @if (invoice.ShowCheckout) @@ -123,9 +107,32 @@ Checkout [^] - - + @if (!invoice.CanMarkStatus) + { + - + } } + @if (invoice.CanMarkStatus) + { + + Change status Toggle Dropdown + + + @if (invoice.CanMarkInvalid) + { + + Mark as invalid + + } + @if (invoice.CanMarkComplete) + { + + Mark as complete + + } + + } Details @@ -153,28 +160,3 @@ - - - - - - - - - - - Set Invoice status to Invalid - × - - - Are you sure you want to invalidate this transaction? This action is NOT undoable! - - - - - - - diff --git a/BTCPayServer/Views/Server/LndRestServices.cshtml b/BTCPayServer/Views/Server/LndRestServices.cshtml deleted file mode 100644 index 0b0b01523..000000000 --- a/BTCPayServer/Views/Server/LndRestServices.cshtml +++ /dev/null @@ -1,41 +0,0 @@ -@model LndRestServicesViewModel -@{ - ViewData.SetActivePageAndTitle(ServerNavPages.Services); -} - - -LND REST - - - - - - - - - - - - BTCPay exposes LND Rest services for outside consumption. See connection information below - - - - Base API Url - - - @if (Model.Macaroon != null) - { - - - - - } - @if (Model.CertificateThumbprint != null) - { - - - - - } - - diff --git a/BTCPayServer/Views/Server/LndGrpcServices.cshtml b/BTCPayServer/Views/Server/LndServices.cshtml similarity index 67% rename from BTCPayServer/Views/Server/LndGrpcServices.cshtml rename to BTCPayServer/Views/Server/LndServices.cshtml index a23b68e5a..ff0fbe69f 100644 --- a/BTCPayServer/Views/Server/LndGrpcServices.cshtml +++ b/BTCPayServer/Views/Server/LndServices.cshtml @@ -4,7 +4,7 @@ } -LND GRPC +LND @Model.ConnectionType @@ -17,14 +17,14 @@ - BTCPay exposes gRPC services for outside consumption, you will find connection information here. + BTCPay exposes LND's @Model.ConnectionType service for outside consumption, you will find connection information here. QR Code connection - You can use this QR Code to connect your Zap wallet to your LND instance. + You can use this QR Code to connect external software to your LND instance. This QR Code is only valid for 10 minutes @@ -61,22 +61,39 @@ Alternatively, you can see the settings by clicking here - - - - - - - - - @if(Model.Macaroon != null) + @if (Model.Uri == null) + { + + + + + + + + + } + else + { + + + + + } + @if (Model.Macaroon != null) { } - @if(Model.CertificateThumbprint != null) + @if (Model.RestrictedMacaroon != null) + { + + + + + } + @if (Model.CertificateThumbprint != null) { diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml index 7451fb2d5..2eb1b6565 100644 --- a/BTCPayServer/Views/Server/Services.cshtml +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -15,8 +15,9 @@ + Crypto services - You can get access here to LND (gRPC, Rest) or SSH services exposed by your server + You can get access here to LND (gRPC, Rest) services exposed by your server @@ -37,31 +38,54 @@ @if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC) { - See information + See information } else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest) { - See information + See information } } - @if (Model.HasSSH) - { - - None - SSH - - See information - - - } +@if (Model.ExternalServices.Count != 0) +{ + + + Other services + + Other external services + + + + + + Name + Actions + + + + @foreach (var s in Model.ExternalServices) + { + + @s.Name + + See information + + + } + + + + + +} + @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index c3c0b4ad9..7ea390f8e 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -15,7 +15,7 @@ - @if(!Model.Confirmation) + @if (!Model.Confirmation) { Derivation Scheme @@ -26,13 +26,19 @@ + + Checking if a ledger wallet is connected... + + + Please validate access on your screen... + No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page. A ledger wallet is detected, which account do you want to use? - @for(int i = 0; i < 4; i++) + @for (int i = 0; i < 4; i++) { Account @i (@Model.RootKeyPath.Derive(i, true)) } @@ -100,7 +106,7 @@ - @foreach(var sample in Model.AddressSamples) + @foreach (var sample in Model.AddressSamples) { @sample.KeyPath diff --git a/BTCPayServer/Views/Stores/AddLightningNode.cshtml b/BTCPayServer/Views/Stores/AddLightningNode.cshtml index 8ff89300d..3437fd2c0 100644 --- a/BTCPayServer/Views/Stores/AddLightningNode.cshtml +++ b/BTCPayServer/Views/Stores/AddLightningNode.cshtml @@ -7,6 +7,24 @@ @ViewData["Title"] + + × + + Before you proceed, please understand that Lightning Network is still in the experimental stage. Do not put the money you can't afford to lose. There is a high risk of you losing the money. + + + Take time to familiarize yourself with the risk. There's no backup for LND or c-lightning keys in BTCPay. Your keys are in a hot-wallet. This means : + + + Most of BTCPay Server deployments run on a pruned node, which, while working, is not officially supported by lightning network implementations. + If you erase your BTCPay Server virtual machine - you lose all the funds. + If your server gets hacked - a hacker can take all of your funds by accessing your keys. + If there is a bug in a lightning network implementation - you might lose all the funds. + You approve being #reckless and being the sole responsible party for your loss. + You approve to keep on your lightning node only what you can afford to lose. + + + @@ -54,7 +72,7 @@ - @if(Model.InternalLightningNode != null) + @if (Model.InternalLightningNode != null) { You can use the internal lightning node by clicking here diff --git a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js index 9a96571a1..2c9cdf0c6 100644 --- a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js +++ b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js @@ -20,22 +20,32 @@ function WriteAlert(type, message) { } - + function showFeedback(id) { + $("#ledger-loading").css("display", id === "ledger-loading" ? "block" : "none"); + $("#no-ledger-info").css("display", id === "no-ledger-info" ? "block" : "none"); + $("#ledger-validate").css("display", id === "ledger-validate" ? "block" : "none"); + $("#ledger-info").css("display", id === "ledger-info" ? "block" : "none"); + } function Write(prefix, type, message) { if (type === "error") { - $("#no-ledger-info").css("display", "block"); - $("#ledger-in fo").css("display", "none"); + showFeedback("no-ledger-info"); } } $(".ledger-info-recommended").on("click", function (elem) { elem.preventDefault(); + + showFeedback("ledger-validate"); + var account = elem.currentTarget.getAttribute("data-ledgeraccount"); var cryptoCode = GetSelectedCryptoCode(); bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&account=" + account) .then(function (result) { if (cryptoCode !== GetSelectedCryptoCode()) return; + + showFeedback("ledger-info"); + $("#DerivationScheme").val(result.extPubKey); $("#DerivationSchemeFormat").val("BTCPay"); }) @@ -60,8 +70,7 @@ } else { Write('check', 'success', 'This store is configured to use your ledger'); - $("#no-ledger-info").css("display", "none"); - $("#ledger-info").css("display", "block"); + showFeedback("ledger-info"); } }); }; diff --git a/BTCPayServer/wwwroot/locales/es-ES.json b/BTCPayServer/wwwroot/locales/es-ES.json index 4209dcffa..b90d88808 100644 --- a/BTCPayServer/wwwroot/locales/es-ES.json +++ b/BTCPayServer/wwwroot/locales/es-ES.json @@ -33,7 +33,7 @@ "What happened?": "¿Qué sucedió?", "InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \nPuedes regresar a {{storeName}} si deseas volver a enviar tu pago.", "InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.", - "InvoiceExpired_Body_3": "", + "InvoiceExpired_Body_3": "Si recibimos el pago despues, procesaremos tu orden o te contactaremos para un reembolso...", "Invoice ID": "ID de la factura", "Order ID": "ID del pedido", "Return to StoreName": "Regresar a {{storeName}}", diff --git a/BTCPayServer/wwwroot/locales/sr.json b/BTCPayServer/wwwroot/locales/sr.json index 1f9429a66..efa38bc13 100644 --- a/BTCPayServer/wwwroot/locales/sr.json +++ b/BTCPayServer/wwwroot/locales/sr.json @@ -33,7 +33,7 @@ "What happened?": "Šta se desilo?", "InvoiceExpired_Body_1": "Ovaj račun je istekao. Račun važi samo {{maxTimeMinutes}} minuta. \nMožete se vratiti na {{storeName}} ukoliko želite da ponovo inicirate plaćanje.", "InvoiceExpired_Body_2": "Ako ste pokušali da pošaljete uplatu, još uvek nije prihvaćena na mreži. Nismo još zaprimili Vašu uplatu.", - "InvoiceExpired_Body_3": "", + "InvoiceExpired_Body_3": "Ukoliko vašu uplatu primimo kasnije, procesuiraćemo vašu narudžbinu ili ćemo vas kontaktirati za povraćaj novca...", "Invoice ID": "Broj računa", "Order ID": "Broj narudžbine", "Return to StoreName": "Vrati se na {{storeName}}",
Are you sure you want to invalidate this transaction? This action is NOT undoable!
BTCPay exposes LND Rest services for outside consumption. See connection information below
- BTCPay exposes gRPC services for outside consumption, you will find connection information here. + BTCPay exposes LND's @Model.ConnectionType service for outside consumption, you will find connection information here.
- You can use this QR Code to connect your Zap wallet to your LND instance. + You can use this QR Code to connect external software to your LND instance. This QR Code is only valid for 10 minutes
Alternatively, you can see the settings by clicking here
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
+ Before you proceed, please understand that Lightning Network is still in the experimental stage. Do not put the money you can't afford to lose. There is a high risk of you losing the money. +
+ Take time to familiarize yourself with the risk. There's no backup for LND or c-lightning keys in BTCPay. Your keys are in a hot-wallet. This means : +
You can use the internal lightning node by clicking here diff --git a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js index 9a96571a1..2c9cdf0c6 100644 --- a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js +++ b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js @@ -20,22 +20,32 @@ function WriteAlert(type, message) { } - + function showFeedback(id) { + $("#ledger-loading").css("display", id === "ledger-loading" ? "block" : "none"); + $("#no-ledger-info").css("display", id === "no-ledger-info" ? "block" : "none"); + $("#ledger-validate").css("display", id === "ledger-validate" ? "block" : "none"); + $("#ledger-info").css("display", id === "ledger-info" ? "block" : "none"); + } function Write(prefix, type, message) { if (type === "error") { - $("#no-ledger-info").css("display", "block"); - $("#ledger-in fo").css("display", "none"); + showFeedback("no-ledger-info"); } } $(".ledger-info-recommended").on("click", function (elem) { elem.preventDefault(); + + showFeedback("ledger-validate"); + var account = elem.currentTarget.getAttribute("data-ledgeraccount"); var cryptoCode = GetSelectedCryptoCode(); bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&account=" + account) .then(function (result) { if (cryptoCode !== GetSelectedCryptoCode()) return; + + showFeedback("ledger-info"); + $("#DerivationScheme").val(result.extPubKey); $("#DerivationSchemeFormat").val("BTCPay"); }) @@ -60,8 +70,7 @@ } else { Write('check', 'success', 'This store is configured to use your ledger'); - $("#no-ledger-info").css("display", "none"); - $("#ledger-info").css("display", "block"); + showFeedback("ledger-info"); } }); }; diff --git a/BTCPayServer/wwwroot/locales/es-ES.json b/BTCPayServer/wwwroot/locales/es-ES.json index 4209dcffa..b90d88808 100644 --- a/BTCPayServer/wwwroot/locales/es-ES.json +++ b/BTCPayServer/wwwroot/locales/es-ES.json @@ -33,7 +33,7 @@ "What happened?": "¿Qué sucedió?", "InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \nPuedes regresar a {{storeName}} si deseas volver a enviar tu pago.", "InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.", - "InvoiceExpired_Body_3": "", + "InvoiceExpired_Body_3": "Si recibimos el pago despues, procesaremos tu orden o te contactaremos para un reembolso...", "Invoice ID": "ID de la factura", "Order ID": "ID del pedido", "Return to StoreName": "Regresar a {{storeName}}", diff --git a/BTCPayServer/wwwroot/locales/sr.json b/BTCPayServer/wwwroot/locales/sr.json index 1f9429a66..efa38bc13 100644 --- a/BTCPayServer/wwwroot/locales/sr.json +++ b/BTCPayServer/wwwroot/locales/sr.json @@ -33,7 +33,7 @@ "What happened?": "Šta se desilo?", "InvoiceExpired_Body_1": "Ovaj račun je istekao. Račun važi samo {{maxTimeMinutes}} minuta. \nMožete se vratiti na {{storeName}} ukoliko želite da ponovo inicirate plaćanje.", "InvoiceExpired_Body_2": "Ako ste pokušali da pošaljete uplatu, još uvek nije prihvaćena na mreži. Nismo još zaprimili Vašu uplatu.", - "InvoiceExpired_Body_3": "", + "InvoiceExpired_Body_3": "Ukoliko vašu uplatu primimo kasnije, procesuiraćemo vašu narudžbinu ili ćemo vas kontaktirati za povraćaj novca...", "Invoice ID": "Broj računa", "Order ID": "Broj narudžbine", "Return to StoreName": "Vrati se na {{storeName}}",