From 241e319e46a55a799753927d7753b4e6178f74d6 Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 4 Jul 2023 14:16:06 +0200 Subject: [PATCH] updates --- .../BTCPayServer.Plugins.Prism.csproj | 2 +- .../Components/PrismBalances.razor | 53 +++ .../Components/PrismEdit.razor | 296 ++++++++++++++++ .../Components/PrismSplit.razor | 104 ++++++ .../Components/ValidationMessage2.razor | 38 ++ .../Components/_Imports.razor | 8 + .../PrismController.cs | 107 +----- .../BTCPayServer.Plugins.Prism/PrismPlugin.cs | 9 + .../PrismSettings.cs | 12 +- .../BTCPayServer.Plugins.Prism/PrismSplit.cs | 22 +- .../BTCPayServer.Plugins.Prism/SatBreaker.cs | 37 +- Plugins/BTCPayServer.Plugins.Prism/Split.cs | 26 +- .../Views/Prism/Edit.cshtml | 328 +----------------- .../BTCPayServer.Plugins.SideShift.csproj | 2 +- .../SideShiftPlugin.cs | 2 +- .../Shared/SideShift/PrismEnhance.cshtml | 93 +++-- 16 files changed, 675 insertions(+), 464 deletions(-) create mode 100644 Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor create mode 100644 Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor create mode 100644 Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor create mode 100644 Plugins/BTCPayServer.Plugins.Prism/Components/ValidationMessage2.razor create mode 100644 Plugins/BTCPayServer.Plugins.Prism/Components/_Imports.razor diff --git a/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj b/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj index 716def6..85d4f11 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.0.9 + 1.1.0 diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor new file mode 100644 index 0000000..00c80cb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismBalances.razor @@ -0,0 +1,53 @@ +
+ @if (DestinationBalance?.Any() is true) + { +
+

Destination Pending Balances

+ + + + + + @foreach (var (dest, balance) in DestinationBalance) + { + + + + + } +
DestinationSats
@dest@(balance / 1000m)
+
+ } + + @if (PendingPayouts?.Any() is true) + { +
+

Pending Payouts

+ + + + + + + @foreach (var (payoutId, pendingPayout) in PendingPayouts) + { + + + + + + } +
Payout IdReserve feeAmount
@payoutId@pendingPayout.FeeCharged@pendingPayout.PayoutAmount
+
+ } +
+ +@code { + + [Parameter] + public Dictionary DestinationBalance { get; set; } + + [Parameter] + public Dictionary PendingPayouts { 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 new file mode 100644 index 0000000..54f6d62 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor @@ -0,0 +1,296 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Abstractions.Models +@using BTCPayServer.Client.Models +@using BTCPayServer.Payments +@using BTCPayServer.PayoutProcessors +@using Microsoft.AspNetCore.Http +@using Microsoft.AspNetCore.Routing +@using LightningAddressData = BTCPayServer.Data.LightningAddressData +@inject IPluginHookService PluginHookService +@inject LightningAddressService LightningAddressService +@inject PayoutProcessorService PayoutProcessorService +@inject IEnumerable PayoutProcessorFactories +@inject SatBreaker SatBreaker +@inject LinkGenerator LinkGenerator +@inject IHttpContextAccessor httpContextAccessor + +@if (Loading) +{ +} +else +{ +@if (NoPayoutProcessors) +{ + +} +@if (Users?.Any() is not true) +{ + +} + + + + @foreach (var user in Users) + { + + } + +

+ Prism + + + +

+

+ The prism plugin allows automated value splits for your lightning payments. You can set up multiple prisms, each with their own source (which is a lightning address username, or use * as a catch-all for all invoices settled through Lightning, excluding ones which Prism can handle explicitly) and destinations (which are other lightning addresses or lnurls). The plugin will automatically credit the configured percentage of the payment to the destination (while also making sure there is 2% reserved to cater for fees, don't worry, once the lightning node tells us the exact fee amount, we credit/debit the balance after the payment), and once the configured threshold is reached, a payout will be created. Then, a payout processor will run at intervals and process the payout. +

+ + + + +
+
+ +
+ + + +
+
+ + + + How many sats do you want to accumulate per destination before sending? +
+
+ + + + 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) + { + + } +
+ + @if (StatusMessageModel != null) + { +
+ @StatusMessageModel.Message +
+ } +
+
+ + +
+
+
+} + +@code { + public bool Loading { get; set; } = true; + public List Users { get; set; } + public PaymentMethodId pmi { get; set; } = new("BTC", LightningPaymentType.Instance); + public bool NoPayoutProcessors { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + 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()}); + + var fetchSettings = SatBreaker.Get(StoreId); + var fetchLnAddresses = LightningAddressService.Get(new LightningAddressQuery() + { + StoreIds = new[] {StoreId} + }); + var fetchProcessors = PayoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() + { + Stores = new[] {StoreId}, + PaymentMethods = new[] {pmi.ToString()} + }); + + var tasks = new Task[] + { + fetchSettings, + fetchLnAddresses, + fetchProcessors + }; + await Task.WhenAll(tasks); + Settings = await fetchSettings; + Users = await fetchLnAddresses; + + EditContext = new EditContext(Settings); + MessageStore = new ValidationMessageStore(EditContext); + EditContext.OnValidationRequested += Validate; + EditContext.OnFieldChanged += FieldChanged; + SatBreaker.PrismUpdated += SatBreakerOnPrismUpdated; + NoPayoutProcessors = PayoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmi)) && !(await fetchProcessors).Any(); + Loading = false; + await InvokeAsync(StateHasChanged); + } + await base.OnAfterRenderAsync(firstRender); + } + + private void FieldChanged(object sender, FieldChangedEventArgs e) + { + StatusMessageModel = null; + } + + private void SatBreakerOnPrismUpdated(object sender, PrismPaymentDetectedEventArgs e) + { + if (e.Settings != Settings && e.Settings.Version != Settings.Version) + { + Settings.DestinationBalance = e.Settings.DestinationBalance; + Settings.PendingPayouts = e.Settings.PendingPayouts; + Settings.Version = e.Settings.Version; + } + InvokeAsync(StateHasChanged); + } + + private void Validate(object sender, ValidationRequestedEventArgs validationRequestedEventArgs) + { + var previousState = EditContext.GetValidationMessages().Any(); + MessageStore.Clear(); + StatusMessageModel = null; + foreach (var prism in Settings.Splits) + { + if (string.IsNullOrEmpty(prism.Source)) + { + MessageStore.Add(() => prism.Source, "Source is required"); + } + else if (Settings.Splits.Count(s => s.Source == prism.Source) > 1) + { + MessageStore.Add(() => prism.Source, "Sources must be unique"); + } + if (!(prism.Destinations?.Count > 0)) + { + MessageStore.Add(() => prism.Destinations, "At least one destination is required"); + continue; + } + + var sum = prism.Destinations.Sum(d => d.Percentage); + if (sum > 100) + { + MessageStore.Add(() => prism.Destinations, "Destinations must sum up to a 100 maximum"); + } + + 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)) + { + MessageStore.Add(() => destination.Destination, "Destination is not valid"); + } + } + } + if (previousState != EditContext.GetValidationMessages().Any()) + EditContext.NotifyValidationStateChanged(); + } + + private bool ValidateDestination(string dest) + { + try + { + LNURL.LNURL.ExtractUriFromInternetIdentifier(dest); + return true; + } + catch (Exception e) + { + try + { + LNURL.LNURL.Parse(dest, out var tag); + return true; + } + catch (Exception exception) + { + var result = PluginHookService.ApplyFilter("prism-destination-validate", dest).Result; + if (result is true) + return true; + } + } + return false; + } + + public ValidationMessageStore MessageStore { get; set; } + + public EditContext EditContext { get; set; } + public StatusMessageModel StatusMessageModel { get; set; } + + public PrismSettings Settings { get; set; } + + public string PayoutProcessorLink { get; set; } + + public string LNAddressLink { get; set; } + public string PayoutsLink { get; set; } + + [Parameter] + public string StoreId { get; set; } + + private async Task CreateNewPrism() + { + Settings.Splits.Add(new Split()); + await InvokeAsync(StateHasChanged); + } + + private async Task RemovePrism(Split item) + { + if (Settings.Splits.Remove(item)) + { + await InvokeAsync(StateHasChanged); + } + } + + private async Task Save() + { + var settz = await SatBreaker.Get(StoreId); + settz.Splits = Settings.Splits; + settz.Destinations = Settings.Destinations; + settz.Enabled = Settings.Enabled; + settz.SatThreshold = Settings.SatThreshold; + settz.Reserve = Settings.Reserve; + Settings = settz; + var updateResult = await SatBreaker.UpdatePrismSettingsForStore(StoreId, settz); + + if (!updateResult) + { + StatusMessageModel = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "The settings have been updated by another process. Please refresh the page and try again." + }; + } + else + { + StatusMessageModel = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Message = "Successfully saved settings" + }; + } + } + +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor new file mode 100644 index 0000000..0504475 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor @@ -0,0 +1,104 @@ +
+
+ + + +
+ + + + + + + + + + @foreach(var destination in Split.Destinations) + { + + + + + + } + + + @if (Split.Destinations.Count > 1) + { + + + + } + + + + +
+ Destination + Percentage Actions
+ + + + + +
+ + % +
+ + +
+ +
+ Sending @(100 - Leftover)% to @Split.Destinations.Count destinations +
+ + +
+
+ +@code { + [CascadingParameter] + private EditContext CascadedEditContext { get; set; } + + public decimal Leftover => 100 - Split.Destinations.Sum(split => split.Percentage); + + [Parameter] + public Split Split { get; set; } + + [Parameter] + public EventCallback OnRequestRemove { get; set; } + + private void CreateDestination() + { + Split.Destinations.Add(new Prism.PrismSplit()); + } + + private void RemoveSplit() + { + OnRequestRemove.InvokeAsync(Split); + } + + private void UpdateDestinationValue(Prism.PrismSplit destination, object eValue) + { + var newValue = Math.Min(100, Math.Round(Convert.ToDecimal(eValue), 2)); + + var allowedMax = Math.Round(destination.Percentage + Leftover, 2); + + if (newValue > allowedMax) + { + //take the difference from the other destinations proportionally + var difference = Math.Round(newValue - allowedMax, 2); + var otherDestinations = Split.Destinations.Where(split => split != destination).ToList(); + var totalPercentage = Math.Round(otherDestinations.Sum(split => split.Percentage), 2); + foreach (var otherDestination in otherDestinations) + { + var percentage = otherDestination.Percentage; + var newPercentage = Math.Round(percentage - (percentage / totalPercentage * difference), 2); + otherDestination.Percentage = newPercentage; + } + } + destination.Percentage = newValue; + } + +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/ValidationMessage2.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/ValidationMessage2.razor new file mode 100644 index 0000000..dbf877c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/ValidationMessage2.razor @@ -0,0 +1,38 @@ +@using System.Linq.Expressions +@typeparam TValue +@implements IDisposable + +@foreach (var message in EditContext.GetValidationMessages(_fieldIdentifier)) +{ +
+ @message +
+} + +@code { + + [CascadingParameter] + private EditContext EditContext { get; set; } + + [Parameter] + public Expression> For { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? InputAttributes { get; set; } + + private FieldIdentifier _fieldIdentifier; + + protected override void OnInitialized() + { + _fieldIdentifier = FieldIdentifier.Create(For); + EditContext.OnValidationStateChanged += HandleValidationStateChanged; + } + + private void HandleValidationStateChanged(object o, ValidationStateChangedEventArgs args) => StateHasChanged(); + + public void Dispose() + { + EditContext.OnValidationStateChanged -= HandleValidationStateChanged; + } + +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Components/_Imports.razor b/Plugins/BTCPayServer.Plugins.Prism/Components/_Imports.razor new file mode 100644 index 0000000..966c28b --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Prism/Components/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using System.IO \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs b/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs index 51f2237..3a341de 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs @@ -1,12 +1,6 @@ #nullable enable - -using System; -using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Filters; using Microsoft.AspNetCore.Authorization; @@ -19,106 +13,9 @@ namespace BTCPayServer.Plugins.Prism; [ContentSecurityPolicy(CSPTemplate.AntiXSS, UnsafeInline = true)] public class PrismController : Controller { - private readonly SatBreaker _satBreaker; - private readonly IPluginHookService _pluginHookService; - - public PrismController( SatBreaker satBreaker, IPluginHookService pluginHookService) - { - _satBreaker = satBreaker; - _pluginHookService = pluginHookService; - } - [HttpGet] - public async Task Edit(string storeId) + public async Task Edit() { - var settings =await _satBreaker.Get(storeId); - return View(settings ); + return View(); } - - [HttpPost] - public async Task Edit(string storeId, PrismSettings settings, string command) - { - - for (var i = 0; i < settings.Splits?.Length; i++) - { - var prism = settings.Splits[i]; - if (string.IsNullOrEmpty(prism.Source)) - { - ModelState.AddModelError($"Splits[{i}].Source", "Source is required"); - } - else if(settings.Splits.Count(s => s.Source == prism.Source) > 1) - { - ModelState.AddModelError($"Splits[{i}].Source", "Sources must be unique"); - } - if (!(prism.Destinations?.Length > 0)) - { - - ModelState.AddModelError($"Splits[{i}].Destinations", "At least one destination is required"); - continue; - } - - var sum = prism.Destinations.Sum(d => d.Percentage); - if (sum > 100) - { - - ModelState.AddModelError($"Splits[{i}].Destinations", "Destinations must sum up to a 100 maximum"); - } - - for (int j = 0; j < prism.Destinations?.Length; j++) - { - var dest = prism.Destinations[j].Destination; - //check that the source is a valid internet identifier, which is username@domain(and optional port) - if (string.IsNullOrEmpty(dest)) - { - ModelState.AddModelError($"Splits[{i}].Destinations[{j}].Destination", "Destination is required"); - continue; - } - - try - { - - LNURL.LNURL.ExtractUriFromInternetIdentifier(dest); - } - catch (Exception e) - { - try - { - LNURL.LNURL.Parse(dest, out var tag); - } - catch (Exception exception) - { - var result = await _pluginHookService.ApplyFilter("prism-destination-validate", dest); - if(result is not true) - ModelState.AddModelError($"Splits[{i}].Destinations[{j}].Destination", "Destination is not a valid LN address or LNURL"); - } - } - } - - } - - - if (!ModelState.IsValid) - { - return View(settings); - } - - var settz = await _satBreaker.Get(storeId); - settz.Splits = settings.Splits; - settz.Enabled = settings.Enabled; - settz.SatThreshold = settings.SatThreshold; - var updateResult = await _satBreaker.UpdatePrismSettingsForStore(storeId, settz); - if (!updateResult) - { - ModelState.AddModelError("VersionConflict", "The settings have been updated by another process. Please refresh the page and try again."); - - return View(settings); - } - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Severity = StatusMessageModel.StatusSeverity.Success, - Message = "Successfully saved settings" - }); - return RedirectToAction("Edit", new {storeId}); - } - } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/PrismPlugin.cs b/Plugins/BTCPayServer.Plugins.Prism/PrismPlugin.cs index df1dd9f..36fd5fc 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/PrismPlugin.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/PrismPlugin.cs @@ -24,4 +24,13 @@ public class PrismPlugin : BaseBTCPayServerPlugin applicationBuilder.AddHostedService(provider => provider.GetRequiredService()); base.Execute(applicationBuilder); } + + public override void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) + { + applicationBuilder.UseStaticFiles(); + applicationBuilder.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + }); + } } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/PrismSettings.cs b/Plugins/BTCPayServer.Plugins.Prism/PrismSettings.cs index 3ae8e0d..b6df62f 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/PrismSettings.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/PrismSettings.cs @@ -7,8 +7,18 @@ public class PrismSettings public bool Enabled { get; set; } public Dictionary DestinationBalance { get; set; } = new(); - public Split[] Splits { get; set; } + public List Splits { get; set; } = new(); public Dictionary PendingPayouts { get; set; } = new(); + public Dictionary Destinations { get; set; } = new(); public long SatThreshold { get; set; } = 100; public ulong Version { get; set; } = 0; + public decimal Reserve { get; set; } = 2; +} + +public class PrismDestination +{ + public string Destination { get; set; } + public decimal? Reserve { get; set; } + public long? SatThreshold { get; set; } + public string? PaymentMethodId { get; set; } } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/PrismSplit.cs b/Plugins/BTCPayServer.Plugins.Prism/PrismSplit.cs index fd4f2cf..4575f0b 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/PrismSplit.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/PrismSplit.cs @@ -1,3 +1,23 @@ namespace BTCPayServer.Plugins.Prism; -public record PrismSplit(decimal Percentage, string Destination); \ No newline at end of file +public class PrismSplit +{ + public PrismSplit() + { + + } + public PrismSplit(decimal Percentage, string Destination) + { + this.Percentage = Percentage; + this.Destination = Destination; + } + + public decimal Percentage { get; set; } + public string Destination { get; set; } + + public void Deconstruct(out decimal Percentage, out string Destination) + { + Percentage = this.Percentage; + Destination = this.Destination; + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs index bb375c7..4262c3b 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs @@ -18,6 +18,8 @@ using BTCPayServer.Services.Stores; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using LightningAddressData = BTCPayServer.Data.LightningAddressData; namespace BTCPayServer.Plugins.Prism @@ -39,6 +41,7 @@ namespace BTCPayServer.Plugins.Prism private readonly IPluginHookService _pluginHookService; private Dictionary _prismSettings; + public event EventHandler PrismUpdated; public SatBreaker(StoreRepository storeRepository, EventAggregator eventAggregator, ILogger logger, @@ -202,7 +205,7 @@ namespace BTCPayServer.Plugins.Prism public async Task Get(string storeId) { - return _prismSettings.TryGetValue(storeId, out var settings) ? settings : new PrismSettings(); + return JObject.FromObject(_prismSettings.TryGetValue(storeId, out var settings) ? settings : new PrismSettings()).ToObject(); } public async Task UpdatePrismSettingsForStore(string storeId, PrismSettings updatedSettings, @@ -233,7 +236,13 @@ namespace BTCPayServer.Plugins.Prism if (!skipLock) _updateLock.Release(); } - + + var prismPaymentDetectedEventArgs = new PrismPaymentDetectedEventArgs() + { + StoreId = storeId, + Settings = updatedSettings + }; + PrismUpdated?.Invoke(this, prismPaymentDetectedEventArgs); return true; // Indicate that the update succeeded } @@ -374,17 +383,22 @@ namespace BTCPayServer.Plugins.Prism var result = false; foreach (var (destination, amtMsats) in prismSettings.DestinationBalance) { + prismSettings.Destinations.TryGetValue(destination, out var destinationSettings); + var satThreshold = destinationSettings?.SatThreshold ?? prismSettings.SatThreshold; + var reserve = destinationSettings?.Reserve ?? prismSettings.Reserve; + var amt = amtMsats / 1000; - if (amt >= prismSettings.SatThreshold) + if (amt >= satThreshold) { - var reserveFee = (long) Math.Max(1, Math.Round(amt * 0.02, 0, MidpointRounding.AwayFromZero)); + var percentage = reserve / 100; + var reserveFee = (long) Math.Max(0, Math.Round(amt * percentage, 0, MidpointRounding.AwayFromZero)); var payoutAmount = amt - reserveFee; if (payoutAmount <= 0) { continue; } IClaimDestination dest = null; - var dest2 = await _pluginHookService.ApplyFilter("prism-claim-destination", destination); + var dest2 = await _pluginHookService.ApplyFilter("prism-claim-destination", destinationSettings?.Destination??destination); dest = dest2 switch { @@ -397,12 +411,17 @@ namespace BTCPayServer.Plugins.Prism { continue; } + + var pmi = string.IsNullOrEmpty(destinationSettings?.PaymentMethodId) || + !PaymentMethodId.TryParse(destinationSettings?.PaymentMethodId, out var pmi2) + ? new PaymentMethodId("BTC", LightningPaymentType.Instance) + : pmi2; var payout = await _pullPaymentHostedService.Claim(new ClaimRequest() { Destination = dest, PreApprove = true, StoreId = storeId, - PaymentMethodId = new PaymentMethodId("BTC", LightningPaymentType.Instance), + PaymentMethodId = pmi, Value = Money.Satoshis(payoutAmount).ToDecimal(MoneyUnit.BTC), }); if (payout.Result == ClaimRequest.ClaimResult.Ok) @@ -420,4 +439,10 @@ namespace BTCPayServer.Plugins.Prism return result; } } + + public class PrismPaymentDetectedEventArgs + { + public string StoreId { get; set; } + public PrismSettings Settings { get; set; } + } } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Split.cs b/Plugins/BTCPayServer.Plugins.Prism/Split.cs index 936f638..b71d622 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/Split.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/Split.cs @@ -1,3 +1,25 @@ -namespace BTCPayServer.Plugins.Prism; +using System.Collections.Generic; -public record Split(string Source, PrismSplit[] Destinations); \ No newline at end of file +namespace BTCPayServer.Plugins.Prism; + +public class Split +{ + public Split() + { + + } + public Split(string Source, List Destinations) + { + this.Source = Source; + this.Destinations = Destinations; + } + + public string Source { get; set; } + public List Destinations { get; init; } = new(); + + public void Deconstruct(out string Source, out List Destinations) + { + Source = this.Source; + Destinations = this.Destinations; + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Prism/Views/Prism/Edit.cshtml b/Plugins/BTCPayServer.Plugins.Prism/Views/Prism/Edit.cshtml index be5a050..709a421 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/Views/Prism/Edit.cshtml +++ b/Plugins/BTCPayServer.Plugins.Prism/Views/Prism/Edit.cshtml @@ -1,330 +1,20 @@ @using BTCPayServer.Abstractions.TagHelpers -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using System.Linq -@using BTCPayServer -@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Extensions -@using BTCPayServer.Payments -@using BTCPayServer.PayoutProcessors -@inject LightningAddressService LightningAddressService +@using BTCPayServer.Components.UIExtensionPoint +@using BTCPayServer.Plugins.Prism.Components +@using BTCPayServer.Abstractions.Contracts @inject IScopeProvider ScopeProvider -@inject IEnumerable PayoutProcessorFactories -@inject PayoutProcessorService PayoutProcessorService -@model BTCPayServer.Plugins.Prism.PrismSettings @{ - var users = await LightningAddressService.Get(new LightningAddressQuery() - { - StoreIds = new[] {ScopeProvider.GetCurrentStoreId()} - }); ViewData.SetActivePage("Prism", "Prism", "Prism"); - var pmi = new PaymentMethodId("BTC", LightningPaymentType.Instance); } -@if (PayoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmi)) && !(await PayoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() -{ - Stores = new[] {ScopeProvider.GetCurrentStoreId()}, - PaymentMethods = new[] {pmi.ToString()} -})).Any()) -{ - +@section PageHeadContent { + } -@if (!users.Any()) +@(await Html.RenderComponentAsync(RenderMode.ServerPrerendered, new { - -} + StoreId = ScopeProvider.GetCurrentStoreId(), +})) - - -@if (ViewData.ModelState.TryGetValue("VersionConflict", out var versionConflict)) -{ -
@versionConflict.Errors.First().ErrorMessage
-} - - -

@ViewData["Title"] - - - -

-

- The prism plugin allows automated value splits for your lightning payments. You can set up multiple prisms, each with their own source (which is a lightning address username, or use * as a catch-all for all invoices settled through Lightning, excluding ones which Prism can handle explicitly) and destinations (which are other lightning addresses or lnurls). The plugin will automatically credit the configured percentage of the payment to the destination (while also making sure there is 2% reserved to cater for fees, don't worry, once the lightning node tells us the exact fee amount, we credit/debit the balance after the payment), and once the configured threshold is reached, a payout will be created. Then, a payout processor will run at intervals and process the payout. -

- - - @foreach (var user in users) - { - - } - - -
-
-
-
-
- -
- - - -
-
- - - - How many sats do you want to accumulate per destination before sending? -
-
-
- -
- @for (int i = 0; i < Model.Splits?.Length; i++) - { -
-
- - - - - -
- - - - - - - - - - @for (var x = 0; x < Model.Splits[i].Destinations?.Length; x++) - { - - - - - - } - - - - - - - - -
- Destination - Percentage Actions
-
- - -
-
-
- - @Model.Splits[i].Destinations[x].Percentage% - -
-
- -
- - -
-
- } -
-
-
- - -
-
-
-
- @if (Model.DestinationBalance?.Any() is true) - { -
-

Destination Balances

- - - - - - @foreach (var (dest, balance) in Model.DestinationBalance) - { - - - - - } -
DestinationSats
@dest@(balance / 1000m)
-
- } - - @if (Model.PendingPayouts?.Any() is true) - { -
-

Pending Payouts

- - - - - - - @foreach (var (payoutId, pendingPayout) in Model.PendingPayouts) - { - - - - - - } -
Payout IdReserve feeAmount
@payoutId@pendingPayout.FeeCharged@pendingPayout.PayoutAmount
-
- } -
-
-
- - - - - + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj index 06cc4a0..338220c 100644 --- a/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj +++ b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj @@ -9,7 +9,7 @@ SideShift Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins. - 1.0.9 + 1.1.0 diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs index 566ff50..394dfe3 100644 --- a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs @@ -16,7 +16,7 @@ namespace BTCPayServer.Plugins.SideShift { public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = { - new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.4"} + new() {Identifier = nameof(BTCPayServer), Condition = ">=1.10.0"} }; public override void Execute(IServiceCollection applicationBuilder) diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/PrismEnhance.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/PrismEnhance.cshtml index 2dd2827..9738cab 100644 --- a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/PrismEnhance.cshtml +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/PrismEnhance.cshtml @@ -17,9 +17,8 @@ var availableCoins = coins.SelectMany(coin => coin.networks.Select(s => (Coin: coin, Network: s))) .Where(tuple => (tuple.Coin.fixedOnly.Type == JTokenType.Boolean && !tuple.Coin.fixedOnly.Value()) || ( tuple.Coin.fixedOnly is JArray varOnlyArray && varOnlyArray.All(v => v.Value() != tuple.Network))).ToList(); - } - +