Skip to content

Commit

Permalink
Add Lightning send
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisreimann committed Dec 3, 2024
1 parent 9b1e328 commit 07c9da2
Showing 1 changed file with 83 additions and 51 deletions.
134 changes: 83 additions & 51 deletions BTCPayApp.UI/Pages/FundsPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using BTCPayApp.Core.Auth
@using BTCPayApp.Core.BTCPayServer
@using BTCPayApp.Core.Data
@using BTCPayApp.Core.Helpers
@using BTCPayApp.Core.LDK
@using BTCPayApp.Core.Wallet
@using BTCPayApp.UI.Features
@using BTCPayApp.UI.Components.Layout
@using BTCPayServer.Client.App
@using BTCPayServer.Client.App.Models
@using NBitcoin
@using BTCPayServer.Client.Models
Expand Down Expand Up @@ -236,7 +239,8 @@

@code {
private readonly SemaphoreSlim _semaphore = new(1, 1);

private LDKNode? Node => LightningNodeManager.Node;
private PaymentsManager? PaymentsManager => Node?.PaymentsManager;
private AppUserStoreInfo? StoreInfo => StoreState.Value.StoreInfo;
private string? StoreId => StoreState.Value.StoreInfo?.Id;
private OnChainWalletOverviewData? OnchainBalance => StoreState.Value.OnchainBalance?.Data;
Expand Down Expand Up @@ -264,6 +268,13 @@
private string GetTitle() => $"{StoreInfo?.Name ?? "Store"} Funds";
private bool InsufficientFunds => Model.Amount is > 0 && MaxAvailable < Model.Amount;
private bool NotSupported => Model.Bolt11 != null && LightningState != SourceState.Supported || Model.Address != null && OnchainState != SourceState.Supported;
private bool CanAdjustAmount => Model.Bolt11?.MinimumAmount == null && MaxAvailable > 0;
private decimal? MaxAvailable => Model.Bolt11 != null
? (LightningOffchainLocalBalance ?? 0)
: (OnchainConfirmedBalance ?? 0);
private bool IsValidForm => !_validationEditContext!.Invalid && MaxAvailable is > 0 && Model.Amount is > 0 && !string.IsNullOrEmpty(Model.Destination) && (
(Model.Bolt11 != null && LightningState == SourceState.Supported) ||
(Model.Address != null && OnchainState == SourceState.Supported));
private string DestinationPlaceholder
{
get
Expand All @@ -280,45 +291,16 @@
}
}

private bool CanAdjustAmount => Model.Bolt11?.MinimumAmount == null && MaxAvailable > 0;
private decimal? MaxAvailable => Model.Bolt11 != null
? (LightningOffchainLocalBalance ?? 0)
: (OnchainConfirmedBalance ?? 0);
private bool IsValidForm => !_validationEditContext!.Invalid && MaxAvailable is > 0 && Model.Amount is > 0 && !string.IsNullOrEmpty(Model.Destination) && (
(Model.Bolt11 != null && LightningState == SourceState.Supported) ||
(Model.Address != null && OnchainState == SourceState.Supported));

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

var (onchain, lightning) = await AccountManager.GetCurrentStorePaymentMethods();
OnchainState = onchain != null
? await OnChainWalletManager.IsOnChainOurs(onchain) ? SourceState.Supported : SourceState.NotSupported
: SourceState.NotConfigured;
LightningState = lightning != null
? await LightningNodeManager.IsLightningOurs(lightning) ? SourceState.Supported : SourceState.NotSupported
: SourceState.NotConfigured;
}

private void ToggleDisplayCurrency()
{
if (Model.Amount.HasValue)
Model.Amount = new Money(Model.Amount.Value, UnitMoney).ToDecimal(UnitMoney == MoneyUnit.Satoshi
? MoneyUnit.BTC
: MoneyUnit.Satoshi);
Dispatcher.Dispatch(new UIState.ToggleBitcoinUnit());
}

private bool _sending { get; set; }
private string? _qrInput;
private string? _successMessage { get; set; }
private string? _errorMessage { get; set; }
private ValidationEditContext? _validationEditContext;
private SendModel Model { get; set; } = new();

private SourceState OnchainState { get; set; }
private SourceState LightningState { get; set; }
private List<AppLightningPayment>? _payments;
private Dictionary<string, TxResp[]>? _txs;

private enum SourceState
{
Expand All @@ -339,6 +321,34 @@
public (Transaction Tx, ICoin[] SpentCoins, BitcoinAddress Change)? Transaction { get; set; }
}

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

var (onchain, lightning) = await AccountManager.GetCurrentStorePaymentMethods();
OnchainState = onchain != null
? await OnChainWalletManager.IsOnChainOurs(onchain) ? SourceState.Supported : SourceState.NotSupported
: SourceState.NotConfigured;
LightningState = lightning != null
? await LightningNodeManager.IsLightningOurs(lightning) ? SourceState.Supported : SourceState.NotSupported
: SourceState.NotConfigured;
}

private async Task LoadOnchainData()
{
if (OnChainWalletManager is null) return;
_txs = await OnChainWalletManager.GetTransactions();
}

private async Task LoadLightningData()
{
if (PaymentsManager is null) return;
await _semaphore.WaitAsync();
var payments = await PaymentsManager.List(payments => payments.Where(p => !p.Inbound));
_payments = payments.OrderByDescending(p => p.Timestamp).ToList();
_semaphore.Release();
}

private void UpdateForDestination(string? destination)
{
var network = ConnectionManager.ReportedNetwork;
Expand Down Expand Up @@ -371,19 +381,6 @@
}
}

// ReSharper disable once AsyncVoidMethod
private async void OnQrCodeScan(string code)
{
// prevent duplicate submission due to quirk in QR reader lib
if (code == _qrInput) return;
_qrInput = code;
await JS.InvokeVoidAsync("Interop.closeModal", "#ScanQrCodeModal");

Model.Destination = code;
UpdateForDestination(Model.Destination);
StateHasChanged();
}

private async Task SendFunds()
{
if (_sending || !IsValidForm) return;
Expand Down Expand Up @@ -488,12 +485,25 @@
await InvokeAsync(StateHasChanged);
}

private async Task CancelLightningPayment(AppLightningPayment payment)
{
if (PaymentsManager is null) return;
await _semaphore.WaitAsync();
await PaymentsManager.Cancel(payment);
_semaphore.Release();
await LoadLightningData();
}

private async Task<FormResult> SendLightning(BOLT11PaymentRequest bolt11, LightMoney amount)
{
if (PaymentsManager is null) return new FormResult(false, "Payment failed: Lightning payments unavailable");
try
{
await _semaphore.WaitAsync();
// TODO
var result = await PaymentsManager.PayInvoice(bolt11, amount);
_successMessage = $"Payment {result.PaymentId} sent with status {result.Status}";
Model = new SendModel();

return new FormResult(true, "Payment sent");
}
catch (Exception ex)
Expand All @@ -508,13 +518,13 @@

private async Task<FormResult> SendLightningExternal(BOLT11PaymentRequest bolt11, LightMoney amount)
{
var lnRequest = new PayLightningInvoiceRequest
{
BOLT11 = bolt11.ToString(),
Amount = amount
};
try
{
var lnRequest = new PayLightningInvoiceRequest
{
BOLT11 = bolt11.ToString(),
Amount = amount
};
var payData = await AccountManager.GetClient().PayLightningInvoice(StoreId, "BTC", lnRequest);
return payData != null
? new FormResult(true, $"Created transaction {payData.PaymentHash} spending {payData.TotalAmount} BTC")
Expand Down Expand Up @@ -550,4 +560,26 @@
return new FormResult(false, "Payment failed: " + ex.Message);
}
}

private void ToggleDisplayCurrency()
{
if (Model.Amount.HasValue)
Model.Amount = new Money(Model.Amount.Value, UnitMoney).ToDecimal(UnitMoney == MoneyUnit.Satoshi
? MoneyUnit.BTC
: MoneyUnit.Satoshi);
Dispatcher.Dispatch(new UIState.ToggleBitcoinUnit());
}

// ReSharper disable once AsyncVoidMethod
private async void OnQrCodeScan(string code)
{
// prevent duplicate submission due to quirk in QR reader lib
if (code == _qrInput) return;
_qrInput = code;
await JS.InvokeVoidAsync("Interop.closeModal", "#ScanQrCodeModal");

Model.Destination = code;
UpdateForDestination(Model.Destination);
StateHasChanged();
}
}

0 comments on commit 07c9da2

Please sign in to comment.