mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
updates
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
<PropertyGroup>
|
||||
<Product>LN Prism</Product>
|
||||
<Description>Automated value splits for lightning.</Description>
|
||||
<Version>1.0.9</Version>
|
||||
<Version>1.1.0</Version>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="row mt-4">
|
||||
@if (DestinationBalance?.Any() is true)
|
||||
{
|
||||
<div class="col-sm-12 col-md-5 col-xxl-constrain border border-light">
|
||||
<h4 class="text-center p-2">Destination Pending Balances</h4>
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th>Destination</th>
|
||||
<th>Sats</th>
|
||||
</tr>
|
||||
@foreach (var (dest, balance) in DestinationBalance)
|
||||
{
|
||||
<tr>
|
||||
<td>@dest</td>
|
||||
<td>@(balance / 1000m)</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (PendingPayouts?.Any() is true)
|
||||
{
|
||||
<div class="col-sm-12 col-md-5 offset-md-1 col-xxl-constrain border border-light">
|
||||
<h4 class="text-center p-2">Pending Payouts</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Payout Id</th>
|
||||
<th>Reserve fee</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
@foreach (var (payoutId, pendingPayout) in PendingPayouts)
|
||||
{
|
||||
<tr>
|
||||
<td>@payoutId</td>
|
||||
<td>@pendingPayout.FeeCharged</td>
|
||||
<td>@pendingPayout.PayoutAmount</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, long> DestinationBalance { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, PendingPayout> PendingPayouts { get; set; }
|
||||
|
||||
}
|
||||
296
Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor
Normal file
296
Plugins/BTCPayServer.Plugins.Prism/Components/PrismEdit.razor
Normal file
@@ -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<IPayoutProcessorFactory> PayoutProcessorFactories
|
||||
@inject SatBreaker SatBreaker
|
||||
@inject LinkGenerator LinkGenerator
|
||||
@inject IHttpContextAccessor httpContextAccessor
|
||||
|
||||
@if (Loading)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (NoPayoutProcessors)
|
||||
{
|
||||
<div class="alert alert-warning mb-5" role="alert">
|
||||
An automated payout procesor for Lightning is required in order to automate prism payouts.
|
||||
<a class="alert-link p-0" href="@PayoutProcessorLink">Configure now</a>
|
||||
</div>
|
||||
}
|
||||
@if (Users?.Any() is not true)
|
||||
{
|
||||
<div class="alert alert-warning mb-5" role="alert">
|
||||
Prisms can currently mostly work on lightning addresses that are owned by the store. Please create a lightning address for the store.
|
||||
<a class="alert-link p-0" href="@LNAddressLink">Configure now</a>. <br/><br/>Alternatively, you can use * as the source, which will match any settled invoice as long as it was paid through Lightning.
|
||||
</div>
|
||||
}
|
||||
|
||||
<datalist id="users">
|
||||
<option value="*"></option>
|
||||
@foreach (var user in Users)
|
||||
{
|
||||
<option value="@user.Username"></option>
|
||||
}
|
||||
</datalist>
|
||||
<h2 class="mb-4">
|
||||
Prism
|
||||
<a href="https://dergigi.com/2023/03/12/lightning-prisms/" class="ms-1" target="_blank" rel="noreferrer noopener">
|
||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-muted">
|
||||
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 <a href="@LNAddressLink">lightning address username</a>, 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 <a href="@PayoutsLink">payout</a> will be created. Then, a <a href="@PayoutProcessorLink">payout processor</a> will run at intervals and process the payout.
|
||||
</p>
|
||||
|
||||
|
||||
<EditForm EditContext="EditContext" OnValidSubmit="Save">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
|
||||
<div class="form-group form-check">
|
||||
<input @bind="Settings.Enabled" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="Enabled" class="form-check-label">Enabled</label>
|
||||
<ValidationMessage2 For="() => Settings.Enabled" class="text-danger"></ValidationMessage2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sat Threshold</label>
|
||||
<input type="number" @bind="Settings.SatThreshold" min="1" class="form-control"/>
|
||||
<ValidationMessage2 For="() => Settings.SatThreshold" class="text-danger"></ValidationMessage2>
|
||||
<span class="text-muted">How many sats do you want to accumulate per destination before sending?</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Reserve fee</label>
|
||||
<input type="number" @bind="Settings.Reserve" min="0" max="100" class="form-control"/>
|
||||
<ValidationMessage2 For="() => Settings.Reserve" class="text-danger"></ValidationMessage2>
|
||||
<span class="text-muted">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.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" id="prism-holder">
|
||||
@foreach (var item in Settings.Splits)
|
||||
{
|
||||
<PrismSplit Split="@item" OnRequestRemove="@RemovePrism"/>
|
||||
}
|
||||
</div>
|
||||
<PrismBalances DestinationBalance="Settings.DestinationBalance" PendingPayouts="Settings.PendingPayouts"></PrismBalances>
|
||||
@if (StatusMessageModel != null)
|
||||
{
|
||||
<div class="alert alert-@StatusMessageModel.ToString(StatusMessageModel.Severity)">
|
||||
@StatusMessageModel.Message
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-primary mx-2" id="add-prism" @onclick="CreateNewPrism">Add Prism</button>
|
||||
<button type="submit" class="btn btn-primary mx-2">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@code {
|
||||
public bool Loading { get; set; } = true;
|
||||
public List<LightningAddressData> 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
104
Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor
Normal file
104
Plugins/BTCPayServer.Plugins.Prism/Components/PrismSplit.razor
Normal file
@@ -0,0 +1,104 @@
|
||||
<div class="prism col-sm-12 col-xl-10 col-xxl-constrain border border-light p-2 m-1">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Source</label>
|
||||
<input type="text" @bind="Split.Source" list="users" class="form-control src"/>
|
||||
<ValidationMessage2 For="() => Split.Source" class="text-danger"></ValidationMessage2>
|
||||
</div>
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Destination
|
||||
</th>
|
||||
<th> Percentage</th>
|
||||
<th> Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var destination in Split.Destinations)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" @bind="@destination.Destination" class="form-control dest"/>
|
||||
|
||||
<ValidationMessage2 For="() => destination.Destination" class="text-danger"></ValidationMessage2>
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" value="@destination.Percentage" @oninput="@((e) => UpdateDestinationValue(destination, e.Value))" min="0" step='0.01' class="form-range" max="100"/>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" step='0.01' value="@destination.Percentage" @onchange="@((e) => UpdateDestinationValue(destination, e.Value))" class="form-control form-control-sm"/>
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
|
||||
<ValidationMessage2 For="() => destination.Percentage" class="text-danger"></ValidationMessage2>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-link remove-dest btn-danger" type="button" @onclick="@(() => Split.Destinations.Remove(destination))">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
@if (Split.Destinations.Count > 1)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Sending @(100 - Leftover)% to @Split.Destinations.Count destinations
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="btn btn-link add-dest" type="button" @onclick="CreateDestination">Add</button>
|
||||
<button class="btn btn-link remove-prism" type="button" @onclick="RemoveSplit">Remove Prism</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@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<Split> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@using System.Linq.Expressions
|
||||
@typeparam TValue
|
||||
@implements IDisposable
|
||||
|
||||
@foreach (var message in EditContext.GetValidationMessages(_fieldIdentifier))
|
||||
{
|
||||
<div @attributes="InputAttributes">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[CascadingParameter]
|
||||
private EditContext EditContext { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Expression<Func<TValue>> For { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object>? 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<IActionResult> Edit(string storeId)
|
||||
public async Task<IActionResult> Edit()
|
||||
{
|
||||
var settings =await _satBreaker.Get(storeId);
|
||||
return View(settings );
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> 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});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,4 +24,13 @@ public class PrismPlugin : BaseBTCPayServerPlugin
|
||||
applicationBuilder.AddHostedService(provider => provider.GetRequiredService<SatBreaker>());
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
|
||||
public override void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices)
|
||||
{
|
||||
applicationBuilder.UseStaticFiles();
|
||||
applicationBuilder.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapBlazorHub();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,18 @@ public class PrismSettings
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public Dictionary<string, long> DestinationBalance { get; set; } = new();
|
||||
public Split[] Splits { get; set; }
|
||||
public List<Split> Splits { get; set; } = new();
|
||||
public Dictionary<string, PendingPayout> PendingPayouts { get; set; } = new();
|
||||
public Dictionary<string, PrismDestination> 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; }
|
||||
}
|
||||
@@ -1,3 +1,23 @@
|
||||
namespace BTCPayServer.Plugins.Prism;
|
||||
|
||||
public record PrismSplit(decimal Percentage, string Destination);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, PrismSettings> _prismSettings;
|
||||
|
||||
public event EventHandler<PrismPaymentDetectedEventArgs> PrismUpdated;
|
||||
public SatBreaker(StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
ILogger<SatBreaker> logger,
|
||||
@@ -202,7 +205,7 @@ namespace BTCPayServer.Plugins.Prism
|
||||
|
||||
public async Task<PrismSettings> 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<PrismSettings>();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePrismSettingsForStore(string storeId, PrismSettings updatedSettings,
|
||||
@@ -234,6 +237,12 @@ namespace BTCPayServer.Plugins.Prism
|
||||
_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; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,25 @@
|
||||
namespace BTCPayServer.Plugins.Prism;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public record Split(string Source, PrismSplit[] Destinations);
|
||||
namespace BTCPayServer.Plugins.Prism;
|
||||
|
||||
public class Split
|
||||
{
|
||||
public Split()
|
||||
{
|
||||
|
||||
}
|
||||
public Split(string Source, List<PrismSplit> Destinations)
|
||||
{
|
||||
this.Source = Source;
|
||||
this.Destinations = Destinations;
|
||||
}
|
||||
|
||||
public string Source { get; set; }
|
||||
public List<PrismSplit> Destinations { get; init; } = new();
|
||||
|
||||
public void Deconstruct(out string Source, out List<PrismSplit> Destinations)
|
||||
{
|
||||
Source = this.Source;
|
||||
Destinations = this.Destinations;
|
||||
}
|
||||
}
|
||||
@@ -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<IPayoutProcessorFactory> 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())
|
||||
{
|
||||
<div class="alert alert-warning mb-5" role="alert">
|
||||
An automated payout procesor for Lightning is required in order to automate prism payouts.
|
||||
<a class="alert-link p-0" asp-action="ConfigureStorePayoutProcessors" asp-controller="UIPayoutProcessors" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">Configure now</a>
|
||||
</div>
|
||||
@section PageHeadContent {
|
||||
<base href="~/"/>
|
||||
}
|
||||
@if (!users.Any())
|
||||
@(await Html.RenderComponentAsync<PrismEdit>(RenderMode.ServerPrerendered, new
|
||||
{
|
||||
<div class="alert alert-warning mb-5" role="alert">
|
||||
Prisms can currently mostly work on lightning addresses that are owned by the store. Please create a lightning address for the store.
|
||||
<a class="alert-link p-0" asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">Configure now</a>. <br/><br/>Alternatively, you can use * as the source, which will match any settled invoice as long as it was paid through Lightning.
|
||||
</div>
|
||||
}
|
||||
StoreId = ScopeProvider.GetCurrentStoreId(),
|
||||
}))
|
||||
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
@if (ViewData.ModelState.TryGetValue("VersionConflict", out var versionConflict))
|
||||
{
|
||||
<div class="alert alert-danger">@versionConflict.Errors.First().ErrorMessage</div>
|
||||
}
|
||||
|
||||
|
||||
<h2 class="mb-4">@ViewData["Title"]
|
||||
<a href="https://dergigi.com/2023/03/12/lightning-prisms/" class="ms-1" target="_blank" rel="noreferrer noopener">
|
||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-muted">
|
||||
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 <a asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">lightning address username</a>, 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 <a asp-action="Payouts" asp-controller="UIStorePullPayments" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()" asp-route-payoutState="AwaitingPayment" asp-route-paymentMethodId="@pmi.ToString()">payout</a> will be created. Then, a <a asp-action="ConfigureStorePayoutProcessors" asp-controller="UIPayoutProcessors" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">payout processor</a> will run at intervals and process the payout.
|
||||
</p>
|
||||
|
||||
<datalist id="users">
|
||||
@foreach (var user in users)
|
||||
{
|
||||
<option value="@user.Username"></option>
|
||||
}
|
||||
</datalist>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
|
||||
<div class="form-group form-check">
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="Enabled" class="form-check-label"></label>
|
||||
<span asp-validation-for="Enabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SatThreshold" class="form-label">Sat Threshold</label>
|
||||
<input type="number" asp-for="SatThreshold" class="form-control"/>
|
||||
<span asp-validation-for="SatThreshold" class="text-danger"></span>
|
||||
<span class="text-muted">How many sats do you want to accumulate per destination before sending?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="prism-holder">
|
||||
@for (int i = 0; i < Model.Splits?.Length; i++)
|
||||
{
|
||||
<div class="prism col-sm-12 col-md-5 border border-light p-2 m-1">
|
||||
<div class="form-group">
|
||||
<label asp-for="Splits[i].Source" class="form-label"></label>
|
||||
<input type="text" asp-for="Splits[i].Source" list="users" class="form-control src"/>
|
||||
<span asp-validation-for="Splits[i].Source" class="text-danger "></span>
|
||||
|
||||
<span asp-validation-for="Splits[i].Destinations" class="text-danger w-100"></span>
|
||||
</div>
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Destination
|
||||
</th>
|
||||
<th> Percentage</th>
|
||||
<th> Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (var x = 0; x < Model.Splits[i].Destinations?.Length; x++)
|
||||
{
|
||||
<tr class="dest">
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<input type="text" asp-for="Splits[i].Destinations[x].Destination" class="form-control"/>
|
||||
<span asp-validation-for="Splits[i].Destinations[x].Destination" class="text-danger"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<input type="range" asp-for="Splits[i].Destinations[x].Percentage" class="form-range" min="0" max="100"/>
|
||||
<output>@Model.Splits[i].Destinations[x].Percentage%</output>
|
||||
<span asp-validation-for="Splits[i].Destinations[x].Percentage" class="text-danger"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="remove-dest btn btn-link">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="btn btn-link add-dest" type="button">Add</button>
|
||||
<button class="btn btn-link remove-prism" type="button">Remove Prism</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex">
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary mx-2">Submit</button>
|
||||
<button type="button" class="btn btn-primary mx-2" id="add-prism">Add Prism</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row mt-4">
|
||||
@if (Model.DestinationBalance?.Any() is true)
|
||||
{
|
||||
<div class="col-sm-12 col-md-5 col-xxl-constrain border border-light">
|
||||
<h4 class="text-center p-2">Destination Balances</h4>
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th>Destination</th>
|
||||
<th>Sats</th>
|
||||
</tr>
|
||||
@foreach (var (dest, balance) in Model.DestinationBalance)
|
||||
{
|
||||
<tr>
|
||||
<td>@dest</td>
|
||||
<td>@(balance / 1000m)</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.PendingPayouts?.Any() is true)
|
||||
{
|
||||
<div class="col-sm-12 col-md-5 col-xxl-constrain border border-light">
|
||||
<h4 class="text-center p-2">Pending Payouts</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Payout Id</th>
|
||||
<th>Reserve fee</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
@foreach (var (payoutId, pendingPayout) in Model.PendingPayouts)
|
||||
{
|
||||
<tr>
|
||||
<td>@payoutId</td>
|
||||
<td>@pendingPayout.FeeCharged</td>
|
||||
<td>@pendingPayout.PayoutAmount</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="prism">
|
||||
|
||||
<div class="prism col-sm-12 col-md-5 border border-light p-2 m-1">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Source</label>
|
||||
<input type="text" name="Splits[i].Source" list="users" class="form-control src"/>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Destination
|
||||
</th>
|
||||
<th> Percentage</th>
|
||||
<th> Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="btn btn-link add-dest" type="button">Add</button>
|
||||
<button class="btn btn-link remove-prism" type="button">Remove Prism</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template id="split">
|
||||
<tr class="dest">
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<input type="text" name="Splits[i].Destinations[x].Destination" class="form-control"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<input type="range" name="Splits[i].Destinations[x].Percentage" class="form-range" min="0" max="100" value="0"/>
|
||||
<output>0%</output>
|
||||
<span class="text-danger"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="remove-dest btn btn-link">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script >
|
||||
document.addEventListener("DOMContentLoaded", ()=>{
|
||||
|
||||
setupDests();
|
||||
|
||||
document.getElementById("add-prism").addEventListener("click", ()=>{
|
||||
const template = document.querySelector('#prism');
|
||||
const clone = template.content.cloneNode(true);
|
||||
const prismholder = document.getElementById("prism-holder");
|
||||
prismholder.appendChild(clone);
|
||||
const el = prismholder.lastElementChild
|
||||
setIndex();
|
||||
el.querySelectorAll(".add-dest").forEach(value =>{
|
||||
value.addEventListener("click",onAddDest );
|
||||
})
|
||||
el.querySelectorAll(".remove-prism").forEach(value =>{
|
||||
value.addEventListener("click",onRemovePrism );
|
||||
})
|
||||
});
|
||||
|
||||
function onRemoveDest(evt){
|
||||
evt.target.parentElement.parentElement.remove();
|
||||
setIndex();
|
||||
}
|
||||
function onRemovePrism(evt){
|
||||
debugger;
|
||||
evt.target.parentElement.parentElement.parentElement.parentElement.parentElement.remove();
|
||||
setIndex();
|
||||
}
|
||||
function onUpdateValue(evt){
|
||||
evt.target.nextElementSibling.value = evt.target.value + "%";
|
||||
setIndex();
|
||||
}
|
||||
|
||||
function onAddDest(evt){
|
||||
const template = document.querySelector('#split');
|
||||
const clone = template.content.cloneNode(true);
|
||||
evt.target.parentElement.parentElement.parentElement.previousElementSibling.appendChild(clone)
|
||||
setIndex();
|
||||
const el = evt.target.parentElement.parentElement.parentElement.previousElementSibling.lastElementChild;
|
||||
el.querySelectorAll(".remove-dest").forEach(value => {
|
||||
value.addEventListener("click",onRemoveDest );
|
||||
});
|
||||
el.querySelectorAll("input[type=range]").forEach(value =>{
|
||||
|
||||
value.addEventListener("input",onUpdateValue );
|
||||
});
|
||||
}
|
||||
|
||||
function setupDests(){
|
||||
document.querySelectorAll(".remove-dest").forEach(value =>{
|
||||
value.removeEventListener("click",onRemoveDest )
|
||||
value.addEventListener("click",onRemoveDest );
|
||||
});
|
||||
|
||||
document.querySelectorAll(".add-dest").forEach(value =>{
|
||||
|
||||
value.removeEventListener("click",onAddDest )
|
||||
value.addEventListener("click",onAddDest );
|
||||
})
|
||||
document.querySelectorAll(".remove-prism").forEach(value =>{
|
||||
value.addEventListener("click",onRemovePrism );
|
||||
})
|
||||
document.querySelectorAll("input[type=range]").forEach(value =>{
|
||||
|
||||
value.addEventListener("input",onUpdateValue );
|
||||
});
|
||||
}
|
||||
|
||||
function setIndex(){
|
||||
document.querySelectorAll(".prism").forEach((prism, key) => {
|
||||
|
||||
prism.querySelector("input.src").name = `Splits[${key}].Source`;
|
||||
prism.querySelectorAll("tr.dest").forEach((value, key2) => {
|
||||
value.setAttribute("data-index", key);
|
||||
value.querySelectorAll("input").forEach(value1 => {
|
||||
value1.name = `Splits[${key}].Destinations[${key2}].${value1.name.split(".").pop()}`;
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
<vc:ui-extension-point location="prism-edit" model="@Model"></vc:ui-extension-point>
|
||||
@@ -9,7 +9,7 @@
|
||||
<PropertyGroup>
|
||||
<Product>SideShift</Product>
|
||||
<Description>Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins.</Description>
|
||||
<Version>1.0.9</Version>
|
||||
<Version>1.1.0</Version>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<bool>()) || (
|
||||
tuple.Coin.fixedOnly is JArray varOnlyArray && varOnlyArray.All(v => v.Value<string>() != tuple.Network))).ToList();
|
||||
|
||||
}
|
||||
|
||||
<button type="button" class="btn btn-primary btn-sm mt-4" data-bs-toggle="modal" data-bs-target="#sideshiftModal" >Generate SideShift destination</button>
|
||||
<script>
|
||||
|
||||
const ssAvailableCoins = @Json.Serialize(availableCoins.ToDictionary(tuple=> $"{tuple.Coin.coin}_{tuple.Network}",tuple =>
|
||||
@@ -30,15 +29,14 @@ const ssAvailableCoins = @Json.Serialize(availableCoins.ToDictionary(tuple=> $"{
|
||||
network = tuple.Network
|
||||
}));
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const sideshiftDestinationButton = document.createElement("button");
|
||||
sideshiftDestinationButton.type= "button";
|
||||
sideshiftDestinationButton.className = "btn btn-primary btn-sm";
|
||||
sideshiftDestinationButton.innerText = "Generate SideShift destination";
|
||||
// const sideshiftDestinationButton = document.createElement("button");
|
||||
// sideshiftDestinationButton.type= "button";
|
||||
// sideshiftDestinationButton.className = "btn btn-primary btn-sm";
|
||||
// sideshiftDestinationButton.innerText = "Generate SideShift destination";
|
||||
// document.getElementById("add-prism").insertAdjacentElement("afterend", sideshiftDestinationButton);
|
||||
|
||||
document.getElementById("add-prism").insertAdjacentElement("afterend", sideshiftDestinationButton);
|
||||
|
||||
const modal = new bootstrap.Modal('#sideshiftModal');
|
||||
sideshiftDestinationButton.addEventListener("click", ev => modal.show());
|
||||
// const modal = new bootstrap.Modal('#sideshiftModal');
|
||||
// sideshiftDestinationButton.addEventListener("click", ev => modal.show());
|
||||
const selectedSideShiftCoin = document.getElementById("sscoin");
|
||||
const specifiedSideShiftDestination = document.getElementById("ssdest");
|
||||
const specifiedSideShiftMemo= document.getElementById("ssmemo");
|
||||
@@ -76,18 +74,52 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
}
|
||||
};
|
||||
selectedSideShiftCoin.addEventListener("change", ev1 => {
|
||||
|
||||
|
||||
handleSelectChanges();
|
||||
});
|
||||
shiftButton.addEventListener("click", ev1 => {
|
||||
|
||||
document.getElementById("ss-server-errors").innerHTML = "";
|
||||
document.getElementById("ss-result-txt").value = "";
|
||||
document.getElementById("ss-result-additional-info").value = "";
|
||||
if (isValid()){
|
||||
|
||||
shiftButton.setAttribute("disabled", "disabled");
|
||||
const type = "permanent";
|
||||
|
||||
if (type ==="permanent"){
|
||||
fetch("https://sideshift.ai/api/v2/shifts/variable",{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settleAddress: specifiedSideShiftDestination.value,
|
||||
settleMemo: specifiedSideShiftMemo.value,
|
||||
affiliateId: "qg0OrfHJV",
|
||||
depositCoin : "BTC",
|
||||
depositNetwork : "lightning",
|
||||
settleCoin: selectedCoin.code,
|
||||
settleNetwork: selectedCoin.network,
|
||||
permanent: true
|
||||
})})
|
||||
.then(async response => {
|
||||
if (!response.ok){
|
||||
try {
|
||||
document.getElementById("ss-server-errors").innerHTML = (await response.json())["error"]["message"];
|
||||
}catch{
|
||||
document.getElementById("ss-server-errors").innerHTML = JSON.stringify((await response.json()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const shift = await response.json();
|
||||
document.getElementById("ss-result").style.display = "block";
|
||||
document.getElementById("ss-result-txt").value = shift.depositAddress;
|
||||
const link = `https://sideshift.ai/orders/${shift.id}`;
|
||||
document.getElementById("ss-result-additional-info").innerHTML = "<b>IMPORTANT:</b> You must keep this link to be able to recover your funds in case of a problem. <a href='"+link+"' target='_blank'>"+link+"</a> ";
|
||||
|
||||
})
|
||||
.catch(error => document.getElementById("ss-server-errors").innerHTML = error)
|
||||
.finally(() => shiftButton.removeAttribute("disabled"));
|
||||
}else{
|
||||
document.getElementById("ss-result").style.display = "block";
|
||||
document.getElementById("ss-result-txt").value = "sideshift:"+JSON.stringify({
|
||||
shiftCoin:selectedCoin.code,
|
||||
@@ -98,6 +130,10 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -114,6 +150,8 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div id="ss-server-errors" class="text-danger"></div>
|
||||
<p>This will generate a piece of code based on Sideshift configuration that can work as a valid destination in prism. Prism will then generate a "shift" on Sideshift and send the funds through LN to it, and Sideshift will send you the conversion. </p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Which coin should Sideshift send you</label>
|
||||
@@ -141,6 +179,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
<div id="ss-result" class="form-group mt-4" style="display: none;">
|
||||
<label class="form-label">Generated code</label>
|
||||
<input type="text" id="ss-result-txt" class="form-control" readonly="readonly"/>
|
||||
<p id="ss-result-additional-info"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user