diff --git a/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj b/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj index dd81dad..e64d493 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj +++ b/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj @@ -11,7 +11,7 @@ LN Prism Automated value splits for lightning. - 1.1.4 + 1.1.5 diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor index 00c80cb..674e1f5 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor @@ -7,12 +7,27 @@ Destination Sats + Actions @foreach (var (dest, balance) in DestinationBalance) { @dest @(balance / 1000m) + + @if (UpdatingDestination == dest) + { + + + + + } + else + { + + + } + } @@ -44,10 +59,33 @@ @code { + private string? UpdatingDestination { get; set; } + private long? UpdatingValue { get; set; } + [Parameter] public Dictionary DestinationBalance { get; set; } [Parameter] public Dictionary PendingPayouts { get; set; } + + [Parameter] + public EventCallback<(string destination, long newBalance)> OnUpdate { get; set; } + + private EventCallback StartUpdate(string dest, long balance) + { + UpdatingDestination = dest; + UpdatingValue = Convert.ToInt32(balance/1000m); + return EventCallback.Empty; + } + private async Task Update() + { + if (UpdatingDestination is null || UpdatingValue is null) + { + return; + } + await OnUpdate.InvokeAsync((UpdatingDestination, Convert.ToInt64(UpdatingValue.Value * 1000m))); + UpdatingDestination = null; + } + } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismDestinationEditor.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismDestinationEditor.razor new file mode 100644 index 0000000..4163a0b --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismDestinationEditor.razor @@ -0,0 +1,112 @@ +@using Newtonsoft.Json.Linq +
+
+ + + @if (InvalidId) + { + Invalid + } + An alias to reference in destinations of prisms. +
+
+ + + @if (Invalid) + { + Invalid + } + Can be a lightning address, LNURL, or a custom value that another plugin supports +
+
+ + + How many sats do you want to accumulate before sending? Leave blank to use default setting. +
+
+ + + When a payout is being generated, how much of its amount in percentage should be excluded to cover the fee? Once the payment is settled, if the lightning node provides the exact fee, the balance is adjusted accordingly. Leave blank to use default setting. +
+
+ + +
+
+ +@code { + + public bool Invalid { get; set; } = false; + + private PrismDestination WorkingCopy { get; set; } + private string WorkingId { get; set; } + private string OriginalWorkingId { get; set; } + + + [Parameter] + public Func ValidateDestination { get; set; } + + [Parameter] + public Func ValidateId { get; set; } + + [Parameter] + public PrismDestination Settings + { + get => WorkingCopy; + set + { + WorkingCopy = JObject.FromObject(value ?? new PrismDestination()).ToObject(); + Invalid = false; + } + } + + [Parameter] + public string Id + { + get => WorkingId; + set + { + if (OriginalWorkingId != value) + { + WorkingId = value; + OriginalWorkingId = value; + InvalidId = false; + } + } + } + + public bool InvalidId { get; set; } + + public async Task Update() + { + if (ValidateDestination.Invoke(WorkingCopy.Destination)) + { + await SettingsChanged.InvokeAsync(WorkingCopy); + + Invalid = false; + } + else + { + Invalid = true; + } + + if (ValidateId.Invoke(WorkingId)) + { + await IdChanged.InvokeAsync(WorkingId); + InvalidId = false; + + } + else + { + InvalidId = true; + } + } + + [Parameter] + public EventCallback SettingsChanged { get; set; } + + [Parameter] + public EventCallback IdChanged { get; set; } + + +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor index aee304d..872911c 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor @@ -6,7 +6,7 @@ @using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Routing @using Microsoft.Extensions.Logging -@using Newtonsoft.Json.Linq +@using NBitcoin @using LightningAddressData = BTCPayServer.Data.LightningAddressData @inject IPluginHookService PluginHookService @inject LightningAddressService LightningAddressService @@ -39,12 +39,20 @@ else } - + + @foreach (var user in Users) { - + } + + @foreach (var destination in Destinations) + { + + } + +

Prism @@ -79,17 +87,41 @@ else When a payout is being generated, how many of its amount in percentage should be excluded to cover the fee? Once the payment is settled, if the lightning node provides the exact fee, the balance is adjusted accordingly. - - + +
- @foreach (var item in Settings.Splits) - { - - } + +
+ @foreach (var item in Settings.Splits) + { + + } +
+
+ @if (Destinations.Any()) + { +
+ +
+ } + @if (SelectedDestinationId is not null && SelectedDestinationId != "null") + { + + } +
- + + + @if (StatusMessageModel != null) {
@@ -99,6 +131,7 @@ else
+
@@ -106,6 +139,43 @@ else } @code { + + + private void AddDestination() + { + SelectedDestinationId = ""; + StateHasChanged(); + } + + public string? SelectedDestinationId { get; set; } + + public PrismDestination? SelectedDestination + { + get + { + if (SelectedDestinationId is null || SelectedDestinationId == "null") + return null; + Settings.Destinations.TryGetValue(SelectedDestinationId, out var res); + return res; + } + set + { + if (SelectedDestinationId is null) + return; + if (value is null) + { + Settings.Destinations.Remove(SelectedDestinationId); + SelectedDestinationId = null; + } + else + { + Settings.Destinations.AddOrReplace(SelectedDestinationId, value); + } + } + } + + public string[] Destinations => Settings.Destinations.Keys.ToArray(); + public bool Loading { get; set; } = true; public List Users { get; set; } = new(); public PaymentMethodId pmi { get; set; } = new("BTC", LightningPaymentType.Instance); @@ -115,7 +185,6 @@ else { if (firstRender) { - PayoutProcessorLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "ConfigureStorePayoutProcessors", "UIPayoutProcessors", new {StoreId}); LNAddressLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "EditLightningAddress", "UILNURL", new {StoreId}); PayoutsLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "Payouts", "UIStorePullPayments", new {StoreId, payoutState = PayoutState.AwaitingPayment, paymentMethodId = pmi.ToString()}); @@ -148,7 +217,6 @@ else NoPayoutProcessors = PayoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmi)) && !(await fetchProcessors).Any(); Loading = false; await InvokeAsync(StateHasChanged); - } await base.OnAfterRenderAsync(firstRender); } @@ -160,7 +228,7 @@ else private void SatBreakerOnPrismUpdated(object sender, PrismPaymentDetectedEventArgs e) { - if(e.StoreId != StoreId) return; + if (e.StoreId != StoreId) return; if (e.Settings != Settings && e.Settings.Version != Settings.Version) { Settings.DestinationBalance = e.Settings.DestinationBalance; @@ -200,14 +268,13 @@ else foreach (var destination in prism.Destinations) { var dest = destination.Destination; - //check that the source is a valid internet identifier, which is username@domain(and optional port) if (string.IsNullOrEmpty(dest)) { MessageStore.Add(() => destination.Destination, "Destination is required"); continue; } - if (!ValidateDestination(dest)) + if (!ValidateDestination(dest, true)) { MessageStore.Add(() => destination.Destination, "Destination is not valid"); } @@ -217,8 +284,12 @@ else EditContext.NotifyValidationStateChanged(); } - private bool ValidateDestination(string dest) + private bool ValidateDestination(string dest, bool allowAlias) { + if (allowAlias && Destinations.Contains(dest)) + { + return true; + } try { LNURL.LNURL.ExtractUriFromInternetIdentifier(dest); @@ -243,7 +314,7 @@ else public ValidationMessageStore MessageStore { get; set; } - public EditContext EditContext { get; set; } + public EditContext? EditContext { get; set; } public StatusMessageModel StatusMessageModel { get; set; } public PrismSettings Settings { get; set; } @@ -301,9 +372,84 @@ else public void Dispose() { - EditContext.OnValidationRequested -= Validate; - EditContext.OnFieldChanged -= FieldChanged; + if (EditContext is not null) + { + EditContext.OnValidationRequested -= Validate; + EditContext.OnFieldChanged -= FieldChanged; + } SatBreaker.PrismUpdated -= SatBreakerOnPrismUpdated; } + private bool ValidateId(string arg) + { + if (string.IsNullOrEmpty(arg)) + return false; + + if (SelectedDestinationId == arg) + { + return true; + } + if (Destinations.Contains(arg)) + { + return false; + } + return true; + } + + private async Task OnIdRenamed(string s) + { + if(SelectedDestinationId == s) + return; + try + { + await SatBreaker._updateLock.WaitAsync(); + + if (SelectedDestinationId == s) + { + return; + } + // find all prisms splits that use this id + all destination balances that use this id + all pending payouts that use this id and rename them + foreach (var destination in Settings.Splits.SelectMany(split => split.Destinations.Where(destination => destination.Destination == SelectedDestinationId))) + { + destination.Destination = s; + } + if (Settings.DestinationBalance.Remove(SelectedDestinationId, out var db)) + { + Settings.DestinationBalance.Add(s, db); + } + if(Settings.Destinations.Remove(SelectedDestinationId, out var dest)) + { + Settings.Destinations.Add(s, dest); + } + SelectedDestinationId = s; + + await SatBreaker.UpdatePrismSettingsForStore(StoreId, Settings, true); + } + finally + { + SatBreaker._updateLock.Release(); + } + } + + private async Task OnUpdateBalance((string destination, long newBalance) obj) + { + try + { + await SatBreaker._updateLock.WaitAsync(); + if (obj.newBalance == 0) + { + Settings.DestinationBalance.Remove(obj.destination); + } + else + { + Settings.DestinationBalance.AddOrReplace(obj.destination, obj.newBalance); + } + await SatBreaker.UpdatePrismSettingsForStore(StoreId, Settings, true); + + } + finally + { + SatBreaker._updateLock.Release(); + } + } } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor index 0504475..78053e7 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor @@ -1,4 +1,4 @@ -
+
@@ -15,11 +15,11 @@ - @foreach(var destination in Split.Destinations) + @foreach (var destination in Split.Destinations) { - + diff --git a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs index 60ead58..0d4cbc2 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs @@ -208,7 +208,7 @@ namespace BTCPayServer.Plugins.Prism record CreditDestination(string StoreId, Dictionary Destcredits, List PayoutsToRemove); - private readonly SemaphoreSlim _updateLock = new(1, 1); + public readonly SemaphoreSlim _updateLock = new(1, 1); public async Task Get(string storeId) {