2021-11-05 04:16:54 +01:00
using System ;
2021-10-25 08:18:02 +02:00
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
2022-12-04 13:23:59 +01:00
using System.Threading ;
2021-10-25 08:18:02 +02:00
using System.Threading.Tasks ;
2021-10-29 10:27:33 +02:00
using BTCPayServer.Abstractions.Constants ;
2023-03-29 12:27:04 +02:00
using BTCPayServer.Abstractions.Contracts ;
2021-10-29 10:27:33 +02:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
using BTCPayServer.Client ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Client.Models ;
using BTCPayServer.Controllers ;
using BTCPayServer.Data ;
2022-06-28 16:02:17 +02:00
using BTCPayServer.Data.Payouts.LightningLike ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Events ;
2022-06-28 16:02:17 +02:00
using BTCPayServer.HostedServices ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Lightning ;
2023-04-10 08:06:59 +02:00
using BTCPayServer.Logging ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Payments ;
using BTCPayServer.Payments.Lightning ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Plugins.Crowdfund ;
using BTCPayServer.Plugins.PointOfSale ;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Plugins.PointOfSale.Models ;
2022-12-04 13:23:59 +01:00
using BTCPayServer.Services ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Services.Apps ;
using BTCPayServer.Services.Invoices ;
2021-10-29 11:01:16 +02:00
using BTCPayServer.Services.Rates ;
2021-10-25 08:18:02 +02:00
using BTCPayServer.Services.Stores ;
using LNURL ;
2021-10-29 10:27:33 +02:00
using Microsoft.AspNetCore.Authorization ;
2023-02-02 01:40:31 +01:00
using Microsoft.AspNetCore.Cors ;
2023-02-21 21:06:36 +01:00
using Microsoft.AspNetCore.Http ;
2021-10-25 08:18:02 +02:00
using Microsoft.AspNetCore.Mvc ;
2021-12-31 08:59:02 +01:00
using Microsoft.AspNetCore.Routing ;
2021-10-25 08:18:02 +02:00
using NBitcoin ;
using Newtonsoft.Json ;
2023-04-07 10:48:58 +02:00
using Newtonsoft.Json.Linq ;
2023-01-23 10:11:34 +01:00
using LightningAddressData = BTCPayServer . Data . LightningAddressData ;
2022-11-15 10:40:57 +01:00
using MarkPayoutRequest = BTCPayServer . HostedServices . MarkPayoutRequest ;
2021-10-25 08:18:02 +02:00
namespace BTCPayServer
{
[Route("~/{cryptoCode}/[controller] / ")]
2022-01-07 04:32:00 +01:00
[Route("~/{cryptoCode}/lnurl/")]
public class UILNURLController : Controller
2021-10-25 08:18:02 +02:00
{
private readonly InvoiceRepository _invoiceRepository ;
private readonly EventAggregator _eventAggregator ;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider ;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler ;
private readonly StoreRepository _storeRepository ;
private readonly AppService _appService ;
2022-01-07 04:32:00 +01:00
private readonly UIInvoiceController _invoiceController ;
2021-10-29 10:27:33 +02:00
private readonly LinkGenerator _linkGenerator ;
2022-04-19 09:58:31 +02:00
private readonly LightningAddressService _lightningAddressService ;
2022-06-28 16:02:17 +02:00
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler ;
private readonly PullPaymentHostedService _pullPaymentHostedService ;
2022-12-04 13:23:59 +01:00
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings ;
2023-03-29 12:27:04 +02:00
private readonly IPluginHookService _pluginHookService ;
2021-10-25 08:18:02 +02:00
2022-01-07 04:32:00 +01:00
public UILNURLController ( InvoiceRepository invoiceRepository ,
2021-10-25 08:18:02 +02:00
EventAggregator eventAggregator ,
BTCPayNetworkProvider btcPayNetworkProvider ,
LightningLikePaymentHandler lightningLikePaymentHandler ,
StoreRepository storeRepository ,
AppService appService ,
2022-01-07 04:32:00 +01:00
UIInvoiceController invoiceController ,
2022-04-19 09:58:31 +02:00
LinkGenerator linkGenerator ,
2022-06-28 16:02:17 +02:00
LightningAddressService lightningAddressService ,
LightningLikePayoutHandler lightningLikePayoutHandler ,
2022-12-04 13:23:59 +01:00
PullPaymentHostedService pullPaymentHostedService ,
2023-03-29 12:27:04 +02:00
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings ,
IPluginHookService pluginHookService )
2021-10-25 08:18:02 +02:00
{
_invoiceRepository = invoiceRepository ;
_eventAggregator = eventAggregator ;
_btcPayNetworkProvider = btcPayNetworkProvider ;
_lightningLikePaymentHandler = lightningLikePaymentHandler ;
_storeRepository = storeRepository ;
_appService = appService ;
_invoiceController = invoiceController ;
2021-10-29 10:27:33 +02:00
_linkGenerator = linkGenerator ;
2022-04-19 09:58:31 +02:00
_lightningAddressService = lightningAddressService ;
2022-06-28 16:02:17 +02:00
_lightningLikePayoutHandler = lightningLikePayoutHandler ;
_pullPaymentHostedService = pullPaymentHostedService ;
2022-12-04 13:23:59 +01:00
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings ;
2023-03-29 12:27:04 +02:00
_pluginHookService = pluginHookService ;
2021-10-25 08:18:02 +02:00
}
2021-10-29 10:27:33 +02:00
2022-06-28 16:02:17 +02:00
[HttpGet("withdraw/pp/{pullPaymentId}")]
2022-12-04 13:23:59 +01:00
public async Task < IActionResult > GetLNURLForPullPayment ( string cryptoCode , string pullPaymentId , string pr , CancellationToken cancellationToken )
2022-06-28 16:02:17 +02:00
{
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( 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 ) ;
2023-01-06 14:18:07 +01:00
2022-06-28 16:02:17 +02:00
var remaining = progress . Limit - progress . Completed - progress . Awaiting ;
2023-02-21 21:06:36 +01:00
var request = new LNURLWithdrawRequest
2022-06-28 16:02:17 +02:00
{
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 ( ) ) ,
2023-02-13 15:39:55 +01:00
// It's not `pp.GetBlob().Description` because this would be HTML
// and LNUrl UI's doesn't expect HTML there
2023-02-21 21:06:36 +01:00
DefaultDescription = pp . GetBlob ( ) . Name ? ? string . Empty ,
2022-06-28 16:02:17 +02:00
} ;
if ( pr is null )
{
return Ok ( request ) ;
}
if ( ! BOLT11PaymentRequest . TryParse ( pr , out var result , network . NBitcoinNetwork ) | | result is null )
{
2023-02-21 21:06:36 +01:00
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = "Payment request was not a valid BOLT11" } ) ;
2022-06-28 16:02:17 +02:00
}
if ( result . MinimumAmount < request . MinWithdrawable | | result . MinimumAmount > request . MaxWithdrawable )
2023-02-21 21:06:36 +01:00
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" } ) ;
2022-06-28 16:02:17 +02:00
var store = await _storeRepository . FindStore ( pp . StoreId ) ;
var pm = store ! . GetSupportedPaymentMethods ( _btcPayNetworkProvider )
. OfType < LightningSupportedPaymentMethod > ( )
. 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 )
2023-02-21 21:06:36 +01:00
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = "Payment request could not be paid" } ) ;
2023-03-17 03:56:32 +01:00
2022-06-28 16:02:17 +02:00
switch ( claimResponse . PayoutData . State )
{
case PayoutState . AwaitingPayment :
{
2023-01-06 14:18:07 +01:00
var client =
_lightningLikePaymentHandler . CreateLightningClient ( pm , network ) ;
var payResult = await UILightningLikePayoutController . TrypayBolt ( client ,
claimResponse . PayoutData . GetBlob ( _btcPayNetworkJsonSerializerSettings ) ,
claimResponse . PayoutData , result , pmi , cancellationToken ) ;
2022-06-28 16:02:17 +02:00
2023-01-06 14:18:07 +01:00
switch ( payResult . Result )
{
case PayResult . Ok :
case PayResult . Unknown :
2023-02-21 21:06:36 +01:00
await _pullPaymentHostedService . MarkPaid ( new MarkPayoutRequest
2023-01-06 14:18:07 +01:00
{
PayoutId = claimResponse . PayoutData . Id ,
State = claimResponse . PayoutData . State ,
Proof = claimResponse . PayoutData . GetProofBlobJson ( )
} ) ;
return Ok ( new LNUrlStatusResponse
2022-06-28 16:02:17 +02:00
{
2023-01-06 14:18:07 +01:00
Status = "OK" ,
Reason = payResult . Message
} ) ;
case PayResult . CouldNotFindRoute :
case PayResult . Error :
default :
await _pullPaymentHostedService . Cancel (
2023-04-10 04:07:03 +02:00
new PullPaymentHostedService . CancelRequest ( new [ ]
2023-02-21 21:06:36 +01:00
{ claimResponse . PayoutData . Id } , null ) ) ;
2022-06-28 16:02:17 +02:00
2023-02-21 21:06:36 +01:00
return BadRequest ( new LNUrlStatusResponse
2023-01-06 14:18:07 +01:00
{
Status = "ERROR" ,
2023-02-21 21:06:36 +01:00
Reason = payResult . Message ? ? payResult . Result . ToString ( )
2023-01-06 14:18:07 +01:00
} ) ;
}
2022-06-28 16:02:17 +02:00
}
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 :
2023-01-06 14:18:07 +01:00
return Ok ( new LNUrlStatusResponse { Status = "OK" } ) ;
2022-06-28 16:02:17 +02:00
case PayoutState . Cancelled :
2023-02-21 21:06:36 +01:00
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = "Payment request could not be paid" } ) ;
2022-06-28 16:02:17 +02:00
}
return Ok ( request ) ;
}
2023-02-09 17:45:09 +01:00
2021-10-29 10:27:33 +02:00
[HttpGet("pay/app/{appId}/{itemCode}")]
public async Task < IActionResult > GetLNURLForApp ( string cryptoCode , string appId , string itemCode = null )
{
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( 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 ( ) ;
}
2022-04-19 09:58:31 +02:00
2021-10-29 10:27:33 +02:00
if ( string . IsNullOrEmpty ( itemCode ) )
{
return NotFound ( ) ;
}
2023-03-17 03:56:32 +01:00
ViewPointOfSaleViewModel . Item [ ] items ;
string currencyCode ;
PointOfSaleSettings posS = null ;
2021-10-29 10:27:33 +02:00
switch ( app . AppType )
{
2023-03-20 02:39:26 +01:00
case CrowdfundAppType . AppType :
2021-10-29 10:27:33 +02:00
var cfS = app . GetSettings < CrowdfundSettings > ( ) ;
currencyCode = cfS . TargetCurrency ;
items = _appService . Parse ( cfS . PerksTemplate , cfS . TargetCurrency ) ;
break ;
2023-03-20 02:39:26 +01:00
case PointOfSaleAppType . AppType :
2023-03-17 03:56:32 +01:00
posS = app . GetSettings < PointOfSaleSettings > ( ) ;
2021-10-29 10:27:33 +02:00
currencyCode = posS . Currency ;
items = _appService . Parse ( posS . Template , posS . Currency ) ;
break ;
2023-03-17 03:56:32 +01:00
default :
//TODO: Allow other apps to define lnurl support
return NotFound ( ) ;
2021-10-29 10:27:33 +02:00
}
2023-03-17 03:56:32 +01:00
ViewPointOfSaleViewModel . Item item = null ;
if ( ! string . IsNullOrEmpty ( itemCode ) )
{
2023-04-07 10:48:58 +02:00
var pmi = GetLNUrlPaymentMethodId ( cryptoCode , store , out _ ) ;
if ( pmi is null )
return NotFound ( "LNUrl or LN is disabled" ) ;
2023-03-17 03:56:32 +01:00
var escapedItemId = Extensions . UnescapeBackSlashUriString ( itemCode ) ;
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 ( ) ;
}
}
2023-03-20 02:39:26 +01:00
else if ( app . AppType = = PointOfSaleAppType . AppType & & posS ? . ShowCustomAmount is not true )
2021-10-29 10:27:33 +02:00
{
return NotFound ( ) ;
}
2023-04-07 10:48:58 +02:00
var createInvoice = new CreateInvoiceRequest ( )
{
Amount = item ? . Price . Value ,
Currency = currencyCode ,
Checkout = new InvoiceDataBase . CheckoutOptions ( )
{
RedirectURL = app . AppType switch
{
PointOfSaleAppType . AppType = > app . GetSettings < PointOfSaleSettings > ( ) . RedirectUrl ? ?
HttpContext . Request . GetAbsoluteUri ( $"/apps/{app.Id}/pos" ) ,
_ = > null
}
}
} ;
var invoiceMetadata = new InvoiceMetadata ( ) ;
2023-04-10 04:07:03 +02:00
invoiceMetadata . OrderId = AppService . GetAppOrderId ( app ) ;
2023-04-07 10:48:58 +02:00
if ( item ! = null )
{
invoiceMetadata . ItemCode = item . Id ;
invoiceMetadata . ItemDesc = item . Description ;
}
createInvoice . Metadata = invoiceMetadata . ToJObject ( ) ;
return await GetLNURLRequest (
cryptoCode ,
store ,
store . GetStoreBlob ( ) ,
createInvoice ,
additionalTags : new List < string > { AppService . GetAppInternalTag ( appId ) } ,
allowOverpay : false ) ;
2021-10-29 10:27:33 +02:00
}
2021-10-29 11:01:16 +02:00
public class EditLightningAddressVM
{
public class EditLightningAddressItem : LightningAddressSettings . LightningAddressItem
{
[Required]
[RegularExpression("[a-zA-Z0-9-_] + ")]
public string Username { get ; set ; }
}
public EditLightningAddressItem Add { get ; set ; }
2022-04-19 09:58:31 +02:00
public List < EditLightningAddressItem > Items { get ; set ; } = new ( ) ;
2021-10-29 11:01:16 +02:00
}
public class LightningAddressSettings
{
public class LightningAddressItem
{
public string StoreId { get ; set ; }
2022-04-19 09:58:31 +02:00
[Display(Name = "Invoice currency")] public string CurrencyCode { get ; set ; }
2021-12-31 08:59:02 +01:00
2021-10-29 11:01:16 +02:00
[Display(Name = "Min sats")]
[Range(1, double.PositiveInfinity)]
public decimal? Min { get ; set ; }
2021-12-31 08:59:02 +01:00
2021-10-29 11:01:16 +02:00
[Display(Name = "Max sats")]
[Range(1, double.PositiveInfinity)]
public decimal? Max { get ; set ; }
2023-04-07 10:48:58 +02:00
[Display(Name = "Invoice metadata")]
public string InvoiceMetadata { get ; set ; }
2021-10-29 11:01:16 +02:00
}
2023-04-10 04:07:03 +02:00
public ConcurrentDictionary < string , LightningAddressItem > Items { get ; } = new ( ) ;
public ConcurrentDictionary < string , string [ ] > StoreToItemMap { get ; } = new ( ) ;
2021-10-29 11:01:16 +02:00
public override string ToString ( )
{
return null ;
}
}
[HttpGet("~/.well-known/lnurlp/{username}")]
2023-02-02 01:40:31 +01:00
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
2021-10-29 11:01:16 +02:00
public async Task < IActionResult > ResolveLightningAddress ( string username )
{
2022-04-19 09:58:31 +02:00
var lightningAddressSettings = await _lightningAddressService . ResolveByAddress ( username ) ;
2023-04-07 10:48:58 +02:00
if ( lightningAddressSettings is null | | username is null )
return NotFound ( "Unknown username" ) ;
var store = await _storeRepository . FindStore ( lightningAddressSettings . StoreDataId ) ;
if ( store is null )
2021-11-05 04:16:54 +01:00
return NotFound ( "Unknown username" ) ;
2021-10-29 11:01:16 +02:00
2023-02-21 07:06:34 +01:00
var blob = lightningAddressSettings . GetBlob ( ) ;
2023-04-07 10:48:58 +02:00
return await GetLNURLRequest (
"BTC" ,
store ,
store . GetStoreBlob ( ) ,
new CreateInvoiceRequest ( )
{
Currency = blob ? . CurrencyCode ,
Metadata = blob ? . InvoiceMetadata
} ,
new LNURLPayRequest ( )
{
MinSendable = blob ? . Min is decimal min ? new LightMoney ( min , LightMoneyUnit . Satoshi ) : null ,
MaxSendable = blob ? . Max is decimal max ? new LightMoney ( max , LightMoneyUnit . Satoshi ) : null ,
} ,
new Dictionary < string , string > ( )
{
{ "text/identifier" , $"{username}@{Request.Host}" }
} ) ;
2021-10-29 11:01:16 +02:00
}
2021-10-29 10:27:33 +02:00
2023-04-07 10:48:58 +02:00
2021-10-29 10:27:33 +02:00
[HttpGet("pay")]
2023-04-07 10:48:58 +02:00
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
public async Task < IActionResult > GetLNUrlForStore (
string cryptoCode ,
string storeId ,
string currencyCode = null )
2021-10-29 10:27:33 +02:00
{
2023-04-07 10:48:58 +02:00
var store = this . HttpContext . GetStoreData ( ) ;
2021-10-29 10:27:33 +02:00
if ( store is null )
return NotFound ( ) ;
2023-04-07 10:48:58 +02:00
var blob = store . GetStoreBlob ( ) ;
if ( ! blob . AnyoneCanInvoice )
return NotFound ( "'Anyone can invoice' is turned off" ) ;
return await GetLNURLRequest (
cryptoCode ,
store ,
blob ,
new CreateInvoiceRequest
2021-10-29 10:27:33 +02:00
{
2023-04-07 10:48:58 +02:00
Currency = currencyCode
} ) ;
}
private async Task < IActionResult > GetLNURLRequest (
string cryptoCode ,
Data . StoreData store ,
Data . StoreBlob blob ,
CreateInvoiceRequest createInvoice ,
LNURLPayRequest lnurlRequest = null ,
Dictionary < string , string > lnUrlMetadata = null ,
List < string > additionalTags = null ,
bool allowOverpay = true )
{
if ( GetLNUrlPaymentMethodId ( cryptoCode , store , out _ ) is null )
return NotFound ( "LNUrl or LN is disabled" ) ;
2022-04-24 13:36:10 +02:00
2023-02-08 12:45:05 +01:00
InvoiceEntity i ;
try
{
2023-04-07 10:48:58 +02:00
i = await _invoiceController . CreateInvoiceCoreRaw ( createInvoice , store , Request . GetAbsoluteRoot ( ) , additionalTags ) ;
2023-02-08 12:45:05 +01:00
}
catch ( Exception e )
{
return this . CreateAPIError ( null , e . Message ) ;
}
2023-04-07 10:48:58 +02:00
lnurlRequest = await CreateLNUrlRequestFromInvoice ( cryptoCode , i , store , blob , lnurlRequest , lnUrlMetadata , allowOverpay ) ;
return lnurlRequest is null ? NotFound ( ) : Ok ( lnurlRequest ) ;
}
private async Task < LNURLPayRequest > CreateLNUrlRequestFromInvoice (
string cryptoCode ,
InvoiceEntity i ,
Data . StoreData store ,
StoreBlob blob ,
LNURLPayRequest lnurlRequest = null ,
Dictionary < string , string > lnUrlMetadata = null ,
bool allowOverpay = true )
{
var pmi = GetLNUrlPaymentMethodId ( cryptoCode , store , out var lnUrlMethod ) ;
if ( pmi is null )
return null ;
lnurlRequest ? ? = new LNURLPayRequest ( ) ;
lnUrlMetadata ? ? = new Dictionary < string , string > ( ) ;
2021-10-29 10:27:33 +02:00
2023-04-10 04:07:03 +02:00
if ( lnUrlMetadata ? . TryGetValue ( "text/identifier" , out var lnAddress ) is true & & lnAddress is not null )
2021-10-29 11:01:16 +02:00
{
var pm = i . GetPaymentMethod ( pmi ) ;
var paymentMethodDetails = ( LNURLPayPaymentMethodDetails ) pm . GetPaymentMethodDetails ( ) ;
paymentMethodDetails . ConsumedLightningAddress = lnAddress ;
pm . SetPaymentMethodDetails ( paymentMethodDetails ) ;
await _invoiceRepository . UpdateInvoicePaymentMethod ( i . Id , pm ) ;
}
2023-01-06 14:18:07 +01:00
2023-04-07 10:48:58 +02:00
if ( ! lnUrlMetadata . ContainsKey ( "text/plain" ) )
2021-10-29 11:01:16 +02:00
{
2023-04-07 10:48:58 +02:00
var invoiceDescription = blob . LightningDescriptionTemplate
. Replace ( "{StoreName}" , store . StoreName ? ? "" , StringComparison . OrdinalIgnoreCase )
. Replace ( "{ItemDescription}" , i . Metadata . ItemDesc ? ? "" , StringComparison . OrdinalIgnoreCase )
. Replace ( "{OrderId}" , i . Metadata . OrderId ? ? "" , StringComparison . OrdinalIgnoreCase ) ;
lnUrlMetadata . Add ( "text/plain" , invoiceDescription ) ;
2021-10-29 11:01:16 +02:00
}
2023-03-29 12:27:04 +02:00
2023-04-07 10:48:58 +02:00
lnurlRequest . Tag = "payRequest" ;
lnurlRequest . CommentAllowed = lnUrlMethod . LUD12Enabled ? 2000 : 0 ;
lnurlRequest . Callback = new Uri ( _linkGenerator . GetUriByAction (
2023-03-29 12:27:04 +02:00
action : nameof ( GetLNURLForInvoice ) ,
controller : "UILNURL" ,
2023-04-07 10:48:58 +02:00
values : new { pmi . CryptoCode , invoiceId = i . Id } , Request . Scheme , Request . Host , Request . PathBase ) ) ;
lnurlRequest . Metadata = JsonConvert . SerializeObject ( lnUrlMetadata . Select ( kv = > new [ ] { kv . Key , kv . Value } ) ) ;
if ( i . Type ! = InvoiceType . TopUp )
2023-03-29 12:27:04 +02:00
{
2023-04-07 10:48:58 +02:00
lnurlRequest . MinSendable = new LightMoney ( i . GetPaymentMethod ( pmi ) . Calculate ( ) . Due . ToDecimal ( MoneyUnit . Satoshi ) , LightMoneyUnit . Satoshi ) ;
if ( ! allowOverpay )
lnurlRequest . MaxSendable = lnurlRequest . MinSendable ;
}
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
if ( lnurlRequest . MinSendable is null | | lnurlRequest . MinSendable < LightMoney . Satoshis ( 1.0 m ) )
lnurlRequest . MinSendable = LightMoney . Satoshis ( 1.0 m ) ;
if ( lnurlRequest . MaxSendable is null )
lnurlRequest . MaxSendable = LightMoney . FromUnit ( 6.12 m , LightMoneyUnit . BTC ) ;
lnurlRequest = await _pluginHookService . ApplyFilter ( "modify-lnurlp-request" , lnurlRequest ) as LNURLPayRequest ;
i . Metadata ? ? = new InvoiceMetadata ( ) ;
var metadata = i . Metadata . ToJObject ( ) ;
if ( metadata . Property ( "payRequest" ) is null )
{
metadata . Add ( "payRequest" , JToken . FromObject ( lnurlRequest ) ) ;
await _invoiceRepository . UpdateInvoiceMetadata ( i . Id , i . StoreId , metadata ) ;
2023-03-29 12:27:04 +02:00
}
2023-04-07 10:48:58 +02:00
return lnurlRequest ;
}
PaymentMethodId GetLNUrlPaymentMethodId ( string cryptoCode , Data . StoreData store , out LNURLPaySupportedPaymentMethod lnUrlSettings )
{
lnUrlSettings = null ;
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( cryptoCode ) ;
if ( network is null | | ! network . SupportLightning )
return null ;
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 null ;
var blob = store . GetStoreBlob ( ) ;
if ( blob . GetExcludedPaymentMethods ( ) . Match ( pmi ) | | blob . GetExcludedPaymentMethods ( ) . Match ( lnpmi ) )
return null ;
lnUrlSettings = lnUrlMethod ;
return pmi ;
2021-10-29 10:27:33 +02:00
}
2021-10-25 08:18:02 +02:00
[HttpGet("pay/i/{invoiceId}")]
2023-02-09 17:45:09 +01:00
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
2021-10-25 08:18:02 +02:00
public async Task < IActionResult > GetLNURLForInvoice ( string invoiceId , string cryptoCode ,
[FromQuery] long? amount = null , string comment = null )
{
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( cryptoCode ) ;
if ( network is null | | ! network . SupportLightning )
{
return NotFound ( ) ;
}
2022-04-19 09:58:31 +02:00
2021-10-25 08:18:02 +02:00
var i = await _invoiceRepository . GetInvoice ( invoiceId , true ) ;
2023-04-07 10:48:58 +02:00
if ( i is null )
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 13:36:10 +02:00
var store = await _storeRepository . FindStore ( i . StoreId ) ;
if ( store is null )
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2021-10-25 08:18:02 +02:00
if ( i . Status = = InvoiceStatusLegacy . New )
{
2023-04-07 10:48:58 +02:00
var pmi = GetLNUrlPaymentMethodId ( cryptoCode , store , out var lnurlSupportedPaymentMethod ) ;
if ( pmi is null )
2021-10-25 08:18:02 +02:00
return NotFound ( ) ;
var lightningPaymentMethod = i . GetPaymentMethod ( pmi ) ;
var paymentMethodDetails =
2023-04-11 06:06:09 +02:00
lightningPaymentMethod ? . GetPaymentMethodDetails ( ) as LNURLPayPaymentMethodDetails ;
if ( paymentMethodDetails ? . LightningSupportedPaymentMethod is null )
2021-10-25 08:18:02 +02:00
return NotFound ( ) ;
2022-04-24 13:36:10 +02:00
2023-04-07 10:48:58 +02:00
LNURLPayRequest lnurlPayRequest ;
2022-04-24 13:36:10 +02:00
var blob = store . GetStoreBlob ( ) ;
2023-04-07 10:48:58 +02:00
if ( i . Metadata . AdditionalData . TryGetValue ( "payRequest" , out var t ) & & t is JObject jo )
2021-10-29 11:01:16 +02:00
{
2023-04-07 10:48:58 +02:00
lnurlPayRequest = jo . ToObject < LNURLPayRequest > ( ) ;
2021-10-29 11:01:16 +02:00
}
2023-04-07 10:48:58 +02:00
else
2021-10-25 08:18:02 +02:00
{
2023-04-07 10:48:58 +02:00
lnurlPayRequest = await CreateLNUrlRequestFromInvoice ( cryptoCode , i , store , blob , allowOverpay : false ) ;
if ( lnurlPayRequest is null )
return NotFound ( ) ;
2021-10-25 08:18:02 +02:00
}
2023-04-07 10:48:58 +02:00
if ( amount is null )
return Ok ( lnurlPayRequest ) ;
var amt = new LightMoney ( amount . Value ) ;
if ( amt < lnurlPayRequest . MinSendable | | amount > lnurlPayRequest . MaxSendable )
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = "Amount is out of bounds." } ) ;
2022-07-06 14:14:55 +02:00
LNURLPayRequest . LNURLPayRequestCallbackResponse . ILNURLPayRequestSuccessAction successAction = null ;
2023-01-06 14:18:07 +01:00
if ( ( i . ReceiptOptions ? . Enabled ? ? blob . ReceiptOptions . Enabled ) is true )
2022-07-06 14:14:55 +02:00
{
successAction =
2023-01-05 14:41:18 +01:00
new LNURLPayRequest . LNURLPayRequestCallbackResponse . LNURLPayRequestSuccessActionUrl
2022-07-06 14:14:55 +02:00
{
Tag = "url" ,
Description = "Thank you for your purchase. Here is your receipt" ,
2023-02-21 21:06:36 +01:00
Url = _linkGenerator . GetUriByAction (
2023-04-07 10:48:58 +02:00
nameof ( UIInvoiceController . InvoiceReceipt ) ,
2023-02-21 21:06:36 +01:00
"UIInvoice" ,
new { invoiceId } ,
Request . Scheme ,
Request . Host ,
Request . PathBase )
2022-07-06 14:14:55 +02:00
} ;
}
2022-08-12 20:10:44 +02:00
2023-04-07 10:48:58 +02:00
bool updatePaymentMethod = false ;
if ( lnurlSupportedPaymentMethod . LUD12Enabled )
2022-08-12 20:10:44 +02:00
{
2023-04-07 10:48:58 +02:00
comment = comment ? . Truncate ( 2000 ) ;
if ( paymentMethodDetails . ProvidedComment ! = comment )
2022-08-12 20:10:44 +02:00
{
2023-04-07 10:48:58 +02:00
paymentMethodDetails . ProvidedComment = comment ;
updatePaymentMethod = true ;
2023-03-29 12:27:04 +02:00
}
2022-08-12 20:10:44 +02:00
}
2023-01-06 14:18:07 +01:00
2023-02-21 21:06:36 +01:00
if ( string . IsNullOrEmpty ( paymentMethodDetails . BOLT11 ) | | paymentMethodDetails . GeneratedBoltAmount ! = amt )
2021-10-25 08:18:02 +02:00
{
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
{
2022-08-25 10:40:06 +02:00
var expiry = i . ExpirationTime . ToUniversalTime ( ) - DateTimeOffset . UtcNow ;
2023-04-10 06:30:38 +02:00
var description = ( await _pluginHookService . ApplyFilter ( "modify-lnurlp-description" , lnurlPayRequest . Metadata ) ) as string ;
2023-03-29 12:27:04 +02:00
if ( description is null )
return NotFound ( ) ;
2023-04-07 10:48:58 +02:00
2023-03-29 12:27:04 +02:00
var param = new CreateInvoiceParams ( amt , description , expiry )
2022-08-25 10:40:06 +02:00
{
2022-12-13 10:56:33 +01:00
PrivateRouteHints = blob . LightningPrivateRouteHints ,
DescriptionHashOnly = true
2022-08-25 10:40:06 +02:00
} ;
invoice = await client . CreateInvoice ( param ) ;
2021-10-25 08:18:02 +02:00
if ( ! BOLT11PaymentRequest . Parse ( invoice . BOLT11 , network . NBitcoinNetwork )
2023-03-29 12:27:04 +02:00
. VerifyDescriptionHash ( description ) )
2021-10-25 08:18:02 +02:00
{
return BadRequest ( new LNUrlStatusResponse
{
Status = "ERROR" ,
2023-01-05 14:41:18 +01:00
Reason = "Lightning node could not generate invoice with a valid description hash"
2021-10-25 08:18:02 +02:00
} ) ;
}
}
2023-01-05 14:41:18 +01:00
catch ( Exception ex )
2021-10-25 08:18:02 +02:00
{
return BadRequest ( new LNUrlStatusResponse
{
Status = "ERROR" ,
2023-01-05 14:41:18 +01:00
Reason = "Lightning node could not generate invoice with description hash" + (
string . IsNullOrEmpty ( ex . Message ) ? "" : $": {ex.Message}" )
2021-10-25 08:18:02 +02:00
} ) ;
}
paymentMethodDetails . BOLT11 = invoice . BOLT11 ;
2023-01-13 09:29:41 +01:00
paymentMethodDetails . PaymentHash = string . IsNullOrEmpty ( invoice . PaymentHash ) ? null : uint256 . Parse ( invoice . PaymentHash ) ;
paymentMethodDetails . Preimage = string . IsNullOrEmpty ( invoice . Preimage ) ? null : uint256 . Parse ( invoice . Preimage ) ;
2021-10-25 08:18:02 +02:00
paymentMethodDetails . InvoiceId = invoice . Id ;
2023-02-21 21:06:36 +01:00
paymentMethodDetails . GeneratedBoltAmount = amt ;
2023-04-07 10:48:58 +02:00
updatePaymentMethod = true ;
2021-10-25 08:18:02 +02:00
}
2023-04-07 10:48:58 +02:00
if ( updatePaymentMethod )
2021-10-25 08:18:02 +02:00
{
2023-04-07 10:48:58 +02:00
lightningPaymentMethod . SetPaymentMethodDetails ( paymentMethodDetails ) ;
await _invoiceRepository . UpdateInvoicePaymentMethod ( invoiceId , lightningPaymentMethod ) ;
2023-04-10 08:06:59 +02:00
_eventAggregator . Publish ( new InvoiceNewPaymentDetailsEvent ( invoiceId , paymentMethodDetails , pmi ) ) ;
2021-10-25 08:18:02 +02:00
}
2023-04-07 10:48:58 +02:00
return Ok ( new LNURLPayRequest . LNURLPayRequestCallbackResponse
{
Disposable = true ,
Routes = Array . Empty < string > ( ) ,
Pr = paymentMethodDetails . BOLT11 ,
SuccessAction = successAction
} ) ;
2021-10-25 08:18:02 +02:00
}
return BadRequest ( new LNUrlStatusResponse
{
2023-01-06 14:18:07 +01:00
Status = "ERROR" ,
Reason = "Invoice not in a valid payable state"
2021-10-25 08:18:02 +02:00
} ) ;
}
2023-04-10 04:07:03 +02:00
2021-10-29 11:01:16 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2022-05-04 19:40:23 +02:00
[HttpGet("~/stores/{storeId}/plugins/lightning-address")]
2021-10-29 11:01:16 +02:00
public async Task < IActionResult > EditLightningAddress ( string storeId )
{
2022-04-19 09:58:31 +02:00
if ( ControllerContext . HttpContext . GetStoreData ( ) . GetEnabledPaymentIds ( _btcPayNetworkProvider )
. All ( id = > id . PaymentType ! = LNURLPayPaymentType . Instance ) )
2021-10-29 11:01:16 +02:00
{
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Message = "LNURL is required for lightning addresses but has not yet been enabled." ,
Severity = StatusMessageModel . StatusSeverity . Error
} ) ;
2023-01-06 14:18:07 +01:00
return RedirectToAction ( nameof ( UIStoresController . GeneralSettings ) , "UIStores" , new { storeId } ) ;
2021-10-29 11:01:16 +02:00
}
2022-04-19 09:58:31 +02:00
var addresses =
2023-01-06 14:18:07 +01:00
await _lightningAddressService . Get ( new LightningAddressQuery ( ) { StoreIds = new [ ] { storeId } } ) ;
2022-04-19 09:58:31 +02:00
2021-10-29 11:01:16 +02:00
return View ( new EditLightningAddressVM
{
2022-04-19 09:58:31 +02:00
Items = addresses . Select ( s = >
{
2023-02-21 07:06:34 +01:00
var blob = s . GetBlob ( ) ;
2022-04-19 09:58:31 +02:00
return new EditLightningAddressVM . EditLightningAddressItem
{
Max = blob . Max ,
Min = blob . Min ,
CurrencyCode = blob . CurrencyCode ,
StoreId = storeId ,
Username = s . Username ,
2023-04-07 10:48:58 +02:00
InvoiceMetadata = blob . InvoiceMetadata ? . ToString ( Formatting . Indented )
2022-04-19 09:58:31 +02:00
} ;
}
) . ToList ( )
2021-10-29 11:01:16 +02:00
} ) ;
}
2022-04-19 09:58:31 +02:00
2021-10-29 11:01:16 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2022-05-04 19:40:23 +02:00
[HttpPost("~/stores/{storeId}/plugins/lightning-address")]
2021-10-29 11:01:16 +02:00
public async Task < IActionResult > EditLightningAddress ( string storeId , [ FromForm ] EditLightningAddressVM vm ,
string command , [ FromServices ] CurrencyNameTable currencyNameTable )
{
if ( command = = "add" )
{
2022-04-19 09:58:31 +02:00
if ( ! string . IsNullOrEmpty ( vm . Add . CurrencyCode ) & &
currencyNameTable . GetCurrencyData ( vm . Add . CurrencyCode , false ) is null )
2021-10-29 11:01:16 +02:00
{
vm . AddModelError ( addressVm = > addressVm . Add . CurrencyCode , "Currency is invalid" , this ) ;
}
2023-04-07 10:48:58 +02:00
JObject metadata = null ;
2023-04-10 04:07:03 +02:00
if ( ! string . IsNullOrEmpty ( vm . Add . InvoiceMetadata ) )
2023-04-07 10:48:58 +02:00
{
try
{
2023-04-10 04:07:03 +02:00
metadata = JObject . Parse ( vm . Add . InvoiceMetadata ) ;
2023-04-07 10:48:58 +02:00
}
2023-04-10 05:01:11 +02:00
catch ( Exception )
2023-04-07 10:48:58 +02:00
{
vm . AddModelError ( addressVm = > addressVm . Add . InvoiceMetadata , "Metadata must be a valid json object" , this ) ;
}
}
2021-10-29 11:01:16 +02:00
if ( ! ModelState . IsValid )
{
return View ( vm ) ;
}
2023-01-06 14:18:07 +01:00
2021-10-29 11:01:16 +02:00
2022-04-19 09:58:31 +02:00
if ( await _lightningAddressService . Set ( new LightningAddressData ( )
2023-01-06 14:18:07 +01:00
{
StoreDataId = storeId ,
2023-02-21 07:06:34 +01:00
Username = vm . Add . Username
} . SetBlob ( new LightningAddressDataBlob ( )
{
Max = vm . Add . Max ,
Min = vm . Add . Min ,
2023-04-07 10:48:58 +02:00
CurrencyCode = vm . Add . CurrencyCode ,
InvoiceMetadata = metadata
2023-02-21 07:06:34 +01:00
} ) ) )
2021-10-29 11:01:16 +02:00
{
2022-04-19 09:58:31 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Success ,
Message = "Lightning address added successfully."
} ) ;
2021-10-29 11:01:16 +02:00
}
else
{
2022-04-19 09:58:31 +02:00
vm . AddModelError ( addressVm = > addressVm . Add . Username , "Username is already taken" , this ) ;
2023-01-06 14:18:07 +01:00
2022-04-19 09:58:31 +02:00
if ( ! ModelState . IsValid )
{
return View ( vm ) ;
}
2021-10-29 11:01:16 +02:00
}
return RedirectToAction ( "EditLightningAddress" ) ;
}
if ( command . StartsWith ( "remove" , StringComparison . InvariantCultureIgnoreCase ) )
{
2022-04-19 09:58:31 +02:00
var index = command . Substring ( command . IndexOf ( ":" , StringComparison . InvariantCultureIgnoreCase ) + 1 ) ;
if ( await _lightningAddressService . Remove ( index , storeId ) )
2021-10-29 11:01:16 +02:00
{
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Success ,
2022-04-19 09:58:31 +02:00
Message = $"Lightning address {index} removed successfully."
2021-10-29 11:01:16 +02:00
} ) ;
2022-04-19 09:58:31 +02:00
return RedirectToAction ( "EditLightningAddress" ) ;
}
else
{
vm . AddModelError ( addressVm = > addressVm . Add . Username , "Username could not be removed" , this ) ;
2023-01-06 14:18:07 +01:00
2022-04-19 09:58:31 +02:00
if ( ! ModelState . IsValid )
{
return View ( vm ) ;
}
2021-10-29 11:01:16 +02:00
}
}
2023-01-06 14:18:07 +01:00
2022-04-19 09:58:31 +02:00
return View ( vm ) ;
2021-10-29 11:01:16 +02:00
}
2021-10-25 08:18:02 +02:00
}
}