using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Data.Payouts.LightningLike; using BTCPayServer.Events; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using LNURL; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using NBitcoin; using Newtonsoft.Json; using LightningAddressData = BTCPayServer.Data.LightningAddressData; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; namespace BTCPayServer { [Route("~/{cryptoCode}/[controller]/")] [Route("~/{cryptoCode}/lnurl/")] public class UILNURLController : Controller { private readonly InvoiceRepository _invoiceRepository; private readonly EventAggregator _eventAggregator; private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; private readonly StoreRepository _storeRepository; private readonly AppService _appService; private readonly UIInvoiceController _invoiceController; private readonly LinkGenerator _linkGenerator; private readonly LightningAddressService _lightningAddressService; private readonly LightningLikePayoutHandler _lightningLikePayoutHandler; private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; public UILNURLController(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, BTCPayNetworkProvider btcPayNetworkProvider, LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository, AppService appService, UIInvoiceController invoiceController, LinkGenerator linkGenerator, LightningAddressService lightningAddressService, LightningLikePayoutHandler lightningLikePayoutHandler, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) { _invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; _btcPayNetworkProvider = btcPayNetworkProvider; _lightningLikePaymentHandler = lightningLikePaymentHandler; _storeRepository = storeRepository; _appService = appService; _invoiceController = invoiceController; _linkGenerator = linkGenerator; _lightningAddressService = lightningAddressService; _lightningLikePayoutHandler = lightningLikePayoutHandler; _pullPaymentHostedService = pullPaymentHostedService; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; } [HttpGet("withdraw/pp/{pullPaymentId}")] public async Task GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken) { var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { return NotFound(); } var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, true); if (!pp.IsRunning() || !pp.IsSupported(pmi)) { return NotFound(); } var blob = pp.GetBlob(); if (!blob.Currency.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase)) { return NotFound(); } var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow); var remaining = progress.Limit - progress.Completed - progress.Awaiting; var request = new LNURLWithdrawRequest { MaxWithdrawable = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC), K1 = pullPaymentId, BalanceCheck = new Uri(Request.GetCurrentUrl()), CurrentBalance = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC), MinWithdrawable = LightMoney.FromUnit( Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining), LightMoneyUnit.BTC), Tag = "withdrawRequest", Callback = new Uri(Request.GetCurrentUrl()), // It's not `pp.GetBlob().Description` because this would be HTML // and LNUrl UI's doesn't expect HTML there DefaultDescription = pp.GetBlob().Name ?? string.Empty, }; if (pr is null) { return Ok(request); } if (!BOLT11PaymentRequest.TryParse(pr, out var result, network.NBitcoinNetwork) || result is null) { return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request was not a valid BOLT11" }); } if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable) return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" }); var store = await _storeRepository.FindStore(pp.StoreId); var pm = store!.GetSupportedPaymentMethods(_btcPayNetworkProvider) .OfType() .FirstOrDefault(method => method.PaymentId == pmi); if (pm is null) { return NotFound(); } var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest() { Destination = new BoltInvoiceClaimDestination(pr, result), PaymentMethodId = pmi, PullPaymentId = pullPaymentId, StoreId = pp.StoreId, Value = result.MinimumAmount.ToDecimal(LightMoneyUnit.BTC) }); if (claimResponse.Result != ClaimRequest.ClaimResult.Ok) return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); switch (claimResponse.PayoutData.State) { case PayoutState.AwaitingPayment: { var client = _lightningLikePaymentHandler.CreateLightningClient(pm, network); var payResult = await UILightningLikePayoutController.TrypayBolt(client, claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings), claimResponse.PayoutData, result, pmi, cancellationToken); switch (payResult.Result) { case PayResult.Ok: case PayResult.Unknown: await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest { PayoutId = claimResponse.PayoutData.Id, State = claimResponse.PayoutData.State, Proof = claimResponse.PayoutData.GetProofBlobJson() }); return Ok(new LNUrlStatusResponse { Status = "OK", Reason = payResult.Message }); case PayResult.CouldNotFindRoute: case PayResult.Error: default: await _pullPaymentHostedService.Cancel( new PullPaymentHostedService.CancelRequest(new [] { claimResponse.PayoutData.Id }, null)); return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = payResult.Message ?? payResult.Result.ToString() }); } } case PayoutState.AwaitingApproval: return Ok(new LNUrlStatusResponse { Status = "OK", Reason = "The payment request has been recorded, but still needs to be approved before execution." }); case PayoutState.InProgress: case PayoutState.Completed: return Ok(new LNUrlStatusResponse { Status = "OK" }); case PayoutState.Cancelled: return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); } return Ok(request); } [HttpGet("pay/app/{appId}/{itemCode}")] public async Task GetLNURLForApp(string cryptoCode, string appId, string itemCode = null) { var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { return NotFound(); } var app = await _appService.GetApp(appId, null, true); if (app is null) { return NotFound(); } var store = app.StoreData; if (store is null) { return NotFound(); } if (string.IsNullOrEmpty(itemCode)) { return NotFound(); } var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider); var lnUrlMethod = methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod; var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi); if (lnUrlMethod is null || lnMethod is null) { return NotFound(); } ViewPointOfSaleViewModel.Item[] items = null; string currencyCode = null; switch (app.AppType) { case nameof(AppType.Crowdfund): var cfS = app.GetSettings(); currencyCode = cfS.TargetCurrency; items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency); break; case nameof(AppType.PointOfSale): var posS = app.GetSettings(); currencyCode = posS.Currency; items = _appService.Parse(posS.Template, posS.Currency); break; } var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode); var item = items.FirstOrDefault(item1 => item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) || item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase)); if (item is null || item.Inventory <= 0 || (item.PaymentMethods?.Any() is true && item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false)) { return NotFound(); } return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null, () => (null, app, item, new List { AppService.GetAppInternalTag(appId) }, item.Price.Value, true)); } public class EditLightningAddressVM { public class EditLightningAddressItem : LightningAddressSettings.LightningAddressItem { [Required] [RegularExpression("[a-zA-Z0-9-_]+")] public string Username { get; set; } } public EditLightningAddressItem Add { get; set; } public List Items { get; set; } = new(); } public class LightningAddressSettings { public class LightningAddressItem { public string StoreId { get; set; } [Display(Name = "Invoice currency")] public string CurrencyCode { get; set; } [Display(Name = "Min sats")] [Range(1, double.PositiveInfinity)] public decimal? Min { get; set; } [Display(Name = "Max sats")] [Range(1, double.PositiveInfinity)] public decimal? Max { get; set; } } public ConcurrentDictionary Items { get; set; } = new ConcurrentDictionary(); public ConcurrentDictionary StoreToItemMap { get; set; } = new ConcurrentDictionary(); public override string ToString() { return null; } } [HttpGet("~/.well-known/lnurlp/{username}")] [EnableCors(CorsPolicies.All)] [IgnoreAntiforgeryToken] public async Task ResolveLightningAddress(string username) { var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username); if (lightningAddressSettings is null) { return NotFound("Unknown username"); } var blob = lightningAddressSettings.GetBlob(); return await GetLNURL("BTC", lightningAddressSettings.StoreDataId, blob.CurrencyCode, blob.Min, blob.Max, () => (username, null, null, null, null, true)); } [HttpGet("pay")] public async Task GetLNURL(string cryptoCode, string storeId, string currencyCode = null, decimal? min = null, decimal? max = null, Func<(string username, AppData app, ViewPointOfSaleViewModel.Item item, List additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)> internalDetails = null) { var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { return NotFound("This network does not support Lightning"); } var store = await _storeRepository.FindStore(storeId); if (store is null) { return NotFound("Store not found"); } var storeBlob = store.GetStoreBlob(); currencyCode ??= storeBlob.DefaultCurrency ?? cryptoCode; var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider); var lnUrlMethod = methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod; var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi); if (lnUrlMethod is null || lnMethod is null) { return NotFound("LNURL or Lightning payment method not found"); } var blob = store.GetStoreBlob(); if (blob.GetExcludedPaymentMethods().Match(pmi) || blob.GetExcludedPaymentMethods().Match(lnpmi)) { return NotFound("LNURL or Lightning payment method disabled"); } (string username, AppData app, ViewPointOfSaleViewModel.Item item, List additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice) = (internalDetails ?? (() => (null, null, null, null, null, null)))(); if ((anyoneCanInvoice ?? blob.AnyoneCanInvoice) is false) { return NotFound(); } var lnAddress = username is null ? null : $"{username}@{Request.Host}"; List lnurlMetadata = new(); var redirectUrl = app?.AppType switch { nameof(AppType.PointOfSale) => app.GetSettings().RedirectUrl ?? HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"), _ => null }; var invoiceRequest = new CreateInvoiceRequest { Amount = invoiceAmount, Checkout = new InvoiceDataBase.CheckoutOptions { PaymentMethods = new[] { pmi.ToStringNormalized() }, Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2) ? blob.InvoiceExpiration : TimeSpan.FromMinutes(2), RedirectURL = redirectUrl }, Currency = currencyCode, Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard, }; if (item != null) { invoiceRequest.Metadata = new InvoiceMetadata { ItemCode = item.Id, ItemDesc = item.Description, OrderId = AppService.GetAppOrderId(app) }.ToJObject(); } InvoiceEntity i; try { i = await _invoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags); } catch (Exception e) { return this.CreateAPIError(null, e.Message); } if (i.Type != InvoiceType.TopUp) { min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi); max = item?.Price?.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum ? null : min; } if (!string.IsNullOrEmpty(username)) { var pm = i.GetPaymentMethod(pmi); var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails(); paymentMethodDetails.ConsumedLightningAddress = lnAddress; pm.SetPaymentMethodDetails(paymentMethodDetails); await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm); } var description = blob.LightningDescriptionTemplate .Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); lnurlMetadata.Add(new[] { "text/plain", description }); if (!string.IsNullOrEmpty(username)) { lnurlMetadata.Add(new[] { "text/identifier", lnAddress }); } return Ok(new LNURLPayRequest { Tag = "payRequest", MinSendable = new LightMoney(min ?? 1m, LightMoneyUnit.Satoshi), MaxSendable = max is null ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : new LightMoney(max.Value, LightMoneyUnit.Satoshi), CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0, Metadata = JsonConvert.SerializeObject(lnurlMetadata), Callback = new Uri(_linkGenerator.GetUriByAction( action: nameof(GetLNURLForInvoice), controller: "UILNURL", values: new { cryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase)) }); } [HttpGet("pay/i/{invoiceId}")] [EnableCors(CorsPolicies.All)] [IgnoreAntiforgeryToken] public async Task GetLNURLForInvoice(string invoiceId, string cryptoCode, [FromQuery] long? amount = null, string comment = null) { var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { return NotFound(); } if (comment is not null) comment = comment.Truncate(2000); var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); var i = await _invoiceRepository.GetInvoice(invoiceId, true); var store = await _storeRepository.FindStore(i.StoreId); if (store is null) { return NotFound(); } if (i.Status == InvoiceStatusLegacy.New) { var isTopup = i.IsUnsetTopUp(); var lnurlSupportedPaymentMethod = i.GetSupportedPaymentMethod(pmi).FirstOrDefault(); if (lnurlSupportedPaymentMethod is null) { return NotFound(); } var lightningPaymentMethod = i.GetPaymentMethod(pmi); var accounting = lightningPaymentMethod.Calculate(); var paymentMethodDetails = lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails; if (paymentMethodDetails.LightningSupportedPaymentMethod is null) { return NotFound(); } var amt = amount.HasValue ? new LightMoney(amount.Value) : null; var min = new LightMoney(isTopup ? 1m : accounting.Due.ToUnit(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi); var max = isTopup ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : min; List lnurlMetadata = new(); var blob = store.GetStoreBlob(); var description = blob.LightningDescriptionTemplate .Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); lnurlMetadata.Add(new[] { "text/plain", description }); if (!string.IsNullOrEmpty(paymentMethodDetails.ConsumedLightningAddress)) { lnurlMetadata.Add(new[] { "text/identifier", paymentMethodDetails.ConsumedLightningAddress }); } var metadata = JsonConvert.SerializeObject(lnurlMetadata); if (amt != null && (amt < min || amount > max)) { return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." }); } LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null; if ((i.ReceiptOptions?.Enabled ?? blob.ReceiptOptions.Enabled) is true) { successAction = new LNURLPayRequest.LNURLPayRequestCallbackResponse.LNURLPayRequestSuccessActionUrl { Tag = "url", Description = "Thank you for your purchase. Here is your receipt", Url = _linkGenerator.GetUriByAction( nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId }, Request.Scheme, Request.Host, Request.PathBase) }; } if (amt is null) { return Ok(new LNURLPayRequest { Tag = "payRequest", MinSendable = min, MaxSendable = max, CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0, Metadata = metadata, Callback = new Uri(Request.GetCurrentUrl()) }); } if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt) { var client = _lightningLikePaymentHandler.CreateLightningClient( paymentMethodDetails.LightningSupportedPaymentMethod, network); if (!string.IsNullOrEmpty(paymentMethodDetails.BOLT11)) { try { await client.CancelInvoice(paymentMethodDetails.InvoiceId); } catch (Exception) { //not a fully supported option } } LightningInvoice invoice; try { var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow; var param = new CreateInvoiceParams(amt, metadata, expiry) { PrivateRouteHints = blob.LightningPrivateRouteHints, DescriptionHashOnly = true }; invoice = await client.CreateInvoice(param); if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork) .VerifyDescriptionHash(metadata)) { return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Lightning node could not generate invoice with a valid description hash" }); } } catch (Exception ex) { return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Lightning node could not generate invoice with description hash" + ( string.IsNullOrEmpty(ex.Message) ? "" : $": {ex.Message}") }); } paymentMethodDetails.BOLT11 = invoice.BOLT11; paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash); paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage); paymentMethodDetails.InvoiceId = invoice.Id; paymentMethodDetails.GeneratedBoltAmount = amt; if (lnurlSupportedPaymentMethod.LUD12Enabled) { paymentMethodDetails.ProvidedComment = comment; } lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); _eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, paymentMethodDetails, pmi)); return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse { Disposable = true, Routes = Array.Empty(), Pr = paymentMethodDetails.BOLT11, SuccessAction = successAction }); } if (paymentMethodDetails.GeneratedBoltAmount == amt) { if (lnurlSupportedPaymentMethod.LUD12Enabled && paymentMethodDetails.ProvidedComment != comment) { paymentMethodDetails.ProvidedComment = comment; lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); } return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse { Disposable = true, Routes = Array.Empty(), Pr = paymentMethodDetails.BOLT11, SuccessAction = successAction }); } } return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Invoice not in a valid payable state" }); } [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [HttpGet("~/stores/{storeId}/plugins/lightning-address")] public async Task EditLightningAddress(string storeId) { if (ControllerContext.HttpContext.GetStoreData().GetEnabledPaymentIds(_btcPayNetworkProvider) .All(id => id.PaymentType != LNURLPayPaymentType.Instance)) { TempData.SetStatusMessageModel(new StatusMessageModel { Message = "LNURL is required for lightning addresses but has not yet been enabled.", Severity = StatusMessageModel.StatusSeverity.Error }); return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId }); } var addresses = await _lightningAddressService.Get(new LightningAddressQuery() { StoreIds = new[] { storeId } }); return View(new EditLightningAddressVM { Items = addresses.Select(s => { var blob = s.GetBlob(); return new EditLightningAddressVM.EditLightningAddressItem { Max = blob.Max, Min = blob.Min, CurrencyCode = blob.CurrencyCode, StoreId = storeId, Username = s.Username, }; } ).ToList() }); } [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [HttpPost("~/stores/{storeId}/plugins/lightning-address")] public async Task EditLightningAddress(string storeId, [FromForm] EditLightningAddressVM vm, string command, [FromServices] CurrencyNameTable currencyNameTable) { if (command == "add") { if (!string.IsNullOrEmpty(vm.Add.CurrencyCode) && currencyNameTable.GetCurrencyData(vm.Add.CurrencyCode, false) is null) { vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, "Currency is invalid", this); } if (!ModelState.IsValid) { return View(vm); } if (await _lightningAddressService.Set(new LightningAddressData() { StoreDataId = storeId, Username = vm.Add.Username }.SetBlob(new LightningAddressDataBlob() { Max = vm.Add.Max, Min = vm.Add.Min, CurrencyCode = vm.Add.CurrencyCode }))) { TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Success, Message = "Lightning address added successfully." }); } else { vm.AddModelError(addressVm => addressVm.Add.Username, "Username is already taken", this); if (!ModelState.IsValid) { return View(vm); } } return RedirectToAction("EditLightningAddress"); } if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase)) { var index = command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1); if (await _lightningAddressService.Remove(index, storeId)) { TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Success, Message = $"Lightning address {index} removed successfully." }); return RedirectToAction("EditLightningAddress"); } else { vm.AddModelError(addressVm => addressVm.Add.Username, "Username could not be removed", this); if (!ModelState.IsValid) { return View(vm); } } } return View(vm); } } }