mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Lightning address support (#2804)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
@@ -21,11 +21,13 @@ using BTCPayServer.Payments.Lightning;
|
||||
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.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using Newtonsoft.Json;
|
||||
@@ -103,7 +105,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item[] items = { };
|
||||
ViewPointOfSaleViewModel.Item[] items = null;
|
||||
string currencyCode = null;
|
||||
switch (app.AppType)
|
||||
{
|
||||
@@ -133,6 +135,62 @@ namespace BTCPayServer
|
||||
() => (null, new List<string> { 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<EditLightningAddressItem> Items { get; set; } = new List<EditLightningAddressItem>();
|
||||
}
|
||||
|
||||
public class LightningAddressSettings
|
||||
{
|
||||
public class LightningAddressItem
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
[Display(Name = "Invoice currency")]
|
||||
public string CurrencyCode { get; set; }
|
||||
public string CryptoCode { 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<string, LightningAddressItem> Items { get; set; } =
|
||||
new ConcurrentDictionary<string, LightningAddressItem>();
|
||||
|
||||
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; set; } =
|
||||
new ConcurrentDictionary<string, string[]>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("~/.well-known/lnurlp/{username}")]
|
||||
public async Task<IActionResult> ResolveLightningAddress(string username)
|
||||
{
|
||||
var lightningAddressSettings = await _settingsRepository.GetSettingAsync<LightningAddressSettings>() ??
|
||||
new LightningAddressSettings();
|
||||
if (!lightningAddressSettings.Items.TryGetValue(username.ToLowerInvariant(), out var item))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetLNURL(item.CryptoCode, item.StoreId, item.CurrencyCode, item.Min, item.Max,
|
||||
() => (username, null, null, true));
|
||||
}
|
||||
|
||||
[HttpGet("pay")]
|
||||
public async Task<IActionResult> GetLNURL(string cryptoCode, string storeId, string currencyCode = null,
|
||||
@@ -140,7 +198,6 @@ namespace BTCPayServer
|
||||
Func<(string username, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)>
|
||||
internalDetails = null)
|
||||
{
|
||||
currencyCode ??= cryptoCode;
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network is null || !network.SupportLightning)
|
||||
{
|
||||
@@ -153,6 +210,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
currencyCode ??= store.GetStoreBlob().DefaultCurrency ?? cryptoCode;
|
||||
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
||||
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
||||
@@ -178,6 +236,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var lnAddress = username is null ? null : $"{username}@{Request.Host.ToString()}";
|
||||
List<string[]> lnurlMetadata = new List<string[]>();
|
||||
|
||||
var i = await _invoiceController.CreateInvoiceCoreRaw(
|
||||
@@ -200,7 +259,21 @@ namespace BTCPayServer
|
||||
max = 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);
|
||||
}
|
||||
|
||||
lnurlMetadata.Add(new[] { "text/plain", i.Id });
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
{
|
||||
lnurlMetadata.Add(new[] { "text/identifier", lnAddress });
|
||||
}
|
||||
|
||||
return Ok(new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
@@ -258,6 +331,10 @@ namespace BTCPayServer
|
||||
List<string[]> lnurlMetadata = new List<string[]>();
|
||||
|
||||
lnurlMetadata.Add(new[] { "text/plain", i.Id });
|
||||
if (!string.IsNullOrEmpty(paymentMethodDetails.ConsumedLightningAddress))
|
||||
{
|
||||
lnurlMetadata.Add(new[] { "text/identifier", paymentMethodDetails.ConsumedLightningAddress });
|
||||
}
|
||||
|
||||
var metadata = JsonConvert.SerializeObject(lnurlMetadata);
|
||||
if (amount.HasValue && (amount < min || amount > max))
|
||||
@@ -303,7 +380,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse
|
||||
{
|
||||
@@ -323,6 +400,7 @@ namespace BTCPayServer
|
||||
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
||||
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
|
||||
|
||||
|
||||
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId,
|
||||
paymentMethodDetails, pmi));
|
||||
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
|
||||
@@ -365,5 +443,124 @@ namespace BTCPayServer
|
||||
Status = "ERROR", Reason = "Invoice not in a valid payable state"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("~/stores/{storeId}/integrations/lightning-address")]
|
||||
public async Task<IActionResult> 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("PaymentMethods", "Stores", new { storeId });
|
||||
}
|
||||
var lightningAddressSettings = await _settingsRepository.GetSettingAsync<LightningAddressSettings>() ??
|
||||
new LightningAddressSettings();
|
||||
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var addresses))
|
||||
{
|
||||
return View(new EditLightningAddressVM
|
||||
{
|
||||
Items = addresses.Select(s => new EditLightningAddressVM.EditLightningAddressItem
|
||||
{
|
||||
Max = lightningAddressSettings.Items[s].Max,
|
||||
Min = lightningAddressSettings.Items[s].Min,
|
||||
CurrencyCode = lightningAddressSettings.Items[s].CurrencyCode,
|
||||
CryptoCode = lightningAddressSettings.Items[s].CryptoCode,
|
||||
StoreId = lightningAddressSettings.Items[s].StoreId,
|
||||
Username = s,
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
return View(new EditLightningAddressVM
|
||||
{
|
||||
Items = new List<EditLightningAddressVM.EditLightningAddressItem>()
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("~/stores/{storeId}/integrations/lightning-address")]
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
var lightningAddressSettings = await _settingsRepository.GetSettingAsync<LightningAddressSettings>() ??
|
||||
new LightningAddressSettings();
|
||||
if (lightningAddressSettings.Items.ContainsKey(vm.Add.Username.ToLowerInvariant()))
|
||||
{
|
||||
vm.AddModelError(addressVm => addressVm.Add.Username, "Username is already taken", this);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var ids))
|
||||
{
|
||||
ids = ids.Concat(new[] { vm.Add.Username.ToLowerInvariant() }).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
ids = new[] { vm.Add.Username.ToLowerInvariant() };
|
||||
}
|
||||
|
||||
lightningAddressSettings.StoreToItemMap.AddOrReplace(storeId, ids);
|
||||
vm.Add.StoreId = storeId;
|
||||
vm.Add.CryptoCode = ControllerContext.HttpContext.GetStoreData()
|
||||
.GetEnabledPaymentIds(_btcPayNetworkProvider)
|
||||
.OrderBy(id => id.CryptoCode == "BTC")
|
||||
.First()
|
||||
.CryptoCode;
|
||||
lightningAddressSettings.Items.TryAdd(vm.Add.Username.ToLowerInvariant(), vm.Add);
|
||||
await _settingsRepository.UpdateSetting(lightningAddressSettings);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Lightning address added successfully."
|
||||
});
|
||||
|
||||
return RedirectToAction("EditLightningAddress");
|
||||
}
|
||||
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var lightningAddressSettings = await _settingsRepository.GetSettingAsync<LightningAddressSettings>() ??
|
||||
new LightningAddressSettings();
|
||||
var index = int.Parse(
|
||||
command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
|
||||
CultureInfo.InvariantCulture);
|
||||
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var addresses))
|
||||
{
|
||||
var addressToRemove = addresses[index];
|
||||
addresses = addresses.Where(s => s != addressToRemove).ToArray();
|
||||
lightningAddressSettings.StoreToItemMap.AddOrReplace(storeId, addresses);
|
||||
lightningAddressSettings.Items.TryRemove(addressToRemove, out _);
|
||||
await _settingsRepository.UpdateSetting(lightningAddressSettings);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = $"Lightning address {addressToRemove} removed successfully."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("EditLightningAddress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user