2021-11-05 12:16:54 +09: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 ;
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 ;
using BTCPayServer.Payments ;
using BTCPayServer.Payments.Lightning ;
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 09:40:31 +09: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 16:59:02 +09:00
using Microsoft.AspNetCore.Routing ;
2021-10-25 08:18:02 +02:00
using NBitcoin ;
using Newtonsoft.Json ;
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 12:32:00 +09: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-04-19 09:58:31 +02:00
2022-01-07 12:32:00 +09: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 ;
2021-10-25 08:18:02 +02:00
2022-01-07 12:32:00 +09: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 12:32:00 +09: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 ,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings )
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 ;
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 23:39:55 +09: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" } ) ;
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-02-21 21:06:36 +01:00
new PullPaymentHostedService . CancelRequest ( new [ ]
{ 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 ( ) ;
}
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 ( ) ;
}
2021-10-29 11:01:16 +02:00
ViewPointOfSaleViewModel . Item [ ] items = null ;
2021-10-29 10:27:33 +02:00
string currencyCode = null ;
switch ( app . AppType )
{
case nameof ( AppType . Crowdfund ) :
var cfS = app . GetSettings < CrowdfundSettings > ( ) ;
currencyCode = cfS . TargetCurrency ;
items = _appService . Parse ( cfS . PerksTemplate , cfS . TargetCurrency ) ;
break ;
case nameof ( AppType . PointOfSale ) :
2022-02-26 21:19:02 -08:00
var posS = app . GetSettings < PointOfSaleSettings > ( ) ;
2021-10-29 10:27:33 +02:00
currencyCode = posS . Currency ;
items = _appService . Parse ( posS . Template , posS . Currency ) ;
break ;
}
2023-02-06 18:17:17 +09:00
var escapedItemId = Extensions . UnescapeBackSlashUriString ( itemCode ) ;
2021-10-29 10:27:33 +02:00
var item = items . FirstOrDefault ( item1 = >
2023-02-06 18:17:17 +09:00
item1 . Id . Equals ( itemCode , StringComparison . InvariantCultureIgnoreCase ) | |
item1 . Id . Equals ( escapedItemId , StringComparison . InvariantCultureIgnoreCase ) ) ;
2021-10-29 10:27:33 +02:00
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 ,
2023-01-06 14:18:07 +01:00
( ) = > ( null , app , item , new List < string > { AppService . GetAppInternalTag ( appId ) } , item . Price . Value , true ) ) ;
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 16:59:02 +09: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 16:59:02 +09:00
2021-10-29 11:01:16 +02:00
[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}")]
2023-02-02 09:40:31 +09: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 ) ;
if ( lightningAddressSettings is null )
2021-10-29 11:01:16 +02:00
{
2021-11-05 12:16:54 +09:00
return NotFound ( "Unknown username" ) ;
2021-10-29 11:01:16 +02:00
}
2023-02-21 15:06:34 +09:00
var blob = lightningAddressSettings . GetBlob ( ) ;
2022-04-19 09:58:31 +02:00
return await GetLNURL ( "BTC" , lightningAddressSettings . StoreDataId , blob . CurrencyCode , blob . Min , blob . Max ,
2022-04-24 13:36:10 +02:00
( ) = > ( username , null , null , null , null , true ) ) ;
2021-10-29 11:01:16 +02:00
}
2021-10-29 10:27:33 +02:00
[HttpGet("pay")]
public async Task < IActionResult > GetLNURL ( string cryptoCode , string storeId , string currencyCode = null ,
decimal? min = null , decimal? max = null ,
2022-04-24 13:36:10 +02:00
Func < ( string username , AppData app , ViewPointOfSaleViewModel . Item item , List < string > additionalTags , decimal? invoiceAmount , bool? anyoneCanInvoice ) >
2021-10-29 10:27:33 +02:00
internalDetails = null )
{
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( cryptoCode ) ;
if ( network is null | | ! network . SupportLightning )
{
2021-11-05 12:16:54 +09:00
return NotFound ( "This network does not support Lightning" ) ;
2021-10-29 10:27:33 +02:00
}
var store = await _storeRepository . FindStore ( storeId ) ;
if ( store is null )
{
2021-11-05 12:16:54 +09:00
return NotFound ( "Store not found" ) ;
2021-10-29 10:27:33 +02:00
}
2022-07-06 14:14:55 +02:00
var storeBlob = store . GetStoreBlob ( ) ;
currencyCode ? ? = storeBlob . DefaultCurrency ? ? cryptoCode ;
2021-10-29 10:27:33 +02:00
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 )
{
2021-11-05 12:16:54 +09:00
return NotFound ( "LNURL or Lightning payment method not found" ) ;
2021-10-29 10:27:33 +02:00
}
var blob = store . GetStoreBlob ( ) ;
if ( blob . GetExcludedPaymentMethods ( ) . Match ( pmi ) | | blob . GetExcludedPaymentMethods ( ) . Match ( lnpmi ) )
{
2021-11-05 12:16:54 +09:00
return NotFound ( "LNURL or Lightning payment method disabled" ) ;
2021-10-29 10:27:33 +02:00
}
2022-04-24 13:36:10 +02:00
( string username , AppData app , ViewPointOfSaleViewModel . Item item , List < string > additionalTags , decimal? invoiceAmount , bool? anyoneCanInvoice ) =
( internalDetails ? ? ( ( ) = > ( null , null , null , null , null , null ) ) ) ( ) ;
2021-10-29 10:27:33 +02:00
if ( ( anyoneCanInvoice ? ? blob . AnyoneCanInvoice ) is false )
{
return NotFound ( ) ;
}
2022-02-21 13:21:33 +09:00
var lnAddress = username is null ? null : $"{username}@{Request.Host}" ;
2022-04-24 13:36:10 +02:00
List < string [ ] > lnurlMetadata = new ( ) ;
2021-10-29 10:27:33 +02:00
2022-06-28 07:05:02 +02:00
var redirectUrl = app ? . AppType switch
{
nameof ( AppType . PointOfSale ) = > app . GetSettings < PointOfSaleSettings > ( ) . RedirectUrl ? ?
HttpContext . Request . GetAbsoluteUri ( $"/apps/{app.Id}/pos" ) ,
_ = > null
} ;
2022-04-24 13:36:10 +02:00
var invoiceRequest = new CreateInvoiceRequest
{
Amount = invoiceAmount ,
Checkout = new InvoiceDataBase . CheckoutOptions
2021-10-29 10:27:33 +02:00
{
2022-04-24 13:36:10 +02:00
PaymentMethods = new [ ] { pmi . ToStringNormalized ( ) } ,
Expiration = blob . InvoiceExpiration < TimeSpan . FromMinutes ( 2 )
? blob . InvoiceExpiration
2022-06-28 07:05:02 +02:00
: TimeSpan . FromMinutes ( 2 ) ,
RedirectURL = redirectUrl
2022-04-24 13:36:10 +02:00
} ,
Currency = currencyCode ,
Type = invoiceAmount is null ? InvoiceType . TopUp : InvoiceType . Standard ,
} ;
if ( item ! = null )
{
invoiceRequest . Metadata =
new InvoiceMetadata
2021-10-29 10:27:33 +02:00
{
2023-01-06 14:18:07 +01:00
ItemCode = item . Id ,
ItemDesc = item . Description ,
2022-06-28 07:05:02 +02:00
OrderId = AppService . GetAppOrderId ( app )
2022-04-24 13:36:10 +02:00
} . ToJObject ( ) ;
}
2023-02-08 20:45:05 +09:00
InvoiceEntity i ;
try
{
i = await _invoiceController . CreateInvoiceCoreRaw ( invoiceRequest , store , Request . GetAbsoluteRoot ( ) , additionalTags ) ;
}
catch ( Exception e )
{
return this . CreateAPIError ( null , e . Message ) ;
}
2021-10-29 10:27:33 +02:00
if ( i . Type ! = InvoiceType . TopUp )
{
min = i . GetPaymentMethod ( pmi ) . Calculate ( ) . Due . ToDecimal ( MoneyUnit . Satoshi ) ;
2023-01-06 14:18:07 +01:00
max = item ? . Price ? . Type = = ViewPointOfSaleViewModel . Item . ItemPrice . ItemPriceType . Minimum ? null : min ;
2021-10-29 10:27:33 +02:00
}
2021-10-29 11:01:16 +02:00
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 ) ;
}
2023-01-06 14:18:07 +01:00
2022-04-24 13:36:10 +02:00
var description = blob . LightningDescriptionTemplate
. Replace ( "{StoreName}" , store . StoreName ? ? "" , StringComparison . OrdinalIgnoreCase )
. Replace ( "{ItemDescription}" , i . Metadata . ItemDesc ? ? "" , StringComparison . OrdinalIgnoreCase )
. Replace ( "{OrderId}" , i . Metadata . OrderId ? ? "" , StringComparison . OrdinalIgnoreCase ) ;
2021-10-29 11:01:16 +02:00
2023-01-06 14:18:07 +01:00
lnurlMetadata . Add ( new [ ] { "text/plain" , description } ) ;
2021-10-29 11:01:16 +02:00
if ( ! string . IsNullOrEmpty ( username ) )
{
2023-01-06 14:18:07 +01:00
lnurlMetadata . Add ( new [ ] { "text/identifier" , lnAddress } ) ;
2021-10-29 11:01:16 +02:00
}
2021-10-29 10:27:33 +02:00
return Ok ( new LNURLPayRequest
{
Tag = "payRequest" ,
MinSendable = new LightMoney ( min ? ? 1 m , LightMoneyUnit . Satoshi ) ,
MaxSendable =
max is null
? LightMoney . FromUnit ( 6.12 m , 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 ) ,
2022-01-07 12:32:00 +09:00
controller : "UILNURL" ,
2023-01-06 14:18:07 +01:00
values : new { cryptoCode , invoiceId = i . Id } , Request . Scheme , Request . Host , Request . PathBase ) )
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
2022-02-21 13:27:02 +09:00
if ( comment is not null )
2022-02-21 13:47:00 +09:00
comment = comment . Truncate ( 2000 ) ;
2021-10-25 08:18:02 +02:00
var pmi = new PaymentMethodId ( cryptoCode , PaymentTypes . LNURLPay ) ;
var i = await _invoiceRepository . GetInvoice ( invoiceId , true ) ;
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 )
{
var isTopup = i . IsUnsetTopUp ( ) ;
var lnurlSupportedPaymentMethod =
i . GetSupportedPaymentMethod < LNURLPaySupportedPaymentMethod > ( pmi ) . FirstOrDefault ( ) ;
2022-07-06 15:09:05 +02:00
if ( lnurlSupportedPaymentMethod is null )
2021-10-25 08:18:02 +02:00
{
return NotFound ( ) ;
}
var lightningPaymentMethod = i . GetPaymentMethod ( pmi ) ;
var accounting = lightningPaymentMethod . Calculate ( ) ;
var paymentMethodDetails =
lightningPaymentMethod . GetPaymentMethodDetails ( ) as LNURLPayPaymentMethodDetails ;
if ( paymentMethodDetails . LightningSupportedPaymentMethod is null )
{
return NotFound ( ) ;
}
2023-02-21 21:06:36 +01:00
var amt = amount . HasValue ? new LightMoney ( amount . Value ) : null ;
var min = new LightMoney ( isTopup ? 1 m : accounting . Due . ToUnit ( MoneyUnit . Satoshi ) , LightMoneyUnit . Satoshi ) ;
2021-10-25 08:18:02 +02:00
var max = isTopup ? LightMoney . FromUnit ( 6.12 m , LightMoneyUnit . BTC ) : min ;
2022-04-24 13:36:10 +02:00
List < string [ ] > 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 ) ;
2021-10-25 08:18:02 +02:00
2023-01-06 14:18:07 +01:00
lnurlMetadata . Add ( new [ ] { "text/plain" , description } ) ;
2021-10-29 11:01:16 +02:00
if ( ! string . IsNullOrEmpty ( paymentMethodDetails . ConsumedLightningAddress ) )
{
2023-01-06 14:18:07 +01:00
lnurlMetadata . Add ( new [ ] { "text/identifier" , paymentMethodDetails . ConsumedLightningAddress } ) ;
2021-10-29 11:01:16 +02:00
}
2021-10-25 08:18:02 +02:00
var metadata = JsonConvert . SerializeObject ( lnurlMetadata ) ;
2023-02-21 21:06:36 +01:00
if ( amt ! = null & & ( amt < min | | amount > max ) )
2021-10-25 08:18:02 +02:00
{
2023-01-06 14:18:07 +01:00
return BadRequest ( new LNUrlStatusResponse { Status = "ERROR" , Reason = "Amount is out of bounds." } ) ;
2021-10-25 08:18:02 +02:00
}
2023-02-21 21:06:36 +01:00
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 (
nameof ( UIInvoiceController . InvoiceReceipt ) ,
"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-02-21 21:06:36 +01:00
if ( amt is null )
2022-08-12 20:10:44 +02:00
{
return Ok ( new LNURLPayRequest
{
Tag = "payRequest" ,
MinSendable = min ,
MaxSendable = max ,
CommentAllowed = lnurlSupportedPaymentMethod . LUD12Enabled ? 2000 : 0 ,
Metadata = metadata ,
Callback = new Uri ( Request . GetCurrentUrl ( ) )
} ) ;
}
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-02-21 21:06:36 +01:00
var param = new CreateInvoiceParams ( amt , metadata , expiry )
2022-08-25 10:40:06 +02:00
{
2022-12-13 18:56:33 +09: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 )
2022-04-19 09:58:31 +02:00
. VerifyDescriptionHash ( metadata ) )
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 ;
2021-10-25 08:18:02 +02:00
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
{
2023-01-06 14:18:07 +01:00
Disposable = true ,
Routes = Array . Empty < string > ( ) ,
Pr = paymentMethodDetails . BOLT11 ,
2022-07-06 14:14:55 +02:00
SuccessAction = successAction
2021-10-25 08:18:02 +02:00
} ) ;
}
2023-02-21 21:06:36 +01:00
if ( paymentMethodDetails . GeneratedBoltAmount = = amt )
2021-10-25 08:18:02 +02:00
{
if ( lnurlSupportedPaymentMethod . LUD12Enabled & & paymentMethodDetails . ProvidedComment ! = comment )
{
paymentMethodDetails . ProvidedComment = comment ;
lightningPaymentMethod . SetPaymentMethodDetails ( paymentMethodDetails ) ;
await _invoiceRepository . UpdateInvoicePaymentMethod ( invoiceId , lightningPaymentMethod ) ;
}
return Ok ( new LNURLPayRequest . LNURLPayRequestCallbackResponse
{
2023-01-06 14:18:07 +01:00
Disposable = true ,
Routes = Array . Empty < string > ( ) ,
Pr = paymentMethodDetails . BOLT11 ,
2022-07-06 14:14:55 +02:00
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-01-05 14:41:18 +01: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 15:06:34 +09: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 ,
} ;
}
) . 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 ) ;
}
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 15:06:34 +09:00
Username = vm . Add . Username
} . SetBlob ( new LightningAddressDataBlob ( )
{
Max = vm . Add . Max ,
Min = vm . Add . Min ,
CurrencyCode = vm . Add . CurrencyCode
} ) ) )
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
}
}