Lightning address support (#2804)

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2021-10-29 11:01:16 +02:00
committed by GitHub
parent 25f84d000b
commit fc8a5ff95f
7 changed files with 595 additions and 86 deletions

View File

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